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 <[email protected]>
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("[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/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("[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."))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]