Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 105 additions & 4 deletions mergin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
from enum import Enum
from typing import Optional, Type, Union

from .models import (
ProjectDelta,
ProjectDeltaItemDiff,
ProjectDeltaItem,
ProjectResponse,
ProjectFile,
ProjectWorkspace,
)

from .common import (
SYNC_ATTEMPT_WAIT,
SYNC_ATTEMPTS,
Expand Down Expand Up @@ -732,6 +741,90 @@ def project_info(self, project_path_or_id, since=None, version=None):
resp = self.get("/v1/project/{}".format(project_path_or_id), params)
return json.load(resp)

def project_info_v2(self, project_id: str, files_at_version=None) -> ProjectResponse:
"""
Fetch info about project.

:param project_id: Project's id
:type project_id: String
:param files_at_version: Version to track files at given version
:type files_at_version: String
"""
self.check_v2_project_info_support()

params = {}
if files_at_version:
params = {"files_at_version": files_at_version}
resp = self.get(f"/v2/projects/{project_id}", params)
resp_json = json.load(resp)
project_workspace = resp_json.get("workspace")
return ProjectResponse(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to ProjectInfo

id=resp_json.get("id"),
name=resp_json.get("name"),
created_at=resp_json.get("created_at"),
updated_at=resp_json.get("updated_at"),
version=resp_json.get("version"),
public=resp_json.get("public"),
role=resp_json.get("role"),
size=resp_json.get("size"),
workspace=ProjectWorkspace(
id=project_workspace.get("id"),
name=project_workspace.get("name"),
),
files=[
ProjectFile(
checksum=f.get("checksum"),
mtime=f.get("mtime"),
path=f.get("path"),
size=f.get("size"),
)
for f in resp_json.get("files", [])
],
)

def get_project_delta(self, project_id: str, since: str, to: typing.Optional[str] = None) -> ProjectDelta:
"""
Fetch info about project delta since given version.

:param project_id: Project's id
:type project_id: String
:param since: Version to track history of files from
:type since: String
:param to: Optional version to track history of files to, if not given latest version is used
:type since: String
:rtype: Dict
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove noise

"""
# If it is not enabled on the server, raise error
if not self.server_features().get("v2_pull_enabled", False):
raise ClientError("Project delta is not supported by the server")

params = {"since": since}
if to:
params["to"] = to
resp = self.get(f"/v2/projects/{project_id}/delta", params)
resp_parsed = json.load(resp)
return ProjectDelta(
to_version=resp_parsed.get("to_version"),
items=[
ProjectDeltaItem(
path=item["path"],
size=item.get("size"),
checksum=item.get("checksum"),
version=item.get("version"),
change=item.get("change"),
diffs=(
[
ProjectDeltaItemDiff(
id=diff.get("id"),
)
for diff in item.get("diffs", [])
]
),
)
Comment on lines +805 to +823
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_project_delta() drops diff metadata (size, version) and may set size/checksum/version to None if the server omits fields. This makes it impossible to precompute download sizes for diff items and risks runtime errors where numeric sizes are expected. Parse size/version from each diff entry (and provide safe defaults like size=0, checksum="") when constructing ProjectDeltaItem/ProjectDeltaItemDiff.

Copilot uses AI. Check for mistakes.
for item in resp_parsed.get("items", [])
],
)

def paginated_project_versions(self, project_path, page, per_page=100, descending=False):
"""
Get records of project's versions (history) using calculated pagination.
Expand Down Expand Up @@ -822,11 +915,11 @@ def download_project(self, project_path, directory, version=None):
:param project_path: Project's full name (<namespace>/<name>)
:type project_path: String

:param version: Project version to download, e.g. v42
:type version: String

:param directory: Target directory
:type directory: String

:param version: Project version to download, e.g. v42
:type version: String
"""
job = download_project_async(self, project_path, directory, version)
download_project_wait(job)
Expand Down Expand Up @@ -1341,13 +1434,21 @@ def check_collaborators_members_support(self):
if not is_version_acceptable(self.server_version(), f"{min_version}"):
raise NotImplementedError(f"This needs server at version {min_version} or later")

def check_v2_project_info_support(self):
"""
Check if the server is compatible with v2 endpoint for project info
"""
min_version = "2025.8.2"
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_v2_project_info_support() uses min_version = "2025.8.2", but utils.is_version_acceptable() only compares major/minor and ignores the patch component. This means servers like 2025.8.0 would incorrectly pass the check. Either change min_version to "2025.8" or enhance is_version_acceptable() to compare patch versions too (and keep call sites consistent).

Suggested change
min_version = "2025.8.2"
min_version = "2025.8"

Copilot uses AI. Check for mistakes.
if not is_version_acceptable(self.server_version(), f"{min_version}"):
raise NotImplementedError(f"This needs server at version {min_version} or later")

def create_user(
self,
email: str,
password: str,
workspace_id: int,
workspace_role: Union[str, WorkspaceRole],
username: str = None,
username: Optional[str] = None,
notify_user: bool = False,
) -> dict:
"""
Expand Down
Loading
Loading