This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-releases-client.git


The following commit(s) were added to refs/heads/main by this push:
     new f170e00  Use consistent API types for starting and resolving votes, 
and more
f170e00 is described below

commit f170e00c83e759d2354b00cbbea4f888484d167a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 15 18:56:02 2025 +0100

    Use consistent API types for starting and resolving votes, and more
---
 pyproject.toml              |  4 +-
 src/atrclient/client.py     | 53 ++++++++++++++------------
 src/atrclient/models/api.py | 92 ++++++++++++++++++++++++++++-----------------
 src/atrclient/models/sql.py | 23 ++++++++++++
 tests/cli_workflow.t        | 16 +++++---
 uv.lock                     |  4 +-
 6 files changed, 125 insertions(+), 67 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 58ab4fb..5bda5ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
 
 [project]
 name            = "apache-trusted-releases"
-version         = "0.20250715.1733"
+version         = "0.20250715.1754"
 description     = "ATR CLI and Python API"
 readme          = "README.md"
 requires-python = ">=3.13"
@@ -72,4 +72,4 @@ select = [
 ]
 
 [tool.uv]
-exclude-newer = "2025-07-15T17:33:00Z"
+exclude-newer = "2025-07-15T17:54:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index d543e8c..c0eb39d 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -260,7 +260,7 @@ def app_dev_delete(project: str, version: str, /) -> None:
     # Only ATR admins may do this
     jwt_value = config_jwt_usable()
     host, verify_ssl = config_host_get()
-    args = models.api.ProjectVersion(project=project, version=version)
+    args = models.api.ReleasesDeleteArgs(project=project, version=version)
     url = f"https://{host}/api/releases/delete";
     response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
     try:
@@ -347,7 +347,7 @@ def app_dev_user() -> None:
 def app_draft_delete(project: str, version: str, /) -> None:
     jwt_value = config_jwt_usable()
     host, verify_ssl = config_host_get()
-    args = models.api.ProjectVersion(project=project, version=version)
+    args = models.api.DraftDeleteArgs(project=project, version=version)
     url = f"https://{host}/api/draft/delete";
     response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
     try:
@@ -470,7 +470,7 @@ def app_release_start(project: str, version: str, /) -> 
None:
     jwt_value = config_jwt_usable()
     host, verify_ssl = config_host_get()
     url = f"https://{host}/api/releases/create";
-    args = models.api.ProjectVersion(project=project, version=version)
+    args = models.api.ReleasesCreateArgs(project=project, version=version)
     response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
     try:
         releases_create = models.api.validate_releases_create(response)
@@ -483,13 +483,12 @@ def app_release_start(project: str, version: str, /) -> 
None:
 def app_revisions(project: str, version: str, /) -> None:
     host, verify_ssl = config_host_get()
     url = f"https://{host}/api/revisions/{project}/{version}";
-    result = asyncio.run(web_get_public(url, verify_ssl))
-    if not is_json_dict(result):
-        show_error_and_exit(f"Unexpected API response: {result}")
-    result_revisions = result.get("revisions", [])
-    if not is_json_list_of_dict(result_revisions):
-        show_error_and_exit(f"Unexpected API response: {result_revisions}")
-    for revision in result_revisions:
+    response = asyncio.run(web_get_public(url, verify_ssl))
+    try:
+        revisions = models.api.validate_revisions(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
+    for revision in revisions.revisions:
         print(revision)
 
 
@@ -529,15 +528,19 @@ def app_upload(project: str, version: str, path: str, 
filepath: str, /) -> None:
     with open(filepath, "rb") as f:
         content = f.read()
 
-    args = models.api.ProjectVersionRelpathContent(
+    args = models.api.UploadArgs(
         project=project,
         version=version,
         relpath=path,
         content=base64.b64encode(content).decode("utf-8"),
     )
 
-    result = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
-    print_json(result)
+    response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
+    try:
+        upload = models.api.validate_upload(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
+    print(upload.revision.model_dump_json(indent=None))
 
 
 @APP_VOTE.command(name="resolve", help="Resolve a vote.")
@@ -549,13 +552,17 @@ def app_vote_resolve(
     jwt_value = config_jwt_usable()
     host, verify_ssl = config_host_get()
     url = f"https://{host}/api/vote/resolve";
-    args = models.api.ProjectVersionResolution(
+    args = models.api.VoteResolveArgs(
         project=project,
         version=version,
         resolution=resolution,
     )
-    result = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
-    print_json(result)
+    response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
+    try:
+        vote_resolve = models.api.validate_vote_resolve(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
+    print(vote_resolve.success)
 
 
 @APP_VOTE.command(name="start", help="Start a vote.")
@@ -576,7 +583,7 @@ def app_vote_start(
     if body:
         with open(body, encoding="utf-8") as f:
             body_text = f.read()
-    args = models.api.VoteStart(
+    args = models.api.VoteStartArgs(
         project=project,
         version=version,
         revision=revision,
@@ -585,8 +592,12 @@ def app_vote_start(
         subject=subject or f"[VOTE] Release {project} {version}",
         body=body_text or f"Release {project} {version} is ready for voting.",
     )
-    result = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
-    print_json(result)
+    response = asyncio.run(web_post(url, args, jwt_value, verify_ssl))
+    try:
+        vote_start = models.api.validate_vote_start(response)
+    except (pydantic.ValidationError, models.api.ResultsTypeError) as e:
+        show_error_and_exit(f"Unexpected API response: {response}\n{e}")
+    print(vote_start.task.model_dump_json(indent=None))
 
 
 def checks_display(results: Sequence[models.sql.CheckResult], verbose: bool = 
False) -> None:
@@ -933,10 +944,6 @@ def main() -> None:
     APP(sys.argv[1:])
 
 
-def print_json(data: JSON) -> None:
-    print(json.dumps(data, indent=None))
-
-
 def releases_display(releases: Sequence[models.sql.Release]) -> None:
     if not releases:
         print("No releases found for this project.")
diff --git a/src/atrclient/models/api.py b/src/atrclient/models/api.py
index 70c3036..02e63b6 100644
--- a/src/atrclient/models/api.py
+++ b/src/atrclient/models/api.py
@@ -30,18 +30,6 @@ class ResultsTypeError(TypeError):
     pass
 
 
[email protected]
-class Pagination:
-    offset: int = 0
-    limit: int = 20
-
-
-# TODO: TaskPagination?
[email protected]
-class Task(Pagination):
-    status: str | None = None
-
-
 class AnnounceArgs(schema.Strict):
     project: str
     version: str
@@ -139,7 +127,7 @@ class KeysSshAddResults(schema.Strict):
     fingerprint: str
 
 
-class KeysSshListQuery(Pagination):
+class KeysSshListQuery:
     offset: int = 0
     limit: int = 20
 
@@ -165,24 +153,6 @@ class ProjectsResults(schema.Strict):
     projects: Sequence[sql.Project]
 
 
-class ProjectVersion(schema.Strict):
-    project: str
-    version: str
-
-
-class ProjectVersionRelpathContent(schema.Strict):
-    project: str
-    version: str
-    relpath: str
-    content: str
-
-
-class ProjectVersionResolution(schema.Strict):
-    project: str
-    version: str
-    resolution: Literal["passed", "failed"]
-
-
 @dataclasses.dataclass
 class ReleasesQuery:
     offset: int = 0
@@ -240,11 +210,36 @@ class ReleasesRevisionsResults(schema.Strict):
     revisions: Sequence[sql.Revision]
 
 
-class Text(schema.Strict):
-    text: str
+class RevisionsResults(schema.Strict):
+    endpoint: Literal["/revisions"] = schema.Field(alias="endpoint")
+    revisions: Sequence[sql.Revision]
 
 
-class VoteStart(schema.Strict):
[email protected]
+class TasksQuery:
+    limit: int = 20
+    offset: int = 0
+    status: str | None = None
+
+
+class TasksResults(schema.Strict):
+    endpoint: Literal["/tasks"] = schema.Field(alias="endpoint")
+    data: Sequence[sql.Task]
+    count: int
+
+
+class VoteResolveArgs(schema.Strict):
+    project: str
+    version: str
+    resolution: Literal["passed", "failed"]
+
+
+class VoteResolveResults(schema.Strict):
+    endpoint: Literal["/vote/resolve"] = schema.Field(alias="endpoint")
+    success: str
+
+
+class VoteStartArgs(schema.Strict):
     project: str
     version: str
     revision: str
@@ -254,6 +249,23 @@ class VoteStart(schema.Strict):
     body: str
 
 
+class VoteStartResults(schema.Strict):
+    endpoint: Literal["/vote/start"] = schema.Field(alias="endpoint")
+    task: sql.Task
+
+
+class UploadArgs(schema.Strict):
+    project: str
+    version: str
+    relpath: str
+    content: str
+
+
+class UploadResults(schema.Strict):
+    endpoint: Literal["/upload"] = schema.Field(alias="endpoint")
+    revision: sql.Revision
+
+
 # This is for *Results classes only
 # We do NOT put *Args classes here
 Results = Annotated[
@@ -279,7 +291,12 @@ Results = Annotated[
     | ReleasesDeleteResults
     | ReleasesProjectResults
     | ReleasesVersionResults
-    | ReleasesRevisionsResults,
+    | ReleasesRevisionsResults
+    | RevisionsResults
+    | TasksResults
+    | VoteResolveResults
+    | VoteStartResults
+    | UploadResults,
     schema.Field(discriminator="endpoint"),
 ]
 
@@ -319,3 +336,8 @@ validate_releases_delete = validator(ReleasesDeleteResults)
 validate_releases_project = validator(ReleasesProjectResults)
 validate_releases_version = validator(ReleasesVersionResults)
 validate_releases_revisions = validator(ReleasesRevisionsResults)
+validate_revisions = validator(RevisionsResults)
+validate_tasks = validator(TasksResults)
+validate_vote_resolve = validator(VoteResolveResults)
+validate_vote_start = validator(VoteStartResults)
+validate_upload = validator(UploadResults)
diff --git a/src/atrclient/models/sql.py b/src/atrclient/models/sql.py
index 802e349..90b6584 100644
--- a/src/atrclient/models/sql.py
+++ b/src/atrclient/models/sql.py
@@ -244,6 +244,22 @@ class Task(sqlmodel.SQLModel, table=True):
     revision_number: str | None = sqlmodel.Field(default=None, index=True)
     primary_rel_path: str | None = sqlmodel.Field(default=None, index=True)
 
+    def model_post_init(self, _context):
+        if isinstance(self.task_type, str):
+            self.task_type = TaskType(self.task_type)
+
+        if isinstance(self.status, str):
+            self.status = TaskStatus(self.status)
+
+        if isinstance(self.added, str):
+            self.added = 
datetime.datetime.fromisoformat(self.added.rstrip("Z"))
+
+        if isinstance(self.started, str):
+            self.started = 
datetime.datetime.fromisoformat(self.started.rstrip("Z"))
+
+        if isinstance(self.completed, str):
+            self.completed = 
datetime.datetime.fromisoformat(self.completed.rstrip("Z"))
+
     # Create an index on status and added for efficient task claiming
     __table_args__ = (
         sqlalchemy.Index("ix_task_status_added", "status", "added"),
@@ -755,6 +771,13 @@ class Revision(sqlmodel.SQLModel, table=True):
 
     description: str | None = sqlmodel.Field(default=None)
 
+    def model_post_init(self, _context):
+        if isinstance(self.created, str):
+            self.created = 
datetime.datetime.fromisoformat(self.created.rstrip("Z"))
+
+        if isinstance(self.phase, str):
+            self.phase = ReleasePhase(self.phase)
+
     __table_args__ = (
         sqlmodel.UniqueConstraint("release_name", "seq", 
name="uq_revision_release_seq"),
         sqlmodel.UniqueConstraint("release_name", "number", 
name="uq_revision_release_number"),
diff --git a/tests/cli_workflow.t b/tests/cli_workflow.t
index 5e621a1..b244736 100644
--- a/tests/cli_workflow.t
+++ b/tests/cli_workflow.t
@@ -14,12 +14,18 @@ $ atr set tokens.pat <!pat!>
 Set tokens.pat to "<!pat!>".
 
 <# Delete any existing draft, ignoring errors. #>
-* atr draft delete tooling-test-example 0.3+cli
+* atr dev delete tooling-test-example 0.3+cli
 <.etc.>
 
 $ atr release start tooling-test-example 0.3+cli
 <.skip.>created<.skip.>
 
+$ atr dev delete tooling-test-example 0.3+cli
+tooling-test-example-0.3+cli
+
+$ atr release start tooling-test-example 0.3+cli
+<.skip.>created<.skip.>
+
 $ atr config path
 <?config_rel_path?>
 
@@ -34,16 +40,16 @@ Total checks: 1
   warning: 1
 
 $ atr vote start tooling-test-example 0.3+cli 00002 -m "<!user!>@apache.org"
-<.skip.>"email_to": "<!user!>@apache.org"<.skip.>
+<.skip.>"email_to":"<!user!>@apache.org"<.skip.>
 
 $ atr vote resolve tooling-test-example 0.3+cli failed
-{"success": "Vote marked as failed"}
+Vote marked as failed
 
 $ atr vote start tooling-test-example 0.3+cli 00002 -m "<!user!>@apache.org"
-<.skip.>"email_to": "<!user!>@apache.org"<.skip.>
+<.skip.>"email_to":"<!user!>@apache.org"<.skip.>
 
 $ atr vote resolve tooling-test-example 0.3+cli passed
-{"success": "Vote marked as passed"}
+Vote marked as passed
 
 $ atr announce tooling-test-example 0.3+cli 00003 -m "<!user!>@apache.org" -s 
"[ANNOUNCE] Release tooling-test-example 0.3+cli" -b "Release 
tooling-test-example 0.3+cli has been announced."
 Announcement sent
diff --git a/uv.lock b/uv.lock
index bc066f3..f578f60 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
 requires-python = ">=3.13"
 
 [options]
-exclude-newer = "2025-07-15T17:33:00Z"
+exclude-newer = "2025-07-15T17:54:00Z"
 
 [[package]]
 name = "aiohappyeyeballs"
@@ -83,7 +83,7 @@ wheels = [
 
 [[package]]
 name = "apache-trusted-releases"
-version = "0.20250715.1733"
+version = "0.20250715.1754"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to