Skip to content

Commit 4767c67

Browse files
committed
Separate download single file to dedicated API call
1 parent d62ff99 commit 4767c67

4 files changed

Lines changed: 76 additions & 44 deletions

File tree

mergin/cli.py

Lines changed: 14 additions & 12 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
)
@@ -348,19 +349,20 @@ def download_file(ctx, filepath, output, version, project):
348349
mc = ctx.obj["client"]
349350
if mc is None:
350351
return
351-
if project is None:
352-
# no --project given, so we default to the current directory - make sure that's actually a checked out project
353-
try:
354-
MerginProject(os.getcwd()).project_full_name()
355-
except InvalidProject:
356-
click.secho(
357-
"Current directory is not a Mergin Maps project. Run this command from within a "
358-
"checked out project directory, or pass --project <workspace>/<project>.",
359-
fg="red",
360-
)
361-
return
362352
try:
363-
job = download_file_async(mc, project or os.getcwd(), filepath, output, version)
353+
if project is not None:
354+
job = download_project_file_async(mc, project, filepath, output, version)
355+
else:
356+
try:
357+
MerginProject(os.getcwd()).project_full_name()
358+
except InvalidProject:
359+
click.secho(
360+
"Current directory is not a Mergin Maps project. Run this command from within a "
361+
"checked out project directory, or pass --project <workspace>/<project>.",
362+
fg="red",
363+
)
364+
return
365+
job = download_file_async(mc, os.getcwd(), filepath, output, version)
364366
with click.progressbar(length=job.total_size) as bar:
365367
last_transferred_size = 0
366368
while download_project_is_running(job):

mergin/client.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
download_file_async,
4747
download_files_async,
4848
download_files_finalize,
49+
download_project_file_async,
4950
download_diffs_async,
5051
download_project_finalize,
5152
download_project_wait,
@@ -1199,7 +1200,7 @@ def download_file(self, project_dir, file_path, output_filename, version=None):
11991200
"""
12001201
Download project file at specified version. Get the latest if no version specified.
12011202
1202-
:param project_dir: project local directory or a full project name ("<workspace>/<project>")
1203+
:param project_dir: project local directory
12031204
:type project_dir: String
12041205
:param file_path: relative path of file to download in the project directory
12051206
:type file_path: String
@@ -1212,6 +1213,24 @@ def download_file(self, project_dir, file_path, output_filename, version=None):
12121213
pull_project_wait(job)
12131214
download_file_finalize(job)
12141215

1216+
def download_project_file(self, project_path, file_path, output_filename, version=None):
1217+
"""
1218+
Download a single project file at specified version directly from the server, without
1219+
needing an existing local project checkout.
1220+
1221+
:param project_path: full project name ("<workspace>/<project>")
1222+
:type project_path: String
1223+
:param file_path: relative path of file to download in the project directory
1224+
:type file_path: String
1225+
:param output_filename: full destination path for saving the downloaded file
1226+
:type output_filename: String
1227+
:param version: optional version tag for downloaded file
1228+
:type version: String
1229+
"""
1230+
job = download_project_file_async(self, project_path, file_path, output_filename, version=version)
1231+
pull_project_wait(job)
1232+
download_file_finalize(job)
1233+
12151234
def get_file_diff(self, project_dir, file_path, output_diff, version_from, version_to):
12161235
"""Create concatenated diff for project file diffs between versions version_from and version_to.
12171236
@@ -1401,11 +1420,11 @@ def download_files(
14011420
"""
14021421
Download project files at specified version. Get the latest if no version specified.
14031422
1404-
:param project_dir: project local directory or a full project name ("<workspace>/<project>")
1423+
:param project_dir: project local directory
14051424
:type project_dir: String
14061425
:param file_path: List of relative paths of files to download in the project directory
14071426
:type file_path: List[String]
1408-
:param output_paths: List of paths for files to download to. Should be same length of as file_path. Default is `None` which means that files are downloaded into MerginProject at project_dir (only valid when project_dir is an existing local checkout).
1427+
:param output_paths: List of paths for files to download to. Should be same length of as file_path. Default is `None` which means that files are downloaded into MerginProject at project_dir.
14091428
:type output_paths: List[String]
14101429
:param version: optional version tag for downloaded file
14111430
:type version: String

mergin/client_pull.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import concurrent.futures
2323

2424

25-
from .common import CHUNK_SIZE, ClientError, DeltaChangeType, InvalidProject, PullActionType
25+
from .common import CHUNK_SIZE, ClientError, DeltaChangeType, PullActionType
2626
from .models import ProjectDelta, ProjectDeltaChange, PullAction
2727
from .merginproject import MerginProject
2828
from .utils import cleanup_tmp_dir, is_versioned_file, save_to_file
@@ -82,8 +82,8 @@ def dump(self):
8282

8383
class DownloadScratchContext:
8484
"""
85-
Minimal stand-in for MerginProject, used by download_files_async() when downloading files
86-
directly by project name ("<workspace>/<project>") without an existing local project checkout.
85+
Minimal stand-in for MerginProject used when downloading a file directly by
86+
project name ("<workspace>/<project>") without an existing local project checkout.
8787
8888
Provides only what the shared download job code actually needs from MerginProject.
8989
"""
@@ -793,6 +793,23 @@ def download_file_finalize(job):
793793
download_files_finalize(job)
794794

795795

796+
def download_project_file_async(mc, project_path: str, file_path: str, output_file: str, version: str = None):
797+
"""
798+
Starts background download of a single project file at specified version, fetched directly
799+
from the server without needing an existing local project checkout.
800+
Returns handle to the pending download.
801+
802+
:param project_path: full project name ("<workspace>/<project>")
803+
:param output_file: destination path for the downloaded file
804+
"""
805+
if not output_file:
806+
raise ClientError("output_file must be provided when downloading a file without a local project checkout")
807+
808+
tmp_dir = tempfile.TemporaryDirectory(prefix="python-api-client-")
809+
mp = DownloadScratchContext(mc, tmp_dir.name)
810+
return _download_files_async(mc, mp, project_path, [file_path], [output_file], version, tmp_dir)
811+
812+
796813
def download_diffs_async(mc, project_directory, file_path, versions):
797814
"""
798815
Starts background download project file diffs for specified versions.
@@ -916,35 +933,29 @@ def download_diffs_finalize(job: PullJob) -> List[str]:
916933

917934

918935
def download_files_async(
919-
mc, project_dir: str, file_paths: typing.List[str], output_paths: typing.List[str], version: str
936+
mc, project_dir: str, file_paths: typing.List[str], output_paths: typing.List[str] = None, version: str = None
920937
):
921938
"""
922939
Starts background download project files at specified version.
923940
Returns handle to the pending download.
924941
925-
`project_dir` can either be an existing local project directory (previously fetched with
926-
download_project()), or a full project name ("<workspace>/<project>") to download files
927-
directly from the server without needing a local checkout. In the latter case, `output_paths`
928-
must be provided explicitly, as there is no project directory to place files into by default.
942+
`project_dir` must be an existing local project directory.
929943
"""
930-
# temporary directory to stage downloaded chunks in
944+
mp = MerginProject(project_dir)
945+
project_path = mp.project_full_name()
931946
tmp_dir = tempfile.TemporaryDirectory(prefix="python-api-client-")
947+
return _download_files_async(mc, mp, project_path, file_paths, output_paths, version, tmp_dir)
932948

933-
mp: Union[MerginProject, "DownloadScratchContext"]
934-
try:
935-
mp = MerginProject(project_dir)
936-
project_path = mp.project_full_name()
937-
except InvalidProject:
938-
# project_dir is not an existing local checkout - treat it as a full project name
939-
# ("<workspace>/<project>") and download straight from the server instead
940-
if output_paths is None:
941-
cleanup_tmp_dir(mc, tmp_dir)
942-
raise ClientError(
943-
"output_paths must be provided when downloading files without an existing local project checkout"
944-
)
945-
project_path = project_dir
946-
mp = DownloadScratchContext(mc, tmp_dir.name)
947949

950+
def _download_files_async(
951+
mc,
952+
mp: Union[MerginProject, "DownloadScratchContext"],
953+
project_path: str,
954+
file_paths: typing.List[str],
955+
output_paths: typing.List[str],
956+
version: str,
957+
tmp_dir: tempfile.TemporaryDirectory,
958+
):
948959
ver_info = f"at version {version}" if version is not None else "at latest version"
949960
mp.log.info(f"Getting [{', '.join(file_paths)}] {ver_info}")
950961
latest_proj_info = mc.project_info(project_path)

mergin/test/test_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,23 +1371,23 @@ def test_download_file_without_checkout(mc):
13711371
f_downloaded = os.path.join(download_dir, f_updated)
13721372

13731373
expected_content = "inserted_1_A.gpkg"
1374-
mc.download_file(project, f_updated, f_downloaded, version="v2")
1374+
mc.download_project_file(project, f_updated, f_downloaded, version="v2")
13751375
expected = os.path.join(TEST_DATA_DIR, expected_content)
13761376
assert check_gpkg_same_content(MerginProject(project_dir), f_downloaded, expected)
13771377
assert not os.path.exists(os.path.join(download_dir, ".mergin"))
13781378

1379-
# output_paths must be provided explicitly when there is no local checkout
1380-
with pytest.raises(ClientError, match="output_paths must be provided"):
1381-
mc.download_files(project, [f_updated])
1379+
# output_file must be provided explicitly when there is no local checkout
1380+
with pytest.raises(ClientError, match="output_file must be provided"):
1381+
mc.download_project_file(project, f_updated, None)
13821382

13831383
# non-existent file in an existing project - same error as with a local checkout
13841384
with pytest.raises(ClientError, match=r"No \[does_not_exist\.gpkg\] exists at version v2"):
1385-
mc.download_file(project, "does_not_exist.gpkg", f_downloaded, version="v2")
1385+
mc.download_project_file(project, "does_not_exist.gpkg", f_downloaded, version="v2")
13861386

13871387
# non-existent / inaccessible project should fail clearly too
13881388
nonexistent_project = create_project_path("this_project_does_not_exist", mc)
13891389
with pytest.raises(ClientError):
1390-
mc.download_file(nonexistent_project, f_updated, f_downloaded)
1390+
mc.download_project_file(nonexistent_project, f_updated, f_downloaded)
13911391

13921392

13931393
def test_download_diffs(mc):

0 commit comments

Comments
 (0)