From 3628fef3f54f520b548b21ed4a8c5989f562cffd Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 11 Dec 2025 02:00:54 +1300 Subject: [PATCH] feat: add support for project seed and seed xlsform APIs in the SDK The two new commands and sdk functions will allow clients to download information about the project creation from a seed. --- qfieldcloud_sdk/cli.py | 51 ++++++++++++++++++++++++++++ qfieldcloud_sdk/sdk.py | 76 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/qfieldcloud_sdk/cli.py b/qfieldcloud_sdk/cli.py index 56a611a..19feda8 100755 --- a/qfieldcloud_sdk/cli.py +++ b/qfieldcloud_sdk/cli.py @@ -245,6 +245,57 @@ def get_project(ctx: Context, project_id: str) -> None: log("User does not have access to projects yet.") +@cli.command() +@click.argument("project_id") +@click.pass_context +def get_project_seed(ctx: Context, project_id: str) -> None: + """Get QFieldCloud project seed data.""" + + project_seed: Dict[str, Any] = ctx.obj["client"].get_project_seed(project_id) + + if ctx.obj["format_json"]: + print_json(project_seed) + else: + if project_seed: + log("Project name: {}".format(project_seed["name"])) + log("Project CRS: {}".format(project_seed["crs"])) + log( + "Project extent: {}".format( + ", ".join(map(lambda n: str(n), project_seed["extent"])) + ) + ) + log( + "Project basemaps: {}".format(len(project_seed["settings"]["basemaps"])) + ) + log("Project XLSForm: {}".format(bool(project_seed["settings"]["xlsform"]))) + else: + log("User does not have access to projects yet.") + + +@cli.command() +@click.argument("project_id") +@click.argument("destination_dir") +@click.pass_context +def get_project_seed_xlsform( + ctx: Context, + project_id: str, + destination_dir: str, +) -> None: + """Get QFieldCloud project seed XLSForm file.""" + + xlsform_filename = ctx.obj["client"].get_project_seed_xlsform( + project_id, destination_dir + ) + + if ctx.obj["format_json"]: + print_json(xlsform_filename) + else: + if xlsform_filename: + log(f"XLSForm seed file downloaded to: {xlsform_filename}") + else: + log("No XLSForm seed file found for the project.") + + @cli.command() @click.argument("project_id") @click.option( diff --git a/qfieldcloud_sdk/sdk.py b/qfieldcloud_sdk/sdk.py index 27d8f35..d477f70 100644 --- a/qfieldcloud_sdk/sdk.py +++ b/qfieldcloud_sdk/sdk.py @@ -3,13 +3,15 @@ import logging import os import sys +import requests +import urllib3 +import cgi + from enum import Enum from pathlib import Path from typing import Any, Callable, Dict, List, Optional, TypedDict, Union, cast from urllib import parse as urlparse -import requests -import urllib3 from requests.adapters import HTTPAdapter, Retry from requests_toolbelt.multipart.encoder import MultipartEncoderMonitor @@ -76,6 +78,7 @@ class JobTypes(str, Enum): PACKAGE = "package" APPLY_DELTAS = "delta_apply" PROCESS_PROJECTFILE = "process_projectfile" + CREATE_PROJECT = "create_project" class ProjectCollaboratorRole(str, Enum): @@ -399,6 +402,75 @@ def get_project( return cast(Dict, payload) + def get_project_seed( + self, + project_id: str, + ) -> Dict[str, Any]: + """Get project seed data. + + Args: + project_id: the project data to get seed data for. + + Returns: + A dictionary containing project seed. + + Example: + ```python + client.get_project_seed(project_id) + ``` + """ + payload = self._request_json("GET", f"projects/{project_id}/seed") + + return cast(Dict, payload) + + def get_project_seed_xlsform( + self, + project_id: str, + destination_dir: str, + ) -> str | None: + """Get project seed XLSForm file content. + + Args: + project_id: the project data to get seed XLSForm for. + + Returns: + The name of the downloaded XLSForm file. + + Example: + ```python + client.get_project_seed_xlsform(project_id) + ``` + """ + + resp = self._request("GET", f"projects/{project_id}/seed/xlsform") + + if resp.status_code != 200: + return None + + content_disposition = resp.headers.get("Content-Disposition", "") + + if not content_disposition: + logger.warning( + "Response has no `Content-Disposition` header. Skip download of XLSForm file!" + ) + + return None + + _value, params = cgi.parse_header(content_disposition) + filename = params.get("filename") + + if not filename: + logger.warning( + "Response has no filename in `Content-Disposition` header. Skip download of XLSForm file!" + ) + + return None + + path = Path(destination_dir).joinpath(filename) + path.write_bytes(resp.content) + + return str(path) + def list_remote_files( self, project_id: str, skip_metadata: bool = True ) -> List[Dict[str, Any]]: