From ed987788fd26de7e4dfbc579f1f6d0b531ce620c Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Wed, 18 Sep 2024 16:36:42 +0300 Subject: [PATCH 1/2] Add endpoint to patch the project and pretty print project data --- qfieldcloud_sdk/cli.py | 43 +++++++++++++++++++++++++++++++++++++- qfieldcloud_sdk/sdk.py | 44 ++++++++++++++++++++++++++++++++++++++- qfieldcloud_sdk/utils.py | 45 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/qfieldcloud_sdk/cli.py b/qfieldcloud_sdk/cli.py index 54905b2..da29763 100755 --- a/qfieldcloud_sdk/cli.py +++ b/qfieldcloud_sdk/cli.py @@ -6,7 +6,7 @@ import click from . import sdk -from .utils import log, print_json +from .utils import format_project_table, log, print_json QFIELDCLOUD_DEFAULT_URL = "https://app.qfield.cloud/api/v1/" @@ -371,6 +371,47 @@ def download_files( log(f"No files to download for project {project_id}") +@cli.command() +@click.argument("project_id") +@click.option( + "--name", + help="New project name", +) +@click.option( + "--description", + help="New project description", +) +@click.option( + "--owner", + help="Transfer the project to a new owner", +) +@click.option( + "--is-public/--is-no-public", + is_flag=True, + help="Whether the project shall be public", +) +@click.pass_context +def patch_project( + ctx: Context, + project_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + owner: Optional[str] = None, + is_public: Optional[bool] = None, +) -> None: + """Patch the project with new data. Pass only the parameters that shall be changed.""" + + project = ctx.obj["client"].patch_project( + project_id, name=name, owner=owner, description=description, is_public=is_public + ) + + if ctx.obj["format_json"]: + print_json(project) + else: + log("Patched project:") + log(format_project_table([project])) + + @cli.command() @click.argument("project_id") @click.argument("paths", nargs=-1, required=True) diff --git a/qfieldcloud_sdk/sdk.py b/qfieldcloud_sdk/sdk.py index e058b90..f25370b 100644 --- a/qfieldcloud_sdk/sdk.py +++ b/qfieldcloud_sdk/sdk.py @@ -416,6 +416,48 @@ def delete_project(self, project_id: str) -> requests.Response: return resp + def patch_project( + self, + project_id: str, + name: Optional[str] = None, + owner: Optional[str] = None, + description: Optional[str] = None, + is_public: Optional[bool] = None, + ) -> Dict[str, Any]: + """Update a project. + + Args: + project_id (str): Project ID. + name (str | None): if passed, the new name. Defaults to None. + owner (str | None, optional): if passed, the new owner. Defaults to None. + description (str, optional): if passed, the new description. Defaults to None. + is_public (bool, optional): if passed, the new public setting. Defaults to None. + + Returns: + Dict[str, Any]: the updated project + """ + project_data: dict[str, Any] = {} + + if name: + project_data["name"] = name + + if description: + project_data["description"] = description + + if owner: + project_data["owner"] = owner + + if is_public: + project_data["is_public"] = is_public + + resp = self._request( + "PATCH", + f"projects/{project_id}", + project_data, + ) + + return resp.json() + def upload_files( self, project_id: str, @@ -1068,7 +1110,7 @@ def download_file( progress_bar = tqdm( total=content_length, unit_scale=True, - desc=remote_filename, + desc=str(remote_filename), ) download_file = CallbackIOWrapper(progress_bar.update, f, "write") else: diff --git a/qfieldcloud_sdk/utils.py b/qfieldcloud_sdk/utils.py index a0e2ac7..9ebb801 100644 --- a/qfieldcloud_sdk/utils.py +++ b/qfieldcloud_sdk/utils.py @@ -2,6 +2,7 @@ import json import os import sys +from typing import List def print_json(data): @@ -69,3 +70,47 @@ def calc_etag(filename: str, part_size: int = 8 * 1024 * 1024) -> str: final_md5sum = hashlib.md5(b"".join(md5sums)) return "{}-{}".format(final_md5sum.hexdigest(), len(md5sums)) + + +def format_table(headers: List[str], data: List[List]) -> str: + length_by_column: List[int] = [] + + for col in headers: + length_by_column.append(len(col)) + + for row in data: + for idx, col in enumerate(row): + length_by_column[idx] = max(length_by_column[idx], len(str(col))) + + row_tmpl = "|" + for col_length in length_by_column: + row_tmpl += " {:<" + str(col_length) + "} |" + + result = row_tmpl.format(*headers) + result += "\r\n" + result += "-" * (sum(length_by_column) + len(headers) * 3 + 1) + + for row in data: + result += "\r\n" + result += row_tmpl.format(*row) + + return result + + +def format_project_table(projects: List) -> str: + data = [] + + for project in projects: + data.append( + [ + project["id"], + project["owner"] + "/" + project["name"], + project["is_public"], + project["description"], + ] + ) + + return format_table( + headers=["ID", "OWNER/NAME", "IS PUBLIC", "DESCRIPTION"], + data=data, + ) From 9b6bb22120d5ef1672221df4515aa174391bda81 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Sat, 21 Sep 2024 11:51:46 +0300 Subject: [PATCH 2/2] Migrate project related stuff to `format_project_table` --- qfieldcloud_sdk/cli.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qfieldcloud_sdk/cli.py b/qfieldcloud_sdk/cli.py index da29763..b37a708 100755 --- a/qfieldcloud_sdk/cli.py +++ b/qfieldcloud_sdk/cli.py @@ -200,9 +200,8 @@ def list_projects(ctx: Context, include_public: bool, **opts) -> None: print_json(projects) else: if projects: - log("Projects:") - for project in projects: - log(f'{project["id"]}\t{project["owner"]}/{project["name"]}') + log("Projects the current user has access to:") + log(format_project_table(projects)) else: log("User does not have any projects yet.") @@ -258,9 +257,8 @@ def create_project(ctx: Context, name, owner, description, is_public): if ctx.obj["format_json"]: print_json(project) else: - log( - f'Created project "{project["owner"]}/{project["name"]}" with project id "{project["id"]}".' - ) + log("Created project:") + log(format_project_table([project])) @cli.command()