Skip to content

Commit ae3e55c

Browse files
committed
Added return types fo new functions
+ descriptions + tests for appl_pull_actions + restore get_pull_changes for backward compatibility with project status
1 parent 68b84c1 commit ae3e55c

File tree

7 files changed

+680
-170
lines changed

7 files changed

+680
-170
lines changed

mergin/client.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@
1717
import typing
1818
import warnings
1919

20-
from mergin.models import ProjectDelta, ProjectDeltaItemDiff, ProjectDeltaItem
20+
from mergin.models import (
21+
ProjectDelta,
22+
ProjectDeltaItemDiff,
23+
ProjectDeltaItem,
24+
ProjectResponse,
25+
ProjectFile,
26+
ProjectWorkspace,
27+
)
2128

2229
from .common import (
2330
ClientError,
@@ -722,7 +729,7 @@ def project_info(self, project_path_or_id, since=None, version=None):
722729
resp = self.get("/v1/project/{}".format(project_path_or_id), params)
723730
return json.load(resp)
724731

725-
def project_info_v2(self, project_id: str, files_at_version=None):
732+
def project_info_v2(self, project_id: str, files_at_version=None) -> ProjectResponse:
726733
"""
727734
Fetch info about project.
728735
@@ -731,11 +738,37 @@ def project_info_v2(self, project_id: str, files_at_version=None):
731738
:param files_at_version: Version to track files at given version
732739
:type files_at_version: String
733740
"""
741+
self.check_v2_project_info_support()
742+
734743
params = {}
735744
if files_at_version:
736745
params = {"files_at_version": files_at_version}
737746
resp = self.get(f"/v2/projects/{project_id}", params)
738-
return json.load(resp)
747+
resp_json = json.load(resp)
748+
project_workspace = resp_json.get("workspace", {})
749+
return ProjectResponse(
750+
id=resp_json.get("id"),
751+
name=resp_json.get("name"),
752+
created_at=resp_json.get("created_at"),
753+
updated_at=resp_json.get("updated_at"),
754+
version=resp_json.get("version"),
755+
public=resp_json.get("public"),
756+
role=resp_json.get("role"),
757+
size=resp_json.get("size"),
758+
workspace=ProjectWorkspace(
759+
id=project_workspace.get("id"),
760+
name=project_workspace.get("name"),
761+
),
762+
files=[
763+
ProjectFile(
764+
checksum=f.get("checksum"),
765+
mtime=f.get("mtime"),
766+
path=f.get("path"),
767+
size=f.get("size"),
768+
)
769+
for f in resp_json.get("files", [])
770+
],
771+
)
739772

740773
def get_project_delta(self, project_id: str, since: str, to: typing.Optional[str] = None) -> ProjectDelta:
741774
"""
@@ -749,6 +782,10 @@ def get_project_delta(self, project_id: str, since: str, to: typing.Optional[str
749782
:type since: String
750783
:rtype: Dict
751784
"""
785+
# If it is not enabled on the server, raise error
786+
if not self.server_features().get("v2_pull_enabled", False):
787+
raise ClientError("Project delta is not supported by the server")
788+
752789
params = {"since": since}
753790
if to:
754791
params["to"] = to
@@ -1114,7 +1151,7 @@ def project_status(self, directory):
11141151
mp = MerginProject(directory)
11151152
server_info = self.project_info(mp.project_full_name(), since=mp.version())
11161153

1117-
pull_changes = mp.get_pull_delta(server_info["files"])
1154+
pull_changes = mp.get_pull_changes(server_info["files"])
11181155

11191156
push_changes = mp.get_push_changes()
11201157
push_changes_summary = mp.get_list_of_push_changes(push_changes)
@@ -1385,13 +1422,21 @@ def check_collaborators_members_support(self):
13851422
if not is_version_acceptable(self.server_version(), f"{min_version}"):
13861423
raise NotImplementedError(f"This needs server at version {min_version} or later")
13871424

1425+
def check_v2_project_info_support(self):
1426+
"""
1427+
Check if the server is compatible with v2 endpoint for project info
1428+
"""
1429+
min_version = "2025.8.2"
1430+
if not is_version_acceptable(self.server_version(), f"{min_version}"):
1431+
raise NotImplementedError(f"This needs server at version {min_version} or later")
1432+
13881433
def create_user(
13891434
self,
13901435
email: str,
13911436
password: str,
13921437
workspace_id: int,
13931438
workspace_role: WorkspaceRole,
1394-
username: str = None,
1439+
username: typing.Optional[str] = None,
13951440
notify_user: bool = False,
13961441
) -> dict:
13971442
"""

mergin/client_pull.py

Lines changed: 18 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
import tempfile
1818
import typing
1919
import traceback
20+
from dataclasses import asdict
2021

2122
import concurrent.futures
2223

23-
from dataclasses import asdict
2424

2525
from .common import CHUNK_SIZE, ClientError, DeltaChangeType, PullActionType
2626
from .models import ProjectDelta, ProjectDeltaItem, PullAction
@@ -135,7 +135,7 @@ def merge(self):
135135
raise ClientError("Download of file {} failed. Please try it again.".format(self.dest_file))
136136

137137

138-
def get_download_items(file_path: str, file_size: int, file_version: str, directory, diff_only=False):
138+
def get_download_items(file_path: str, file_size: int, file_version: str, directory: str, diff_only=False):
139139
"""Returns an array of download queue items"""
140140

141141
file_dir = os.path.dirname(os.path.normpath(os.path.join(directory, file_path)))
@@ -433,6 +433,7 @@ def __init__(
433433
)
434434
self.mc = mc
435435
self.futures = [] # list of concurrent.futures.Future instances
436+
self.v2_pull = mc.server_features().get("v2_pull_enabled", False)
436437

437438
def dump(self):
438439
print("--- JOB ---", self.total_size, "bytes")
@@ -447,7 +448,7 @@ def dump(self):
447448
print("--- END ---")
448449

449450

450-
def prepare_chunks_destination(target_dir: str, path: str) -> str:
451+
def prepare_file_destination(target_dir: str, path: str) -> str:
451452
"""Prepares destination path for downloaded files chunks"""
452453

453454
# figure out destination path for the file
@@ -458,46 +459,19 @@ def prepare_chunks_destination(target_dir: str, path: str) -> str:
458459
return dest_file_path
459460

460461

461-
_pulling_file_with_diffs = lambda f: "diffs" in f and len(f["diffs"]) != 0
462-
463-
464462
def get_diff_merge_files(delta_item: ProjectDeltaItem, target_dir: str) -> List[FileToMerge]:
465463
"""
466-
Extracts list of diff files to be downloaded delta item.
464+
Extracts list of diff files to be downloaded from delta item using v1 endpoint.
467465
"""
468466
result = []
469467

470468
for diff in delta_item.diffs:
471-
dest_file_path = prepare_chunks_destination(target_dir, diff.id)
469+
dest_file_path = prepare_file_destination(target_dir, diff.id)
472470
download_items = get_download_items(diff.id, diff.size, diff.version, target_dir, True)
473471
result.append(FileToMerge(dest_file_path, download_items))
474472
return result
475473

476474

477-
def get_delta_download_files(
478-
delta_items: List[ProjectDeltaItem], target_dir: str
479-
) -> Tuple[List[FileToMerge], List[DownloadDiffQueueItem]]:
480-
"""
481-
Extracts list of files to be merged from delta dictionary. If there is any diff files to be downloaded, they
482-
are returned as a separate list.
483-
"""
484-
merge_files = []
485-
diff_files = []
486-
for item in delta_items:
487-
change = item.change
488-
if change == DeltaChangeType.CREATE or change == DeltaChangeType.UPDATE:
489-
path = item.path
490-
download_items = get_download_items(asdict(item), target_dir, False)
491-
dest_file_path = prepare_chunks_destination(target_dir, path)
492-
merge_files.append(FileToMerge(dest_file_path, download_items))
493-
elif change == DeltaChangeType.UPDATE_DIFF:
494-
diffs = item.diffs
495-
for diff in diffs:
496-
diff_path = diff.id
497-
diff_files.append(DownloadDiffQueueItem(diff_path, os.path.join(target_dir, diff_path)))
498-
return merge_files, diff_files
499-
500-
501475
def pull_project_async(mc, directory) -> Optional[PullJob]:
502476
"""
503477
Starts project pull in background and returns handle to the pending job.
@@ -531,7 +505,7 @@ def pull_project_async(mc, directory) -> Optional[PullJob]:
531505
else:
532506
server_info = mc.project_info(project_path, since=local_version)
533507
server_version = server_info.get("version")
534-
delta = ProjectDelta(to_version=server_version, items=mp.get_pull_delta(server_info))
508+
delta = mp.get_pull_delta(server_info)
535509
except ClientError as err:
536510
mp.log.error("Error getting project info: " + str(err))
537511
mp.log.info("--- pull aborted")
@@ -562,13 +536,7 @@ def pull_project_async(mc, directory) -> Optional[PullJob]:
562536
continue # no action needed
563537

564538
pull_actions.append(PullAction(pull_action, item, local_item))
565-
if pull_action == PullActionType.COPY:
566-
# simply download the server version of the files
567-
dest_file_path = prepare_chunks_destination(tmp_dir.name, item.path)
568-
download_items = get_download_items(item.path, item.size, server_version, mp.cache_dir)
569-
merge_files.append(FileToMerge(dest_file_path, download_items))
570-
571-
elif pull_action == PullActionType.APPLY_DIFF or (
539+
if pull_action == PullActionType.APPLY_DIFF or (
572540
pull_action == PullActionType.COPY_CONFLICT and item.change == DeltaChangeType.UPDATE_DIFF
573541
):
574542
# if we have diff to apply, let's download the diff files
@@ -594,12 +562,10 @@ def pull_project_async(mc, directory) -> Optional[PullJob]:
594562
dest_file_path = mp.fpath(item.path, tmp_dir.name)
595563
merge_files.append(FileToMerge(dest_file_path, items))
596564
basefiles_to_patch.append((item.path, [diff.id for diff in item.diffs]))
597-
598-
elif pull_action == PullActionType.COPY_CONFLICT:
599-
# if we have conflict and create or update action, let's just download the server version of the file
600-
# let's download server version of the file
601-
dest_file_path = prepare_chunks_destination(tmp_dir.name, item.path)
602-
download_items = get_download_items(item.path, item.size, server_version, mp.cache_dir)
565+
elif pull_action == PullActionType.COPY or pull_action == PullActionType.COPY_CONFLICT:
566+
# simply download the server version of the files
567+
dest_file_path = prepare_file_destination(tmp_dir.name, item.path)
568+
download_items = get_download_items(item.path, item.size, server_version, tmp_dir.name)
603569
merge_files.append(FileToMerge(dest_file_path, download_items))
604570
# Do nothing for DELETE actions
605571

@@ -696,8 +662,11 @@ def pull_project_finalize(job: PullJob):
696662
raise future.exception()
697663

698664
job.mp.log.info("finalizing pull")
699-
if not job.project_info:
700-
job.project_info = job.mc.project_info_v2(job.mp.project_id(), files_at_version=job.version)
665+
if not job.project_info and job.v2_pull:
666+
project_info_response = job.mc.project_info(job.project_path, version=job.version)
667+
job.project_info = asdict(project_info_response)
668+
else:
669+
raise ClientError("Missing project info for pull finalization")
701670

702671
# merge downloaded chunks
703672
try:
@@ -729,7 +698,7 @@ def pull_project_finalize(job: PullJob):
729698
job.mp.log.info("--- pull aborted")
730699
os.remove(basefile)
731700
raise ClientError("Cannot patch basefile {}! Please try syncing again.".format(basefile))
732-
701+
conflicts = []
733702
try:
734703
if job.pull_actions:
735704
conflicts = job.mp.apply_pull_actions(job.pull_actions, job.tmp_dir.name, job.project_info, job.mc)

0 commit comments

Comments
 (0)