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-trusted-release.git
The following commit(s) were added to refs/heads/main by this push: new 386fe98 Add more examples to model documentation 386fe98 is described below commit 386fe98b99fe1486acf41bd966b59ce1be3f3108 Author: Sean B. Palmer <s...@miscoranda.com> AuthorDate: Tue Jul 29 15:46:36 2025 +0100 Add more examples to model documentation --- atr/blueprints/api/api.py | 13 +++---- atr/models/api.py | 95 +++++++++++++++++++++++++---------------------- atr/models/sql.py | 83 +++++++++++++++++++++++++++-------------- atr/models/tabulate.py | 47 ++++++++++++++++------- 4 files changed, 146 insertions(+), 92 deletions(-) diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py index 0cbec73..80313a7 100644 --- a/atr/blueprints/api/api.py +++ b/atr/blueprints/api/api.py @@ -314,7 +314,6 @@ async def key_add(data: models.api.KeyAddArgs) -> DictResponse: return models.api.KeyAddResults( endpoint="/key/add", - success="Key added", fingerprint=key.key_model.fingerprint.upper(), ).model_dump(), 200 @@ -348,7 +347,7 @@ async def key_delete(data: models.api.KeyDeleteArgs) -> DictResponse: return models.api.KeyDeleteResults( endpoint="/key/delete", - success="Key deleted", + success=True, ).model_dump(), 200 @@ -574,7 +573,7 @@ async def release_delete(data: models.api.ReleaseDeleteArgs) -> DictResponse: await db_data.commit() return models.api.ReleaseDeleteResults( endpoint="/release/delete", - deleted=release_name, + deleted=True, ).model_dump(), 200 @@ -615,7 +614,7 @@ async def release_draft_delete(data: models.api.ReleaseDraftDeleteArgs) -> DictR await db_data.commit() return models.api.ReleaseDraftDeleteResults( endpoint="/release/draft/delete", - success=f"Draft {release_name} deleted", + success=True, ).model_dump(), 200 @@ -843,7 +842,7 @@ async def ssh_key_delete(data: models.api.SshKeyDeleteArgs) -> DictResponse: await keys.ssh_key_delete(data.fingerprint, asf_uid) return models.api.SshKeyDeleteResults( endpoint="/ssh-key/delete", - success="SSH key deleted", + success=True, ).model_dump(), 201 @@ -964,7 +963,6 @@ async def vote_resolve(data: models.api.VoteResolveArgs) -> DictResponse: match data.resolution: case "passed": release.phase = sql.ReleasePhase.RELEASE_PREVIEW - success_message = "Vote marked as passed" description = "Create a preview revision from the last candidate draft" async with revision.create_and_manage( data.project, release.version, asf_uid, description=description @@ -972,11 +970,10 @@ async def vote_resolve(data: models.api.VoteResolveArgs) -> DictResponse: pass case "failed": release.phase = sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT - success_message = "Vote marked as failed" await db_data.commit() return models.api.VoteResolveResults( endpoint="/vote/resolve", - success=success_message, + success=True, ).model_dump(), 200 diff --git a/atr/models/api.py b/atr/models/api.py index e50de9c..414a11d 100644 --- a/atr/models/api.py +++ b/atr/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("d...@example.apache.org")) + 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/atr/models/sql.py b/atr/models/sql.py index 9af0a0a..4a1ccc8 100644 --- a/atr/models/sql.py +++ b/atr/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/atr/models/tabulate.py b/atr/models/tabulate.py index e58b035..fdd0ff1 100644 --- a/atr/models/tabulate.py +++ b/atr/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("u...@example.org")) + status: VoteStatus = schema.Field(..., **example(VoteStatus.BINDING)) + asf_eid: str = schema.Field(..., **example("102ed8a-503db792-79bc789-b8ca8...@apache.org")) + 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="u...@example.org", + status=VoteStatus.BINDING, + asf_eid="102ed8a-503db792-79bc789-b8ca8...@apache.org", + 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.")) --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@tooling.apache.org For additional commands, e-mail: commits-h...@tooling.apache.org