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 d05ae6d Migrate to the current API interface
d05ae6d is described below
commit d05ae6d9bc67f19ec57a6ac64921781ffec719f7
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 29 15:47:31 2025 +0100
Migrate to the current API interface
---
pyproject.toml | 4 +-
src/atrclient/client.py | 8 ++--
src/atrclient/models/api.py | 95 +++++++++++++++++++++-------------------
src/atrclient/models/sql.py | 83 +++++++++++++++++++++++------------
src/atrclient/models/tabulate.py | 47 ++++++++++++++------
tests/cli_workflow.t | 4 +-
uv.lock | 4 +-
7 files changed, 151 insertions(+), 94 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 82eec0a..c41c93e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
[project]
name = "apache-trusted-releases"
-version = "0.20250729.1413"
+version = "0.20250729.1447"
description = "ATR CLI and Python API"
readme = "README.md"
requires-python = ">=3.13"
@@ -79,4 +79,4 @@ filterwarnings = [
]
[tool.uv]
-exclude-newer = "2025-07-29T14:13:00Z"
+exclude-newer = "2025-07-29T14:47:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index eb30934..aa3849e 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -457,8 +457,8 @@ def app_config_path() -> None:
@APP_DEV.command(name="delete", help="Delete a release.")
def app_dev_delete(project: str, version: str, /) -> None:
releases_delete_args = models.api.ReleaseDeleteArgs(project=project,
version=version)
- release_delete = api_release_delete(releases_delete_args)
- print(release_delete.deleted)
+ api_release_delete(releases_delete_args)
+ print(f"{project}-{version}")
@APP_DEV.command(name="env", help="Show the environment variables.")
@@ -946,8 +946,8 @@ def app_vote_resolve(
version=version,
resolution=resolution,
)
- vote_resolve = api_vote_resolve(vote_resolve_args)
- print(vote_resolve.success)
+ api_vote_resolve(vote_resolve_args)
+ print(f"Vote marked as {resolution}.")
@APP_VOTE.command(name="start", help="Start a vote.")
diff --git a/src/atrclient/models/api.py b/src/atrclient/models/api.py
index e50de9c..414a11d 100644
--- a/src/atrclient/models/api.py
+++ b/src/atrclient/models/api.py
@@ -92,7 +92,6 @@ class KeyAddArgs(schema.Strict):
class KeyAddResults(schema.Strict):
endpoint: Literal["/key/add"] = schema.Field(alias="endpoint")
- success: str = schema.Field(..., **example("Key added"))
fingerprint: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
@@ -102,7 +101,7 @@ class KeyDeleteArgs(schema.Strict):
class KeyDeleteResults(schema.Strict):
endpoint: Literal["/key/delete"] = schema.Field(alias="endpoint")
- success: str = schema.Field(..., **example("Key deleted"))
+ success: Literal[True] = schema.Field(..., **example(True))
class KeyGetResults(schema.Strict):
@@ -180,7 +179,7 @@ class ReleaseAnnounceArgs(schema.Strict):
class ReleaseAnnounceResults(schema.Strict):
endpoint: Literal["/release/announce"] = schema.Field(alias="endpoint")
- success: bool = schema.Field(..., **example(True))
+ success: Literal[True] = schema.Field(..., **example(True))
class ReleaseDraftDeleteArgs(schema.Strict):
@@ -190,12 +189,12 @@ class ReleaseDraftDeleteArgs(schema.Strict):
class ReleaseDraftDeleteResults(schema.Strict):
endpoint: Literal["/release/draft/delete"] = schema.Field(alias="endpoint")
- success: str = schema.Field(..., **example("Draft 'example-0.0.1'
deleted"))
+ success: Literal[True] = schema.Field(..., **example(True))
class ReleaseCreateArgs(schema.Strict):
- project: str
- version: str
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
class ReleaseCreateResults(schema.Strict):
@@ -204,13 +203,13 @@ class ReleaseCreateResults(schema.Strict):
class ReleaseDeleteArgs(schema.Strict):
- project: str
- version: str
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
class ReleaseDeleteResults(schema.Strict):
endpoint: Literal["/release/delete"] = schema.Field(alias="endpoint")
- deleted: str
+ deleted: Literal[True] = schema.Field(..., **example(True))
class ReleaseGetResults(schema.Strict):
@@ -233,7 +232,7 @@ class ReleaseGetResults(schema.Strict):
class ReleasePathsResults(schema.Strict):
endpoint: Literal["/release/paths"] = schema.Field(alias="endpoint")
- rel_paths: Sequence[str]
+ rel_paths: Sequence[str] = schema.Field(...,
**example(["example/0.0.1/example-0.0.1-bin.tar.gz"]))
class ReleaseRevisionsResults(schema.Strict):
@@ -242,10 +241,10 @@ class ReleaseRevisionsResults(schema.Strict):
class ReleaseUploadArgs(schema.Strict):
- project: str
- version: str
- relpath: str
- content: str
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
+ relpath: str = schema.Field(...,
**example("example/0.0.1/example-0.0.1-bin.tar.gz"))
+ content: str = schema.Field(..., **example("This is the content of the
file."))
class ReleaseUploadResults(schema.Strict):
@@ -267,42 +266,48 @@ class ReleasesListResults(schema.Strict):
class SignatureProvenanceArgs(schema.Strict):
- artifact_file_name: str
- artifact_sha3_256: str
- signature_file_name: str
- signature_asc_text: str
- signature_sha3_256: str
+ artifact_file_name: str = schema.Field(...,
**example("example-0.0.1-bin.tar.gz"))
+ artifact_sha3_256: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
+ signature_file_name: str = schema.Field(...,
**example("example-0.0.1-bin.tar.gz.asc"))
+ signature_asc_text: str = schema.Field(
+ ..., **example("-----BEGIN PGP SIGNATURE-----\n\n...\n-----END PGP
SIGNATURE-----\n")
+ )
+ signature_sha3_256: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
class SignatureProvenanceKey(schema.Strict):
- committee: str
- keys_file_url: str
- keys_file_sha3_256: str
+ committee: str = schema.Field(..., **example("example"))
+ keys_file_url: str = schema.Field(...,
**example("https://example.apache.org/example/KEYS"))
+ keys_file_sha3_256: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
class SignatureProvenanceResults(schema.Strict):
endpoint: Literal["/signature/provenance"] = schema.Field(alias="endpoint")
- fingerprint: str
- key_asc_text: str
+ fingerprint: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
+ key_asc_text: str = schema.Field(
+ ..., **example("-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n...\n-----END
PGP PUBLIC KEY BLOCK-----\n")
+ )
committees_with_artifact: list[SignatureProvenanceKey]
class SshKeyAddArgs(schema.Strict):
- text: str
+ text: str = schema.Field(
+ ..., **example("ssh-ed25519
AAAAC3NzaC1lZDI1NTEgH5C9okWi0dh25AAAAIOMqqnkVzrm0SdG6UOoqKLsabl9GKJl")
+ )
class SshKeyAddResults(schema.Strict):
endpoint: Literal["/ssh-key/add"] = schema.Field(alias="endpoint")
- fingerprint: str
+ fingerprint: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
class SshKeyDeleteArgs(schema.Strict):
- fingerprint: str
+ fingerprint: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
class SshKeyDeleteResults(schema.Strict):
endpoint: Literal["/ssh-key/delete"] = schema.Field(alias="endpoint")
- success: str
+ success: Literal[True] = schema.Field(..., **example(True))
@dataclasses.dataclass
@@ -314,7 +319,7 @@ class SshKeysListQuery:
class SshKeysListResults(schema.Strict):
endpoint: Literal["/ssh-keys/list"] = schema.Field(alias="endpoint")
data: Sequence[sql.SSHKey]
- count: int
+ count: int = schema.Field(..., **example(10))
@dataclasses.dataclass
@@ -327,33 +332,35 @@ class TasksListQuery:
class TasksListResults(schema.Strict):
endpoint: Literal["/tasks/list"] = schema.Field(alias="endpoint")
data: Sequence[sql.Task]
- count: int
+ count: int = schema.Field(..., **example(10))
class UsersListResults(schema.Strict):
endpoint: Literal["/users/list"] = schema.Field(alias="endpoint")
- users: Sequence[str]
+ users: Sequence[str] = schema.Field(..., **example(["user1", "user2"]))
class VoteResolveArgs(schema.Strict):
- project: str
- version: str
- resolution: Literal["passed", "failed"]
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
+ resolution: Literal["passed", "failed"] = schema.Field(...,
**example("passed"))
class VoteResolveResults(schema.Strict):
endpoint: Literal["/vote/resolve"] = schema.Field(alias="endpoint")
- success: str
+ success: Literal[True] = schema.Field(..., **example(True))
class VoteStartArgs(schema.Strict):
- project: str
- version: str
- revision: str
- email_to: str
- vote_duration: int
- subject: str
- body: str
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
+ revision: str = schema.Field(..., **example("00005"))
+ email_to: str = schema.Field(..., **example("[email protected]"))
+ vote_duration: int = schema.Field(..., **example(10))
+ subject: str = schema.Field(..., **example("[VOTE] Apache Example 0.0.1
release"))
+ body: str = schema.Field(
+ ..., **example("The Apache Example team is pleased to announce the
release of Example 0.0.1...")
+ )
class VoteStartResults(schema.Strict):
@@ -362,8 +369,8 @@ class VoteStartResults(schema.Strict):
class VoteTabulateArgs(schema.Strict):
- project: str
- version: str
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
class VoteTabulateResults(schema.Strict):
diff --git a/src/atrclient/models/sql.py b/src/atrclient/models/sql.py
index 9af0a0a..4a1ccc8 100644
--- a/src/atrclient/models/sql.py
+++ b/src/atrclient/models/sql.py
@@ -121,13 +121,21 @@ class UserRole(str, enum.Enum):
# Pydantic models
+def pydantic_example(value: Any) -> dict[Literal["json_schema_extra"],
dict[str, Any]]:
+ return {"json_schema_extra": {"example": value}}
+
+
class VoteEntry(schema.Strict):
- result: bool
- summary: str
- binding_votes: int
- community_votes: int
- start: datetime.datetime
- end: datetime.datetime
+ result: bool = schema.Field(alias="result", **pydantic_example(True))
+ summary: str = schema.Field(alias="summary", **pydantic_example("This is a
summary"))
+ binding_votes: int = schema.Field(alias="binding_votes",
**pydantic_example(10))
+ community_votes: int = schema.Field(alias="community_votes",
**pydantic_example(10))
+ start: datetime.datetime = schema.Field(
+ alias="start", **pydantic_example(datetime.datetime(2025, 5, 5, 1, 2,
3, tzinfo=datetime.UTC))
+ )
+ end: datetime.datetime = schema.Field(
+ alias="end", **pydantic_example(datetime.datetime(2025, 5, 7, 1, 2, 3,
tzinfo=datetime.UTC))
+ )
# Type decorators
@@ -551,24 +559,32 @@ class Release(sqlmodel.SQLModel, table=True):
# We guarantee that "{project.name}-{version}" is unique
# Therefore we can use that for the name
- name: str = sqlmodel.Field(default="", primary_key=True, unique=True)
- phase: ReleasePhase
- created: datetime.datetime =
sqlmodel.Field(sa_column=sqlalchemy.Column(UTCDateTime))
- released: datetime.datetime | None = sqlmodel.Field(default=None,
sa_column=sqlalchemy.Column(UTCDateTime))
+ name: str = sqlmodel.Field(default="", primary_key=True, unique=True,
**example("example-0.0.1"))
+ phase: ReleasePhase =
sqlmodel.Field(**example(ReleasePhase.RELEASE_CANDIDATE_DRAFT))
+ created: datetime.datetime = sqlmodel.Field(
+ sa_column=sqlalchemy.Column(UTCDateTime),
**example(datetime.datetime(2025, 5, 1, 1, 2, 3, tzinfo=datetime.UTC))
+ )
+ released: datetime.datetime | None = sqlmodel.Field(
+ default=None,
+ sa_column=sqlalchemy.Column(UTCDateTime),
+ **example(datetime.datetime(2025, 6, 1, 1, 2, 3, tzinfo=datetime.UTC)),
+ )
# M-1: Release -> Project
# 1-M: Project -> [Release]
- project_name: str = sqlmodel.Field(foreign_key="project.name")
+ project_name: str = sqlmodel.Field(foreign_key="project.name",
**example("example"))
project: Project = sqlmodel.Relationship(back_populates="releases")
see_also(Project.releases)
- package_managers: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
+ package_managers: list[str] = sqlmodel.Field(
+ default_factory=list, sa_column=sqlalchemy.Column(sqlalchemy.JSON),
**example([])
+ )
# TODO: Not all releases have a version
# We could either make this str | None, or we could require version to be
set on packages only
# For example, Apache Airflow Providers do not have an overall version
# They have one version per package, i.e. per provider
- version: str
- sboms: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
+ version: str = sqlmodel.Field(**example("0.0.1"))
+ sboms: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON), **example([]))
# 1-1: Release -C-> ReleasePolicy
# 1-1: ReleasePolicy -> Release
@@ -579,10 +595,18 @@ class Release(sqlmodel.SQLModel, table=True):
# VoteEntry is a Pydantic model, not a SQL model
votes: list[VoteEntry] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
- vote_manual: bool = sqlmodel.Field(default=False)
- vote_started: datetime.datetime | None = sqlmodel.Field(default=None,
sa_column=sqlalchemy.Column(UTCDateTime))
- vote_resolved: datetime.datetime | None = sqlmodel.Field(default=None,
sa_column=sqlalchemy.Column(UTCDateTime))
- podling_thread_id: str | None = sqlmodel.Field(default=None)
+ vote_manual: bool = sqlmodel.Field(default=False, **example(False))
+ vote_started: datetime.datetime | None = sqlmodel.Field(
+ default=None,
+ sa_column=sqlalchemy.Column(UTCDateTime),
+ **example(datetime.datetime(2025, 5, 5, 1, 2, 3, tzinfo=datetime.UTC)),
+ )
+ vote_resolved: datetime.datetime | None = sqlmodel.Field(
+ default=None,
+ sa_column=sqlalchemy.Column(UTCDateTime),
+ **example(datetime.datetime(2025, 5, 7, 1, 2, 3, tzinfo=datetime.UTC)),
+ )
+ podling_thread_id: str | None = sqlmodel.Field(default=None,
**example("hmk1lpwnnxn5zsbp8gwh7115h2qm7jrh"))
# 1-M: Release -C-> [Revision]
# M-1: Revision -> Release
@@ -625,6 +649,7 @@ class Release(sqlmodel.SQLModel, table=True):
raise ValueError("Release has no revisions")
return number
+ # TODO: How do we give an example for this?
@pydantic.computed_field # type: ignore[prop-decorator]
@property
def latest_revision_number(self) -> str | None:
@@ -777,11 +802,11 @@ class ReleasePolicy(sqlmodel.SQLModel, table=True):
# Revision: Release
class Revision(sqlmodel.SQLModel, table=True):
- name: str = sqlmodel.Field(default="", primary_key=True, unique=True)
+ name: str = sqlmodel.Field(default="", primary_key=True, unique=True,
**example("example-0.0.1 00002"))
# M-1: Revision -> Release
# 1-M: Release -C-> [Revision]
- release_name: str | None = sqlmodel.Field(default=None,
foreign_key="release.name")
+ release_name: str | None = sqlmodel.Field(default=None,
foreign_key="release.name", **example("example-0.0.1"))
release: Release = sqlmodel.Relationship(
back_populates="revisions",
sa_relationship_kwargs={
@@ -789,19 +814,23 @@ class Revision(sqlmodel.SQLModel, table=True):
},
)
- seq: int = sqlmodel.Field(default=0)
+ seq: int = sqlmodel.Field(default=0, **example(1))
# This was designed as a property, but it's better for it to be a column
# That way, we can do dynamic Release.latest_revision_number construction
easier
- number: str = sqlmodel.Field(default="")
- asfuid: str
+ number: str = sqlmodel.Field(default="", **example("00002"))
+ asfuid: str = sqlmodel.Field(**example("user"))
created: datetime.datetime = sqlmodel.Field(
- default_factory=lambda: datetime.datetime.now(datetime.UTC),
sa_column=sqlalchemy.Column(UTCDateTime)
+ default_factory=lambda: datetime.datetime.now(datetime.UTC),
+ sa_column=sqlalchemy.Column(UTCDateTime),
+ **example(datetime.datetime(2025, 5, 1, 1, 2, 3, tzinfo=datetime.UTC)),
)
- phase: ReleasePhase
+ phase: ReleasePhase =
sqlmodel.Field(**example(ReleasePhase.RELEASE_CANDIDATE_DRAFT))
# 1-1: Revision -> Revision
# 1-1: Revision -> Revision
- parent_name: str | None = sqlmodel.Field(default=None,
foreign_key="revision.name")
+ parent_name: str | None = sqlmodel.Field(
+ default=None, foreign_key="revision.name", **example("example-0.0.1
00001")
+ )
parent: Optional["Revision"] = sqlmodel.Relationship(
sa_relationship_kwargs=dict(
remote_side=lambda: Revision.name,
@@ -815,7 +844,7 @@ class Revision(sqlmodel.SQLModel, table=True):
# 1-1: Revision -> Revision
child: Optional["Revision"] =
sqlmodel.Relationship(back_populates="parent")
- description: str | None = sqlmodel.Field(default=None)
+ description: str | None = sqlmodel.Field(default=None, **example("This is
a description"))
def model_post_init(self, _context):
if isinstance(self.created, str):
diff --git a/src/atrclient/models/tabulate.py b/src/atrclient/models/tabulate.py
index e58b035..fdd0ff1 100644
--- a/src/atrclient/models/tabulate.py
+++ b/src/atrclient/models/tabulate.py
@@ -16,6 +16,7 @@
# under the License.
import enum
+from typing import Any, Literal
import pydantic
@@ -36,15 +37,19 @@ class VoteStatus(enum.Enum):
UNKNOWN = "Unknown"
+def example(value: Any) -> dict[Literal["json_schema_extra"], dict[str, Any]]:
+ return {"json_schema_extra": {"example": value}}
+
+
class VoteEmail(schema.Strict):
- asf_uid_or_email: str
- from_email: str
- status: VoteStatus
- asf_eid: str
- iso_datetime: str
- vote: Vote
- quotation: str
- updated: bool
+ asf_uid_or_email: str = schema.Field(..., **example("user"))
+ from_email: str = schema.Field(..., **example("[email protected]"))
+ status: VoteStatus = schema.Field(..., **example(VoteStatus.BINDING))
+ asf_eid: str = schema.Field(...,
**example("[email protected]"))
+ iso_datetime: str = schema.Field(..., **example("2025-05-01T12:00:00Z"))
+ vote: Vote = schema.Field(..., **example(Vote.YES))
+ quotation: str = schema.Field(..., **example("+1 (Binding)"))
+ updated: bool = schema.Field(..., **example(True))
@pydantic.field_validator("status", mode="before")
@classmethod
@@ -58,8 +63,24 @@ class VoteEmail(schema.Strict):
class VoteDetails(schema.Strict):
- start_unixtime: int | None
- votes: dict[str, VoteEmail]
- summary: dict[str, int]
- passed: bool
- outcome: str
+ start_unixtime: int | None = schema.Field(..., **example(1714435200))
+ votes: dict[str, VoteEmail] = schema.Field(
+ ...,
+ **example(
+ {
+ "user": VoteEmail(
+ asf_uid_or_email="user",
+ from_email="[email protected]",
+ status=VoteStatus.BINDING,
+ asf_eid="[email protected]",
+ iso_datetime="2025-05-01T12:00:00Z",
+ vote=Vote.YES,
+ quotation="+1 (Binding)",
+ updated=True,
+ )
+ }
+ ),
+ )
+ summary: dict[str, int] = schema.Field(..., **example({"user": 1}))
+ passed: bool = schema.Field(..., **example(True))
+ outcome: str = schema.Field(..., **example("The vote passed."))
diff --git a/tests/cli_workflow.t b/tests/cli_workflow.t
index 6e7705a..927426b 100644
--- a/tests/cli_workflow.t
+++ b/tests/cli_workflow.t
@@ -47,13 +47,13 @@ $ atr vote start tooling-test-example 0.3+cli 00002 -m
"<!user!>@apache.org"
<.skip.>"email_to":"<!user!>@apache.org"<.skip.>
$ atr vote resolve tooling-test-example 0.3+cli failed
-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.>
$ atr vote resolve tooling-test-example 0.3+cli passed
-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 20c713f..71123ac 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
requires-python = ">=3.13"
[options]
-exclude-newer = "2025-07-29T14:13:00Z"
+exclude-newer = "2025-07-29T14:47:00Z"
[[package]]
name = "aiohappyeyeballs"
@@ -83,7 +83,7 @@ wheels = [
[[package]]
name = "apache-trusted-releases"
-version = "0.20250729.1413"
+version = "0.20250729.1447"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]