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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ htmlcov
deps
venv
.vscode/settings.json
debug.py
130 changes: 126 additions & 4 deletions mergin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
import typing
import warnings

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

from .common import (
ClientError,
LoginError,
Expand Down Expand Up @@ -119,6 +128,7 @@ def __init__(
self._user_info = None
self._server_type = None
self._server_version = None
self._server_features = {}
self.client_version = "Python-client/" + __version__
if plugin_version is not None: # this could be e.g. "Plugin/2020.1 QGIS/3.14"
self.client_version += " " + plugin_version
Expand Down Expand Up @@ -309,6 +319,13 @@ def delete(self, path, validate_auth=True):
request = urllib.request.Request(url, method="DELETE")
return self._do_request(request, validate_auth=validate_auth)

def head(self, path, data=None, headers={}, validate_auth=True):
url = urllib.parse.urljoin(self.url, urllib.parse.quote(path))
if data:
url += "?" + urllib.parse.urlencode(data)
request = urllib.request.Request(url, headers=headers, method="HEAD")
return self._do_request(request, validate_auth=validate_auth)

def login(self, login, password):
"""
Authenticate login credentials and store session token
Expand Down Expand Up @@ -412,6 +429,19 @@ def server_version(self):

return self._server_version

def server_features(self):
"""
Returns feature flags of the server.
"""
if self._server_features:
return self._server_features
config = self.server_config()
self._server_features = {
"v2_push_enabled": config.get("v2_push_enabled", False),
"v2_pull_enabled": config.get("v2_pull_enabled", False),
}
return self._server_features

def workspaces_list(self):
"""
Find all available workspaces
Expand Down Expand Up @@ -699,6 +729,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(
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
"""
# 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", [])
]
),
)
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 @@ -789,11 +903,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 @@ -1308,13 +1422,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"
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: WorkspaceRole,
username: str = None,
username: typing.Optional[str] = None,
notify_user: bool = False,
) -> dict:
"""
Expand Down
Loading