Skip to content

Commit 9678a5d

Browse files
committed
Merge remote-tracking branch 'origin/master' into fix/sync_loop_for_too_long_path
2 parents 5c14a10 + f37e56b commit 9678a5d

18 files changed

Lines changed: 699 additions & 70 deletions
Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,16 @@
1-
name: Python-api QA (Security & Style)
1+
name: QGIS Plugin Repository Checks
22

3-
# Trigger the workflow on every push
43
on: [push]
54

65
jobs:
7-
quality-assurance:
6+
qgis_repo_checks:
7+
name: Mirror plugins.qgis.org upload scan
88
runs-on: ubuntu-latest
99
steps:
10-
- name: Checkout code
11-
uses: actions/checkout@v4
10+
- uses: actions/checkout@v4
1211

13-
- name: Set up Python
14-
uses: actions/setup-python@v4
12+
# This package ships inside the QGIS plugin zip, so the scan that gates it is
13+
# the plugin's. The action lives there to keep one copy of the rule snapshot.
14+
- uses: MerginMaps/qgis-plugin/.github/actions/qgis-repo-checks@master
1515
with:
16-
python-version: '3.10'
17-
18-
- name: Install dependencies
19-
run: |
20-
# Upgrade pip and install security/linting tools
21-
python -m pip install --upgrade pip
22-
pip install bandit detect-secrets
23-
24-
# - name: Install dependencies
25-
# run: |
26-
# # Upgrade pip and install security/linting tools
27-
# python -m pip install --upgrade pip
28-
# pip install bandit detect-secrets flake8 flake8-json
29-
30-
- name: Run Bandit (Security Scan)
31-
# Scan the mergin folder for vulnerabilities, excluding the test directory
32-
run: bandit -r ./mergin/ -ll --exclude ./mergin/test
33-
34-
- name: Run Detect Secrets
35-
# Scan the plugin directory for hardcoded secrets/credentials
36-
run: detect-secrets scan ./mergin/ --all-files
37-
38-
# - name: Run Flake8 (Style Check)
39-
# # Style enforcement using MerginMaps standards
40-
# # Ignoring E501 (line length) and W503 (operator line breaks)
41-
# run: |
42-
# flake8 ./mergin/ --max-line-length=120 --ignore=E501,W503 --exclude=test
16+
root: mergin

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ deps
1414
venv
1515
debug.py
1616
.vscode/
17+
.python-version

‎.pre-commit-config.yaml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Black version is kept in sync with .github/workflows/code_style.yml
2+
repos:
3+
- repo: https://github.com/psf/black-pre-commit-mirror
4+
rev: 26.1.0
5+
hooks:
6+
- id: black
7+
args: ["--line-length=120"]

‎CITATION.cff‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cff-version: 1.2.0
2+
message: "If you use this software, please cite it as below."
3+
title: "Mergin Maps Python Client"
4+
type: software
5+
authors:
6+
- name: "Lutra Consulting Ltd."
7+
url: "https://pypi.org/project/mergin-client/"
8+
repository-code: "https://github.com/MerginMaps/python-api-client"
9+
license: MIT

‎README.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99

1010
This repository contains a Python client module for access to [Mergin Maps](https://merginmaps.com/) service and a command-line tool for easy access to data stored in Mergin Maps.
1111

12-
<div><img align="left" width="45" height="45" src="https://raw.githubusercontent.com/MerginMaps/docs/main/src/public/slack.svg"><a href="https://merginmaps.com/community/join">Join our community chat</a><br/>and ask questions!</div><br />
13-
12+
<a href="https://community.merginmaps.com">Join our community</a> and ask questions!
1413

1514
To install the module:
1615
```bash
@@ -192,3 +191,13 @@ For running test do:
192191
pip install pytest pytest-cov coveralls
193192
pytest --cov-report html --cov=mergin mergin/test/
194193
```
194+
195+
### Code style
196+
197+
Code is formatted with [black](https://github.com/psf/black) (line length 120), enforced in CI.
198+
To run the same check automatically before each commit, install the [pre-commit](https://pre-commit.com/) hooks once:
199+
200+
```bash
201+
pip install pre-commit
202+
pre-commit install
203+
```

‎mergin/cli.py‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
download_project_cancel,
3030
download_file_async,
3131
download_file_finalize,
32+
download_project_file_async,
3233
download_project_finalize,
3334
download_project_is_running,
3435
)
@@ -248,16 +249,20 @@ def list_projects(ctx, name, namespace, order_params):
248249
@click.argument("project")
249250
@click.argument("directory", type=click.Path(), required=False)
250251
@click.option("--version", default=None, help="Version of project to download")
252+
@click.option("--include", multiple=True, help="Only download files matching this pattern, e.g. '*.gpkg'")
253+
@click.option("--exclude", multiple=True, help="Skip files matching this pattern, e.g. 'media/*'")
251254
@click.pass_context
252-
def download(ctx, project, directory, version):
255+
def download(ctx, project, directory, version, include, exclude):
253256
"""Download last version of mergin project."""
254257
mc = ctx.obj["client"]
255258
if mc is None:
256259
return
260+
if include and exclude:
261+
raise click.UsageError("--include and --exclude cannot be used together")
257262
directory = directory or os.path.basename(project)
258263
click.echo("Downloading into {}".format(directory))
259264
try:
260-
job = download_project_async(mc, project, directory, version)
265+
job = download_project_async(mc, project, directory, version, include=include, exclude=exclude)
261266
with click.progressbar(length=job.total_size) as bar:
262267
last_transferred_size = 0
263268
while download_project_is_running(job):
@@ -335,17 +340,33 @@ def share(ctx, project):
335340
@click.argument("filepath")
336341
@click.argument("output")
337342
@click.option("--version", help="Project version tag, for example 'v3'")
343+
@click.option(
344+
"--project",
345+
help="Full project name ('<workspace>/<project>') to download the file directly from the server. "
346+
"If not given, the current directory is used and must be an existing checked out project.",
347+
)
338348
@click.pass_context
339-
def download_file(ctx, filepath, output, version):
349+
def download_file(ctx, filepath, output, version, project):
340350
"""
341-
Download project file at specified version. `project` needs to be a combination of namespace/project.
342-
If no version is given, the latest will be fetched.
351+
Download project file at specified version. If no version is given, the latest will be fetched.
343352
"""
344353
mc = ctx.obj["client"]
345354
if mc is None:
346355
return
347356
try:
348-
job = download_file_async(mc, os.getcwd(), filepath, output, version)
357+
if project is not None:
358+
job = download_project_file_async(mc, project, filepath, output, version)
359+
else:
360+
try:
361+
MerginProject(os.getcwd()).project_full_name()
362+
except InvalidProject:
363+
click.secho(
364+
"Current directory is not a Mergin Maps project. Run this command from within a "
365+
"checked out project directory, or pass --project <workspace>/<project>.",
366+
fg="red",
367+
)
368+
return
369+
job = download_file_async(mc, os.getcwd(), filepath, output, version)
349370
with click.progressbar(length=job.total_size) as bar:
350371
last_transferred_size = 0
351372
while download_project_is_running(job):

‎mergin/client.py‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
download_file_async,
4646
download_files_async,
4747
download_files_finalize,
48+
download_project_file_async,
4849
download_diffs_async,
4950
download_project_finalize,
5051
download_project_wait,
@@ -62,6 +63,7 @@
6263
from .utils import DateTimeEncoder, get_versions_with_file_changes, int_version, is_version_acceptable
6364
from .utils import (
6465
DateTimeEncoder,
66+
filter_files,
6567
get_versions_with_file_changes,
6668
int_version,
6769
is_version_acceptable,
@@ -903,7 +905,7 @@ def project_versions(self, project_path, since=1, to=None):
903905
filtered_versions = list(filter(lambda v: (num_since <= int_version(v["name"]) <= num_to), versions))
904906
return filtered_versions
905907

906-
def download_project(self, project_path, directory, version=None):
908+
def download_project(self, project_path, directory, version=None, include=None, exclude=None):
907909
"""
908910
Download project into given directory. If version is not specified, latest version is downloaded
909911
@@ -915,8 +917,16 @@ def download_project(self, project_path, directory, version=None):
915917
916918
:param version: Project version to download, e.g. v42
917919
:type version: String
920+
921+
:param include: Optional list of glob patterns (matched against each file's project path, e.g.
922+
"media/*" or "*.gpkg") - only matching files are downloaded.
923+
:type include: List[String]
924+
925+
:param exclude: Optional list of glob patterns - matching files are skipped. Mutually exclusive
926+
with include.
927+
:type exclude: List[String]
918928
"""
919-
job = download_project_async(self, project_path, directory, version)
929+
job = download_project_async(self, project_path, directory, version, include=include, exclude=exclude)
920930
download_project_wait(job)
921931
download_project_finalize(job)
922932

@@ -1158,7 +1168,13 @@ def project_status(self, directory):
11581168
mp = MerginProject(directory)
11591169
server_info = self.project_info(mp.project_full_name(), since=mp.version())
11601170

1171+
mp.update_project_role(server_info["role"])
1172+
11611173
pull_changes = mp.get_pull_changes(server_info.get("files", []), server_info.get("version"))
1174+
# on a sparse checkout, don't report excluded files as pending server changes -
1175+
# they were never meant to be pulled in the first place
1176+
file_filter = mp.file_filter()
1177+
pull_changes = {change_type: filter_files(files, **file_filter) for change_type, files in pull_changes.items()}
11621178

11631179
push_changes = mp.get_push_changes()
11641180
push_changes_summary = mp.get_list_of_push_changes(push_changes)
@@ -1213,6 +1229,24 @@ def download_file(self, project_dir, file_path, output_filename, version=None):
12131229
pull_project_wait(job)
12141230
download_file_finalize(job)
12151231

1232+
def download_project_file(self, project_path, file_path, output_filename, version=None):
1233+
"""
1234+
Download a single project file at specified version directly from the server, without
1235+
needing an existing local project checkout.
1236+
1237+
:param project_path: full project name ("<workspace>/<project>")
1238+
:type project_path: String
1239+
:param file_path: relative path of file to download in the project directory
1240+
:type file_path: String
1241+
:param output_filename: full destination path for saving the downloaded file
1242+
:type output_filename: String
1243+
:param version: optional version tag for downloaded file
1244+
:type version: String
1245+
"""
1246+
job = download_project_file_async(self, project_path, file_path, output_filename, version=version)
1247+
pull_project_wait(job)
1248+
download_file_finalize(job)
1249+
12161250
def get_file_diff(self, project_dir, file_path, output_diff, version_from, version_to):
12171251
"""Create concatenated diff for project file diffs between versions version_from and version_to.
12181252
@@ -1241,7 +1275,7 @@ def get_file_diff(self, project_dir, file_path, output_diff, version_from, versi
12411275
diffs.append(mp.fpath_cache(file_history["history"][v]["diff"]["path"], v))
12421276

12431277
# concatenate diffs, if needed
1244-
output_dir = os.path.dirname(output_diff)
1278+
output_dir = os.path.abspath(os.path.dirname(output_diff))
12451279
if len(diffs) >= 1:
12461280
fs.makedirs(output_dir, exist_ok=True)
12471281
if len(diffs) > 1:

0 commit comments

Comments
 (0)