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 5bed644 Ensure that API endpoint names match the cardinality of their
results
5bed644 is described below
commit 5bed644232965b4b862836c38af3ccebb79fad84
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 29 14:57:35 2025 +0100
Ensure that API endpoint names match the cardinality of their results
---
atr/blueprints/api/api.py | 582 +++++++++++++++++++++-------------------------
atr/models/api.py | 308 +++++++++++-------------
2 files changed, 406 insertions(+), 484 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 26401e4..86a7609 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -179,10 +179,9 @@ async def checks_ongoing(
).model_dump(), 200
-# TODO: Rename all paths to avoid clashes
[email protected]("/committees/get/<name>")
-@quart_schema.validate_response(models.api.CommitteesGetResults, 200)
-async def committees_get(name: str) -> DictResponse:
[email protected]("/committee/get/<name>")
+@quart_schema.validate_response(models.api.CommitteeGetResults, 200)
+async def committee_get(name: str) -> DictResponse:
"""
A specific committee by name.
@@ -194,15 +193,15 @@ async def committees_get(name: str) -> DictResponse:
_simple_check(name)
async with db.session() as data:
committee = await
data.committee(name=name).demand(exceptions.NotFound(f"Committee '{name}' was
not found"))
- return models.api.CommitteesGetResults(
- endpoint="/committees/get",
+ return models.api.CommitteeGetResults(
+ endpoint="/committee/get",
committee=committee,
).model_dump(), 200
[email protected]("/committees/keys/<name>")
-@quart_schema.validate_response(models.api.CommitteesKeysResults, 200)
-async def committees_keys(name: str) -> DictResponse:
[email protected]("/committee/keys/<name>")
+@quart_schema.validate_response(models.api.CommitteeKeysResults, 200)
+async def committee_keys(name: str) -> DictResponse:
"""
Public OpenPGP keys associated with a specific committee.
@@ -216,31 +215,15 @@ async def committees_keys(name: str) -> DictResponse:
committee = await data.committee(name=name,
_public_signing_keys=True).demand(
exceptions.NotFound(f"Committee '{name}' was not found")
)
- return models.api.CommitteesKeysResults(
- endpoint="/committees/keys",
+ return models.api.CommitteeKeysResults(
+ endpoint="/committee/keys",
keys=committee.public_signing_keys,
).model_dump(), 200
[email protected]("/committees/list")
-@quart_schema.validate_response(models.api.CommitteesListResults, 200)
-async def committees_list() -> DictResponse:
- """
- All committees.
-
- The list of committees is returned in no particular order.
- """
- async with db.session() as data:
- committees = await data.committee().all()
- return models.api.CommitteesListResults(
- endpoint="/committees/list",
- committees=committees,
- ).model_dump(), 200
-
-
[email protected]("/committees/projects/<name>")
-@quart_schema.validate_response(models.api.CommitteesProjectsResults, 200)
-async def committees_projects(name: str) -> DictResponse:
[email protected]("/committee/projects/<name>")
+@quart_schema.validate_response(models.api.CommitteeProjectsResults, 200)
+async def committee_projects(name: str) -> DictResponse:
"""
Projects managed by a specific committee.
@@ -254,49 +237,25 @@ async def committees_projects(name: str) -> DictResponse:
committee = await data.committee(name=name, _projects=True).demand(
exceptions.NotFound(f"Committee '{name}' was not found")
)
- return models.api.CommitteesProjectsResults(
- endpoint="/committees/projects",
+ return models.api.CommitteeProjectsResults(
+ endpoint="/committee/projects",
projects=committee.projects,
).model_dump(), 200
[email protected]("/draft/delete", methods=["POST"])
[email protected]
-@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.DraftDeleteArgs)
-@quart_schema.validate_response(models.api.DraftDeleteResults, 200)
-async def draft_delete(data: models.api.DraftDeleteArgs) -> DictResponse:
[email protected]("/committees/list")
+@quart_schema.validate_response(models.api.CommitteesListResults, 200)
+async def committees_list() -> DictResponse:
"""
- Delete a draft release.
-
- The draft release is deleted, and all of its associated metadata and files
- are removed from the database and the filesystem. This cannot be undone.
+ All committees.
- Warning: we plan to change how draft deletion works.
+ The list of committees is returned in no particular order.
"""
- asf_uid = _jwt_asf_uid()
-
- async with db.session() as db_data:
- release_name = sql.release_name(data.project, data.version)
- release = await db_data.release(
- name=release_name, phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
_committee=True
- ).demand(exceptions.NotFound(f"Draft release '{release_name}' not
found"))
- if release.project.committee is None:
- raise exceptions.NotFound("Project has no committee")
- _committee_member_or_admin(release.project.committee, asf_uid)
-
- # TODO: This causes "A transaction is already begun on this Session"
- # async with data.begin():
- # Probably due to autobegin in data.release above
- # We pass the phase again to guard against races
- # But the removal is not actually locked
- await interaction.release_delete(
- release_name, phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
include_downloads=False
- )
- await db_data.commit()
- return models.api.DraftDeleteResults(
- endpoint="/draft/delete",
- success=f"Draft {release_name} deleted",
+ async with db.session() as data:
+ committees = await data.committee().all()
+ return models.api.CommitteesListResults(
+ endpoint="/committees/list",
+ committees=committees,
).model_dump(), 200
@@ -324,12 +283,12 @@ async def jwt_create(data: models.api.JwtCreateArgs) ->
DictResponse:
).model_dump(), 200
[email protected]("/keys/add", methods=["POST"])
[email protected]("/key/add", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.KeysAddArgs)
-@quart_schema.validate_response(models.api.KeysAddResults, 200)
-async def keys_add(data: models.api.KeysAddArgs) -> DictResponse:
+@quart_schema.validate_request(models.api.KeyAddArgs)
+@quart_schema.validate_response(models.api.KeyAddResults, 200)
+async def key_add(data: models.api.KeyAddArgs) -> DictResponse:
"""
Add a public OpenPGP key to all specified committees.
@@ -351,19 +310,19 @@ async def keys_add(data: models.api.KeysAddArgs) ->
DictResponse:
)
outcome.result_or_raise()
- return models.api.KeysAddResults(
- endpoint="/keys/add",
+ return models.api.KeyAddResults(
+ endpoint="/key/add",
success="Key added",
fingerprint=key.key_model.fingerprint.upper(),
).model_dump(), 200
[email protected]("/keys/delete", methods=["POST"])
[email protected]("/key/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.KeysDeleteArgs)
-@quart_schema.validate_response(models.api.KeysDeleteResults, 200)
-async def keys_delete(data: models.api.KeysDeleteArgs) -> DictResponse:
+@quart_schema.validate_request(models.api.KeyDeleteArgs)
+@quart_schema.validate_response(models.api.KeyDeleteResults, 200)
+async def key_delete(data: models.api.KeyDeleteArgs) -> DictResponse:
"""
Delete a public OpenPGP key from all committees.
@@ -385,15 +344,15 @@ async def keys_delete(data: models.api.KeysDeleteArgs) ->
DictResponse:
outcomes.append(await wacm.keys.autogenerate_keys_file())
# TODO: Add error outcomes as warnings to the response
- return models.api.KeysDeleteResults(
- endpoint="/keys/delete",
+ return models.api.KeyDeleteResults(
+ endpoint="/key/delete",
success="Key deleted",
).model_dump(), 200
[email protected]("/keys/get/<fingerprint>")
-@quart_schema.validate_response(models.api.KeysGetResults, 200)
-async def keys_get(fingerprint: str) -> DictResponse:
[email protected]("/key/get/<fingerprint>")
+@quart_schema.validate_response(models.api.KeyGetResults, 200)
+async def key_get(fingerprint: str) -> DictResponse:
"""
A single public OpenPGP key by fingerprint.
@@ -404,8 +363,8 @@ async def keys_get(fingerprint: str) -> DictResponse:
key = await
data.public_signing_key(fingerprint=fingerprint.lower()).demand(
exceptions.NotFound(f"Key '{fingerprint}' not found")
)
- return models.api.KeysGetResults(
- endpoint="/keys/get",
+ return models.api.KeyGetResults(
+ endpoint="/key/get",
key=key,
).model_dump(), 200
@@ -483,18 +442,31 @@ async def keys_user(asf_uid: str) -> DictResponse:
).model_dump(), 200
[email protected]("/projects/get/<name>")
-@quart_schema.validate_response(models.api.ProjectsGetResults, 200)
-async def projects_get(name: str) -> DictResponse:
[email protected]("/project/get/<name>")
+@quart_schema.validate_response(models.api.ProjectGetResults, 200)
+async def project_get(name: str) -> DictResponse:
_simple_check(name)
async with db.session() as data:
project = await data.project(name=name).demand(exceptions.NotFound())
- return models.api.ProjectsGetResults(
- endpoint="/projects/get",
+ return models.api.ProjectGetResults(
+ endpoint="/project/get",
project=project,
).model_dump(), 200
[email protected]("/project/releases/<name>")
+@quart_schema.validate_response(models.api.ProjectReleasesResults, 200)
+async def project_releases(name: str) -> DictResponse:
+ """List all releases for a specific project."""
+ _simple_check(name)
+ async with db.session() as data:
+ releases = await data.release(project_name=name).all()
+ return models.api.ProjectReleasesResults(
+ endpoint="/project/releases",
+ releases=releases,
+ ).model_dump(), 200
+
+
@api.BLUEPRINT.route("/projects/list")
@quart_schema.validate_response(models.api.ProjectsListResults, 200)
async def projects_list() -> DictResponse:
@@ -508,19 +480,6 @@ async def projects_list() -> DictResponse:
).model_dump(), 200
[email protected]("/projects/releases/<name>")
-@quart_schema.validate_response(models.api.ProjectsReleasesResults, 200)
-async def projects_releases(name: str) -> DictResponse:
- """List all releases for a specific project."""
- _simple_check(name)
- async with db.session() as data:
- releases = await data.release(project_name=name).all()
- return models.api.ProjectsReleasesResults(
- endpoint="/projects/releases",
- releases=releases,
- ).model_dump(), 200
-
-
@api.BLUEPRINT.route("/release/announce", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@@ -558,50 +517,12 @@ async def release_announce(data:
models.api.ReleaseAnnounceArgs) -> DictResponse
).model_dump(), 201
[email protected]("/releases")
-@quart_schema.validate_querystring(models.api.ReleasesQuery)
-@quart_schema.validate_response(models.api.ReleasesResults, 200)
-async def releases(query_args: models.api.ReleasesQuery) -> DictResponse:
- """Paged list of releases with optional filtering by phase."""
- _pagination_args_validate(query_args)
- via = sql.validate_instrumented_attribute
- async with db.session() as data:
- statement = sqlmodel.select(sql.Release)
-
- if query_args.phase:
- try:
- phase_value = sql.ReleasePhase(query_args.phase)
- except ValueError:
- raise exceptions.BadRequest(f"Invalid phase:
{query_args.phase}")
- statement = statement.where(sql.Release.phase == phase_value)
-
- statement = (
-
statement.order_by(via(sql.Release.created).desc()).limit(query_args.limit).offset(query_args.offset)
- )
-
- paged_releases = (await data.execute(statement)).scalars().all()
-
- count_stmt =
sqlalchemy.select(sqlalchemy.func.count(via(sql.Release.name)))
- if query_args.phase:
- phase_value = sql.ReleasePhase(query_args.phase) if
query_args.phase else None
- if phase_value is not None:
- count_stmt = count_stmt.where(via(sql.Release.phase) ==
phase_value)
-
- count = (await data.execute(count_stmt)).scalar_one()
-
- return models.api.ReleasesResults(
- endpoint="/releases",
- data=paged_releases,
- count=count,
- ).model_dump(), 200
-
-
[email protected]("/releases/create", methods=["POST"])
[email protected]("/release/create", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.ReleasesCreateArgs)
-@quart_schema.validate_response(models.api.ReleasesCreateResults, 201)
-async def releases_create(data: models.api.ReleasesCreateArgs) -> DictResponse:
+@quart_schema.validate_request(models.api.ReleaseCreateArgs)
+@quart_schema.validate_response(models.api.ReleaseCreateResults, 201)
+async def release_create(data: models.api.ReleaseCreateArgs) -> DictResponse:
"""Create a new release draft for a project via POSTed JSON."""
asf_uid = _jwt_asf_uid()
@@ -614,18 +535,19 @@ async def releases_create(data:
models.api.ReleasesCreateArgs) -> DictResponse:
except routes.FlashError as exc:
raise exceptions.BadRequest(str(exc))
- return models.api.ReleasesCreateResults(
- endpoint="/releases/create",
+ return models.api.ReleaseCreateResults(
+ endpoint="/release/create",
release=release,
).model_dump(), 201
[email protected]("/releases/delete", methods=["POST"])
+# TODO: Duplicates the below
[email protected]("/release/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.ReleasesDeleteArgs)
-@quart_schema.validate_response(models.api.ReleasesDeleteResults, 200)
-async def releases_delete(data: models.api.ReleasesDeleteArgs) -> DictResponse:
+@quart_schema.validate_request(models.api.ReleaseDeleteArgs)
+@quart_schema.validate_response(models.api.ReleaseDeleteResults, 200)
+async def release_delete(data: models.api.ReleaseDeleteArgs) -> DictResponse:
"""Delete a release draft for a project via POSTed JSON."""
asf_uid = _jwt_asf_uid()
if not user.is_admin(asf_uid):
@@ -635,15 +557,70 @@ async def releases_delete(data:
models.api.ReleasesDeleteArgs) -> DictResponse:
release_name = sql.release_name(data.project, data.version)
await interaction.release_delete(release_name, include_downloads=True)
await db_data.commit()
- return models.api.ReleasesDeleteResults(
- endpoint="/releases/delete",
+ return models.api.ReleaseDeleteResults(
+ endpoint="/release/delete",
deleted=release_name,
).model_dump(), 200
[email protected]("/releases/paths/<project>/<version>")
[email protected]("/releases/paths/<project>/<version>/<revision>")
-@quart_schema.validate_response(models.api.ReleasesPathsResults, 200)
+# TODO: Duplicates the above
[email protected]("/release/draft/delete", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.ReleaseDraftDeleteArgs)
+@quart_schema.validate_response(models.api.ReleaseDraftDeleteResults, 200)
+async def release_draft_delete(data: models.api.ReleaseDraftDeleteArgs) ->
DictResponse:
+ """
+ Delete a draft release.
+
+ The draft release is deleted, and all of its associated metadata and files
+ are removed from the database and the filesystem. This cannot be undone.
+
+ Warning: we plan to change how draft deletion works.
+ """
+ asf_uid = _jwt_asf_uid()
+
+ async with db.session() as db_data:
+ release_name = sql.release_name(data.project, data.version)
+ release = await db_data.release(
+ name=release_name, phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
_committee=True
+ ).demand(exceptions.NotFound(f"Draft release '{release_name}' not
found"))
+ if release.project.committee is None:
+ raise exceptions.NotFound("Project has no committee")
+ _committee_member_or_admin(release.project.committee, asf_uid)
+
+ # TODO: This causes "A transaction is already begun on this Session"
+ # async with data.begin():
+ # Probably due to autobegin in data.release above
+ # We pass the phase again to guard against races
+ # But the removal is not actually locked
+ await interaction.release_delete(
+ release_name, phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
include_downloads=False
+ )
+ await db_data.commit()
+ return models.api.ReleaseDraftDeleteResults(
+ endpoint="/release/draft/delete",
+ success=f"Draft {release_name} deleted",
+ ).model_dump(), 200
+
+
[email protected]("/release/get/<project>/<version>")
+@quart_schema.validate_response(models.api.ReleaseGetResults, 200)
+async def release_get(project: str, version: str) -> DictResponse:
+ """Return a single release by project and version."""
+ _simple_check(project, version)
+ async with db.session() as data:
+ release_name = sql.release_name(project, version)
+ release = await
data.release(name=release_name).demand(exceptions.NotFound())
+ return models.api.ReleaseGetResults(
+ endpoint="/release/get",
+ release=release,
+ ).model_dump(), 200
+
+
[email protected]("/release/paths/<project>/<version>")
[email protected]("/release/paths/<project>/<version>/<revision>")
+@quart_schema.validate_response(models.api.ReleasePathsResults, 200)
async def release_paths(project: str, version: str, revision: str | None =
None) -> DictResponse:
_simple_check(project, version, revision)
async with db.session() as data:
@@ -658,125 +635,181 @@ async def release_paths(project: str, version: str,
revision: str | None = None)
raise exceptions.NotFound("Files not found")
files: list[str] = [str(path) for path in [p async for p in
util.paths_recursive(dir_path)]]
files.sort()
- return models.api.ReleasesPathsResults(
- endpoint="/releases/paths",
+ return models.api.ReleasePathsResults(
+ endpoint="/release/paths",
rel_paths=files,
).model_dump(), 200
[email protected]("/releases/project/<project>")
-@quart_schema.validate_querystring(models.api.ReleasesProjectQuery)
-async def releases_project(project: str, query_args:
models.api.ReleasesProjectQuery) -> DictResponse:
- """List all releases for a specific project with pagination."""
- _simple_check(project)
[email protected]("/release/revisions/<project>/<version>")
+@quart_schema.validate_response(models.api.ReleaseRevisionsResults, 200)
+async def release_revisions(project: str, version: str) -> DictResponse:
+ """List all revisions for a given release."""
+ _simple_check(project, version)
+ async with db.session() as data:
+ release_name = sql.release_name(project, version)
+ revisions = await data.revision(release_name=release_name).all()
+ if not isinstance(revisions, list):
+ revisions = list(revisions)
+ revisions.sort(key=lambda rev: rev.number)
+ return models.api.ReleaseRevisionsResults(
+ endpoint="/release/revisions",
+ revisions=revisions,
+ ).model_dump(), 200
+
+
[email protected]("/release/upload", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.ReleaseUploadArgs)
+@quart_schema.validate_response(models.api.ReleaseUploadResults, 201)
+async def release_upload(data: models.api.ReleaseUploadArgs) -> DictResponse:
+ asf_uid = _jwt_asf_uid()
+
+ async with db.session() as db_data:
+ project = await db_data.project(name=data.project,
_committee=True).demand(exceptions.NotFound())
+ # TODO: user.is_participant(project, asf_uid)
+ if not (user.is_committee_member(project.committee, asf_uid) or
user.is_admin(asf_uid)):
+ raise exceptions.Forbidden("You do not have permission to upload
to this project")
+
+ revision = await _upload_process_file(data, asf_uid)
+ return models.api.ReleaseUploadResults(
+ endpoint="/release/upload",
+ revision=revision,
+ ).model_dump(), 201
+
+
[email protected]("/releases/list")
+@quart_schema.validate_querystring(models.api.ReleasesListQuery)
+@quart_schema.validate_response(models.api.ReleasesListResults, 200)
+async def releases_list(query_args: models.api.ReleasesListQuery) ->
DictResponse:
+ """Paged list of releases with optional filtering by phase."""
_pagination_args_validate(query_args)
+ via = sql.validate_instrumented_attribute
async with db.session() as data:
- project_result = await data.project(name=project).get()
- if project_result is None:
- raise exceptions.NotFound(f"Project '{project}' does not exist")
+ statement = sqlmodel.select(sql.Release)
+
+ if query_args.phase:
+ try:
+ phase_value = sql.ReleasePhase(query_args.phase)
+ except ValueError:
+ raise exceptions.BadRequest(f"Invalid phase:
{query_args.phase}")
+ statement = statement.where(sql.Release.phase == phase_value)
- via = sql.validate_instrumented_attribute
statement = (
- sqlmodel.select(sql.Release)
- .where(sql.Release.project_name == project)
- .order_by(via(sql.Release.created).desc())
- .limit(query_args.limit)
- .offset(query_args.offset)
+
statement.order_by(via(sql.Release.created).desc()).limit(query_args.limit).offset(query_args.offset)
)
paged_releases = (await data.execute(statement)).scalars().all()
- count_stmt =
sqlalchemy.select(sqlalchemy.func.count(via(sql.Release.name))).where(
- via(sql.Release.project_name) == project
- )
+ count_stmt =
sqlalchemy.select(sqlalchemy.func.count(via(sql.Release.name)))
+ if query_args.phase:
+ phase_value = sql.ReleasePhase(query_args.phase) if
query_args.phase else None
+ if phase_value is not None:
+ count_stmt = count_stmt.where(via(sql.Release.phase) ==
phase_value)
+
count = (await data.execute(count_stmt)).scalar_one()
- return models.api.ReleasesProjectResults(
- endpoint="/releases/project",
+ return models.api.ReleasesListResults(
+ endpoint="/releases/list",
data=paged_releases,
count=count,
).model_dump(), 200
[email protected]("/releases/version/<project>/<version>")
-@quart_schema.validate_response(models.api.ReleasesVersionResults, 200)
-async def releases_version(project: str, version: str) -> DictResponse:
- """Return a single release by project and version."""
- _simple_check(project, version)
- async with db.session() as data:
- release_name = sql.release_name(project, version)
- release = await
data.release(name=release_name).demand(exceptions.NotFound())
- return models.api.ReleasesVersionResults(
- endpoint="/releases/version",
- release=release,
- ).model_dump(), 200
[email protected]("/signature/provenance", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.SignatureProvenanceArgs)
+@quart_schema.validate_response(models.api.SignatureProvenanceResults, 200)
+async def signature_provenance(data: models.api.SignatureProvenanceArgs) ->
DictResponse:
+ # POST because this uses significant computation and I/O
+ # We receive a file name and an SHA3-256 hash
+ # From these we find which committee(s) published the file with a signature
+ # Then we deliver the appropriate signing key from the KEYS file(s)
+ # And the URL of the KEYS file(s) for them to check
+ signing_keys: list[models.api.SignatureProvenanceKey] = []
+ conf = config.get()
+ host = conf.APP_HOST
-# TODO: Rename this to revisions? I.e. /revisions/<project>/<version>
[email protected]("/releases/revisions/<project>/<version>")
-@quart_schema.validate_response(models.api.ReleasesRevisionsResults, 200)
-async def releases_revisions(project: str, version: str) -> DictResponse:
- """List all revisions for a given release."""
- _simple_check(project, version)
- async with db.session() as data:
- release_name = sql.release_name(project, version)
- revisions = await data.revision(release_name=release_name).all()
- return models.api.ReleasesRevisionsResults(
- endpoint="/releases/revisions",
- revisions=revisions,
- ).model_dump(), 200
+ signature_asc_data = data.signature_asc_text
+ sig = pgpy.PGPSignature.from_blob(signature_asc_data)
+ if not hasattr(sig, "signer_fingerprint"):
+ raise exceptions.NotFound("No signer fingerprint found")
[email protected]("/revisions/<project>/<version>")
-@quart_schema.validate_response(models.api.RevisionsResults, 200)
-async def revisions(project: str, version: str) -> DictResponse:
- _simple_check(project, version)
- async with db.session() as data:
- release_name = sql.release_name(project, version)
- await data.release(name=release_name).demand(exceptions.NotFound())
- revisions = await data.revision(release_name=release_name).all()
- if not isinstance(revisions, list):
- revisions = list(revisions)
- revisions.sort(key=lambda rev: rev.number)
- return models.api.RevisionsResults(
- endpoint="/revisions",
- revisions=revisions,
+ signer_fingerprint = getattr(sig, "signer_fingerprint").lower()
+ async with db.session() as db_data:
+ key = await db_data.public_signing_key(
+ fingerprint=signer_fingerprint,
+ _committees=True,
+ ).demand(
+ exceptions.NotFound(
+ f"Key with fingerprint {signer_fingerprint} not found",
+ )
+ )
+
+ downloads_dir = util.get_downloads_dir()
+ matched_committee_names = await _match_committee_names(key.committees,
util.get_finished_dir(), data)
+
+ for matched_committee_name in matched_committee_names:
+ keys_file_path = downloads_dir / matched_committee_name / "KEYS"
+ async with aiofiles.open(keys_file_path, "rb") as f:
+ keys_file_data = await f.read()
+ keys_file_sha3_256 = hashlib.sha3_256(keys_file_data).hexdigest()
+ signing_keys.append(
+ models.api.SignatureProvenanceKey(
+ committee=matched_committee_name,
+
keys_file_url=f"https://{host}/downloads/{matched_committee_name}/KEYS",
+ keys_file_sha3_256=keys_file_sha3_256,
+ )
+ )
+
+ if not signing_keys:
+ raise exceptions.NotFound("No signing keys found")
+
+ return models.api.SignatureProvenanceResults(
+ endpoint="/signature/provenance",
+ fingerprint=signer_fingerprint,
+ key_asc_text=key.ascii_armored_key,
+ committees_with_artifact=signing_keys,
).model_dump(), 200
[email protected]("/ssh/add", methods=["POST"])
[email protected]("/ssh-key/add", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.SshAddArgs)
-@quart_schema.validate_response(models.api.SshAddResults, 201)
-async def ssh_add(data: models.api.SshAddArgs) -> DictResponse:
+@quart_schema.validate_request(models.api.SshKeyAddArgs)
+@quart_schema.validate_response(models.api.SshKeyAddResults, 201)
+async def ssh_key_add(data: models.api.SshKeyAddArgs) -> DictResponse:
"""Add an SSH key for a user."""
asf_uid = _jwt_asf_uid()
fingerprint = await keys.ssh_key_add(data.text, asf_uid)
- return models.api.SshAddResults(
- endpoint="/ssh/add",
+ return models.api.SshKeyAddResults(
+ endpoint="/ssh-key/add",
fingerprint=fingerprint,
).model_dump(), 201
[email protected]("/ssh/delete", methods=["POST"])
[email protected]("/ssh-key/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.SshDeleteArgs)
-@quart_schema.validate_response(models.api.SshDeleteResults, 201)
-async def ssh_delete(data: models.api.SshDeleteArgs) -> DictResponse:
+@quart_schema.validate_request(models.api.SshKeyDeleteArgs)
+@quart_schema.validate_response(models.api.SshKeyDeleteResults, 201)
+async def ssh_key_delete(data: models.api.SshKeyDeleteArgs) -> DictResponse:
"""Delete an SSH key for a user."""
asf_uid = _jwt_asf_uid()
await keys.ssh_key_delete(data.fingerprint, asf_uid)
- return models.api.SshDeleteResults(
- endpoint="/ssh/delete",
+ return models.api.SshKeyDeleteResults(
+ endpoint="/ssh-key/delete",
success="SSH key deleted",
).model_dump(), 201
[email protected]("/ssh/list/<asf_uid>")
-@quart_schema.validate_querystring(models.api.SshListQuery)
-async def ssh_list(asf_uid: str, query_args: models.api.SshListQuery) ->
DictResponse:
[email protected]("/ssh-keys/list/<asf_uid>")
+@quart_schema.validate_querystring(models.api.SshKeysListQuery)
+async def ssh_keys_list(asf_uid: str, query_args: models.api.SshKeysListQuery)
-> DictResponse:
"""List of developer SSH public keys."""
_simple_check(asf_uid)
_pagination_args_validate(query_args)
@@ -794,8 +827,8 @@ async def ssh_list(asf_uid: str, query_args:
models.api.SshListQuery) -> DictRes
count_stmt =
sqlalchemy.select(sqlalchemy.func.count(via(sql.SSHKey.fingerprint)))
count = (await data.execute(count_stmt)).scalar_one()
- return models.api.SshListResults(
- endpoint="/ssh/list",
+ return models.api.SshKeysListResults(
+ endpoint="/ssh-keys/list",
data=paged_keys,
count=count,
).model_dump(), 200
@@ -859,66 +892,6 @@ async def users_list() -> DictResponse:
).model_dump(), 200
[email protected]("/verify/provenance", methods=["POST"])
[email protected]
-@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.VerifyProvenanceArgs)
-@quart_schema.validate_response(models.api.VerifyProvenanceResults, 200)
-async def verify_provenance(data: models.api.VerifyProvenanceArgs) ->
DictResponse:
- # POST because this uses significant computation and I/O
- # We receive a file name and an SHA3-256 hash
- # From these we find which committee(s) published the file with a signature
- # Then we deliver the appropriate signing key from the KEYS file(s)
- # And the URL of the KEYS file(s) for them to check
-
- signing_keys: list[models.api.VerifyProvenanceKey] = []
- conf = config.get()
- host = conf.APP_HOST
-
- signature_asc_data = data.signature_asc_text
- sig = pgpy.PGPSignature.from_blob(signature_asc_data)
-
- if not hasattr(sig, "signer_fingerprint"):
- raise exceptions.NotFound("No signer fingerprint found")
-
- signer_fingerprint = getattr(sig, "signer_fingerprint").lower()
- async with db.session() as db_data:
- key = await db_data.public_signing_key(
- fingerprint=signer_fingerprint,
- _committees=True,
- ).demand(
- exceptions.NotFound(
- f"Key with fingerprint {signer_fingerprint} not found",
- )
- )
-
- downloads_dir = util.get_downloads_dir()
- matched_committee_names = await _match_committee_names(key.committees,
util.get_finished_dir(), data)
-
- for matched_committee_name in matched_committee_names:
- keys_file_path = downloads_dir / matched_committee_name / "KEYS"
- async with aiofiles.open(keys_file_path, "rb") as f:
- keys_file_data = await f.read()
- keys_file_sha3_256 = hashlib.sha3_256(keys_file_data).hexdigest()
- signing_keys.append(
- models.api.VerifyProvenanceKey(
- committee=matched_committee_name,
-
keys_file_url=f"https://{host}/downloads/{matched_committee_name}/KEYS",
- keys_file_sha3_256=keys_file_sha3_256,
- )
- )
-
- if not signing_keys:
- raise exceptions.NotFound("No signing keys found")
-
- return models.api.VerifyProvenanceResults(
- endpoint="/verify/provenance",
- fingerprint=signer_fingerprint,
- key_asc_text=key.ascii_armored_key,
- committees_with_artifact=signing_keys,
- ).model_dump(), 200
-
-
@api.BLUEPRINT.route("/vote/resolve", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@@ -1035,27 +1008,6 @@ async def vote_tabulate(data:
models.api.VoteTabulateArgs) -> DictResponse:
).model_dump(), 200
[email protected]("/upload", methods=["POST"])
[email protected]
-@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.UploadArgs)
-@quart_schema.validate_response(models.api.UploadResults, 201)
-async def upload(data: models.api.UploadArgs) -> DictResponse:
- asf_uid = _jwt_asf_uid()
-
- async with db.session() as db_data:
- project = await db_data.project(name=data.project,
_committee=True).demand(exceptions.NotFound())
- # TODO: user.is_participant(project, asf_uid)
- if not (user.is_committee_member(project.committee, asf_uid) or
user.is_admin(asf_uid)):
- raise exceptions.Forbidden("You do not have permission to upload
to this project")
-
- revision = await _upload_process_file(data, asf_uid)
- return models.api.UploadResults(
- endpoint="/upload",
- revision=revision,
- ).model_dump(), 201
-
-
def _committee_member_or_admin(committee: sql.Committee, asf_uid: str) -> None:
if not (user.is_committee_member(committee, asf_uid) or
user.is_admin(asf_uid)):
raise exceptions.Forbidden("You do not have permission to perform this
action")
@@ -1080,7 +1032,7 @@ def _jwt_asf_uid() -> str:
async def _match_committee_names(
- key_committees: list[sql.Committee], finished_dir: pathlib.Path, data:
models.api.VerifyProvenanceArgs
+ key_committees: list[sql.Committee], finished_dir: pathlib.Path, data:
models.api.SignatureProvenanceArgs
) -> set[str]:
key_committee_names = set(committee.name for committee in key_committees)
finished_dir = util.get_finished_dir()
@@ -1115,7 +1067,7 @@ async def _match_committee_names(
return matched_committee_names
-async def _match_unfinished(release_directory: pathlib.Path, data:
models.api.VerifyProvenanceArgs) -> bool:
+async def _match_unfinished(release_directory: pathlib.Path, data:
models.api.SignatureProvenanceArgs) -> bool:
async for rel_path in util.paths_recursive(release_directory):
if rel_path.name == data.signature_file_name:
abs_path = release_directory / rel_path
@@ -1141,7 +1093,7 @@ def _simple_check(*args: str | None) -> None:
raise exceptions.BadRequest("Argument cannot be the string 'None'")
-async def _upload_process_file(args: models.api.UploadArgs, asf_uid: str) ->
sql.Revision:
+async def _upload_process_file(args: models.api.ReleaseUploadArgs, asf_uid:
str) -> sql.Revision:
file_bytes = base64.b64decode(args.content, validate=True)
file_path = args.relpath.lstrip("/")
description = f"Upload via API: {file_path}"
diff --git a/atr/models/api.py b/atr/models/api.py
index f6d0906..8a0b0a9 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -51,34 +51,24 @@ class ChecksOngoingResults(schema.Strict):
ongoing: int = schema.Field(..., **example(10))
-class CommitteesGetResults(schema.Strict):
- endpoint: Literal["/committees/get"] = schema.Field(alias="endpoint")
+class CommitteeGetResults(schema.Strict):
+ endpoint: Literal["/committee/get"] = schema.Field(alias="endpoint")
committee: sql.Committee
-class CommitteesKeysResults(schema.Strict):
- endpoint: Literal["/committees/keys"] = schema.Field(alias="endpoint")
+class CommitteeKeysResults(schema.Strict):
+ endpoint: Literal["/committee/keys"] = schema.Field(alias="endpoint")
keys: Sequence[sql.PublicSigningKey]
-class CommitteesListResults(schema.Strict):
- endpoint: Literal["/committees/list"] = schema.Field(alias="endpoint")
- committees: Sequence[sql.Committee]
-
-
-class CommitteesProjectsResults(schema.Strict):
- endpoint: Literal["/committees/projects"] = schema.Field(alias="endpoint")
+class CommitteeProjectsResults(schema.Strict):
+ endpoint: Literal["/committee/projects"] = schema.Field(alias="endpoint")
projects: Sequence[sql.Project]
-class DraftDeleteArgs(schema.Strict):
- project: str = schema.Field(..., **example("example"))
- version: str = schema.Field(..., **example("0.0.1"))
-
-
-class DraftDeleteResults(schema.Strict):
- endpoint: Literal["/draft/delete"] = schema.Field(alias="endpoint")
- success: str = schema.Field(..., **example("Draft 'example-0.0.1'
deleted"))
+class CommitteesListResults(schema.Strict):
+ endpoint: Literal["/committees/list"] = schema.Field(alias="endpoint")
+ committees: Sequence[sql.Committee]
class JwtCreateArgs(schema.Strict):
@@ -92,7 +82,7 @@ class JwtCreateResults(schema.Strict):
jwt: str = schema.Field(...,
**example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI="))
-class KeysAddArgs(schema.Strict):
+class KeyAddArgs(schema.Strict):
asfuid: str = schema.Field(..., **example("user"))
key: str = schema.Field(
..., **example("-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n...\n-----END
PGP PUBLIC KEY BLOCK-----\n")
@@ -100,23 +90,23 @@ class KeysAddArgs(schema.Strict):
committees: list[str] = schema.Field(..., **example(["example"]))
-class KeysAddResults(schema.Strict):
- endpoint: Literal["/keys/add"] = schema.Field(alias="endpoint")
+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"))
-class KeysDeleteArgs(schema.Strict):
+class KeyDeleteArgs(schema.Strict):
fingerprint: str = schema.Field(...,
**example("0123456789abcdef0123456789abcdef01234567"))
-class KeysDeleteResults(schema.Strict):
- endpoint: Literal["/keys/delete"] = schema.Field(alias="endpoint")
+class KeyDeleteResults(schema.Strict):
+ endpoint: Literal["/key/delete"] = schema.Field(alias="endpoint")
success: str = schema.Field(..., **example("Key deleted"))
-class KeysGetResults(schema.Strict):
- endpoint: Literal["/keys/get"] = schema.Field(alias="endpoint")
+class KeyGetResults(schema.Strict):
+ endpoint: Literal["/key/get"] = schema.Field(alias="endpoint")
key: sql.PublicSigningKey
@@ -147,13 +137,6 @@ KeysUploadOutcome = Annotated[
KeysUploadOutcomeAdapter = pydantic.TypeAdapter(KeysUploadOutcome)
-# def validate_keys_upload_outcome(value: Any) -> KeysUploadOutcome:
-# obj = KeysUploadOutcomeAdapter.validate_python(value)
-# if not isinstance(obj, KeysUploadOutcome):
-# raise ResultsTypeError(f"Invalid API response: {value}")
-# return obj
-
-
class KeysUploadResults(schema.Strict):
endpoint: Literal["/keys/upload"] = schema.Field(alias="endpoint")
results: Sequence[KeysUploadResult | KeysUploadException]
@@ -167,21 +150,21 @@ class KeysUserResults(schema.Strict):
keys: Sequence[sql.PublicSigningKey]
-class ProjectsGetResults(schema.Strict):
- endpoint: Literal["/projects/get"] = schema.Field(alias="endpoint")
+class ProjectGetResults(schema.Strict):
+ endpoint: Literal["/project/get"] = schema.Field(alias="endpoint")
project: sql.Project
+class ProjectReleasesResults(schema.Strict):
+ endpoint: Literal["/project/releases"] = schema.Field(alias="endpoint")
+ releases: Sequence[sql.Release]
+
+
class ProjectsListResults(schema.Strict):
endpoint: Literal["/projects/list"] = schema.Field(alias="endpoint")
projects: Sequence[sql.Project]
-class ProjectsReleasesResults(schema.Strict):
- endpoint: Literal["/projects/releases"] = schema.Field(alias="endpoint")
- releases: Sequence[sql.Release]
-
-
class ReleaseAnnounceArgs(schema.Strict):
project: str = schema.Field(..., **example("example"))
version: str = schema.Field(..., **example("1.0.0"))
@@ -200,60 +183,38 @@ class ReleaseAnnounceResults(schema.Strict):
success: bool = schema.Field(..., **example(True))
[email protected]
-class ReleasesQuery:
- offset: int = 0
- limit: int = 20
- phase: str | None = None
+class ReleaseDraftDeleteArgs(schema.Strict):
+ project: str = schema.Field(..., **example("example"))
+ version: str = schema.Field(..., **example("0.0.1"))
-class ReleasesResults(schema.Strict):
- endpoint: Literal["/releases"] = schema.Field(alias="endpoint")
- data: Sequence[sql.Release]
- count: int
+class ReleaseDraftDeleteResults(schema.Strict):
+ endpoint: Literal["/release/draft/delete"] = schema.Field(alias="endpoint")
+ success: str = schema.Field(..., **example("Draft 'example-0.0.1'
deleted"))
-class ReleasesCreateArgs(schema.Strict):
+class ReleaseCreateArgs(schema.Strict):
project: str
version: str
-class ReleasesCreateResults(schema.Strict):
- endpoint: Literal["/releases/create"] = schema.Field(alias="endpoint")
+class ReleaseCreateResults(schema.Strict):
+ endpoint: Literal["/release/create"] = schema.Field(alias="endpoint")
release: sql.Release
-class ReleasesDeleteArgs(schema.Strict):
+class ReleaseDeleteArgs(schema.Strict):
project: str
version: str
-class ReleasesDeleteResults(schema.Strict):
- endpoint: Literal["/releases/delete"] = schema.Field(alias="endpoint")
+class ReleaseDeleteResults(schema.Strict):
+ endpoint: Literal["/release/delete"] = schema.Field(alias="endpoint")
deleted: str
-class ReleasesPathsResults(schema.Strict):
- endpoint: Literal["/releases/paths"] = schema.Field(alias="endpoint")
- rel_paths: Sequence[str]
-
-
[email protected]
-class ReleasesProjectQuery:
- limit: int = 20
- offset: int = 0
- # project: str
- # version: str
-
-
-class ReleasesProjectResults(schema.Strict):
- endpoint: Literal["/releases/project"] = schema.Field(alias="endpoint")
- data: Sequence[sql.Release]
- count: int
-
-
-class ReleasesVersionResults(schema.Strict):
- endpoint: Literal["/releases/version"] = schema.Field(alias="endpoint")
+class ReleaseGetResults(schema.Strict):
+ endpoint: Literal["/release/get"] = schema.Field(alias="endpoint")
release: sql.Release
@pydantic.field_validator("release", mode="before")
@@ -270,42 +231,88 @@ class ReleasesVersionResults(schema.Strict):
return v
-class ReleasesRevisionsResults(schema.Strict):
- endpoint: Literal["/releases/revisions"] = schema.Field(alias="endpoint")
- revisions: Sequence[sql.Revision]
+class ReleasePathsResults(schema.Strict):
+ endpoint: Literal["/release/paths"] = schema.Field(alias="endpoint")
+ rel_paths: Sequence[str]
-class RevisionsResults(schema.Strict):
- endpoint: Literal["/revisions"] = schema.Field(alias="endpoint")
+class ReleaseRevisionsResults(schema.Strict):
+ endpoint: Literal["/release/revisions"] = schema.Field(alias="endpoint")
revisions: Sequence[sql.Revision]
-class SshAddArgs(schema.Strict):
+class ReleaseUploadArgs(schema.Strict):
+ project: str
+ version: str
+ relpath: str
+ content: str
+
+
+class ReleaseUploadResults(schema.Strict):
+ endpoint: Literal["/release/upload"] = schema.Field(alias="endpoint")
+ revision: sql.Revision
+
+
[email protected]
+class ReleasesListQuery:
+ offset: int = 0
+ limit: int = 20
+ phase: str | None = None
+
+
+class ReleasesListResults(schema.Strict):
+ endpoint: Literal["/releases/list"] = schema.Field(alias="endpoint")
+ data: Sequence[sql.Release]
+ count: int
+
+
+class SignatureProvenanceArgs(schema.Strict):
+ artifact_file_name: str
+ artifact_sha3_256: str
+ signature_file_name: str
+ signature_asc_text: str
+ signature_sha3_256: str
+
+
+class SignatureProvenanceKey(schema.Strict):
+ committee: str
+ keys_file_url: str
+ keys_file_sha3_256: str
+
+
+class SignatureProvenanceResults(schema.Strict):
+ endpoint: Literal["/signature/provenance"] = schema.Field(alias="endpoint")
+ fingerprint: str
+ key_asc_text: str
+ committees_with_artifact: list[SignatureProvenanceKey]
+
+
+class SshKeyAddArgs(schema.Strict):
text: str
-class SshAddResults(schema.Strict):
- endpoint: Literal["/ssh/add"] = schema.Field(alias="endpoint")
+class SshKeyAddResults(schema.Strict):
+ endpoint: Literal["/ssh-key/add"] = schema.Field(alias="endpoint")
fingerprint: str
-class SshDeleteArgs(schema.Strict):
+class SshKeyDeleteArgs(schema.Strict):
fingerprint: str
-class SshDeleteResults(schema.Strict):
- endpoint: Literal["/ssh/delete"] = schema.Field(alias="endpoint")
+class SshKeyDeleteResults(schema.Strict):
+ endpoint: Literal["/ssh-key/delete"] = schema.Field(alias="endpoint")
success: str
@dataclasses.dataclass
-class SshListQuery:
+class SshKeysListQuery:
offset: int = 0
limit: int = 20
-class SshListResults(schema.Strict):
- endpoint: Literal["/ssh/list"] = schema.Field(alias="endpoint")
+class SshKeysListResults(schema.Strict):
+ endpoint: Literal["/ssh-keys/list"] = schema.Field(alias="endpoint")
data: Sequence[sql.SSHKey]
count: int
@@ -364,77 +371,42 @@ class VoteTabulateResults(schema.Strict):
details: tabulate.VoteDetails
-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
-
-
-class VerifyProvenanceArgs(schema.Strict):
- artifact_file_name: str
- artifact_sha3_256: str
- signature_file_name: str
- signature_asc_text: str
- signature_sha3_256: str
-
-
-class VerifyProvenanceKey(schema.Strict):
- committee: str
- keys_file_url: str
- keys_file_sha3_256: str
-
-
-class VerifyProvenanceResults(schema.Strict):
- endpoint: Literal["/verify/provenance"] = schema.Field(alias="endpoint")
- fingerprint: str
- key_asc_text: str
- committees_with_artifact: list[VerifyProvenanceKey]
-
-
# This is for *Results classes only
# We do NOT put *Args classes here
Results = Annotated[
ChecksListResults
| ChecksOngoingResults
- | CommitteesGetResults
- | CommitteesKeysResults
+ | CommitteeGetResults
+ | CommitteeKeysResults
+ | CommitteeProjectsResults
| CommitteesListResults
- | CommitteesProjectsResults
- | DraftDeleteResults
| JwtCreateResults
- | KeysAddResults
- | KeysDeleteResults
- | KeysGetResults
+ | KeyAddResults
+ | KeyDeleteResults
+ | KeyGetResults
| KeysUploadResults
| KeysUserResults
- | ProjectsGetResults
+ | ProjectGetResults
+ | ProjectReleasesResults
| ProjectsListResults
- | ProjectsReleasesResults
| ReleaseAnnounceResults
- | ReleasesResults
- | ReleasesCreateResults
- | ReleasesDeleteResults
- | ReleasesPathsResults
- | ReleasesProjectResults
- | ReleasesVersionResults
- | ReleasesRevisionsResults
- | RevisionsResults
- | SshAddResults
- | SshDeleteResults
- | SshListResults
+ | ReleaseCreateResults
+ | ReleaseDeleteResults
+ | ReleaseDraftDeleteResults
+ | ReleaseGetResults
+ | ReleasePathsResults
+ | ReleaseRevisionsResults
+ | ReleaseUploadResults
+ | ReleasesListResults
+ | SignatureProvenanceResults
+ | SshKeyAddResults
+ | SshKeyDeleteResults
+ | SshKeysListResults
| TasksResults
| UsersListResults
- | VerifyProvenanceResults
| VoteResolveResults
| VoteStartResults
- | VoteTabulateResults
- | UploadResults,
+ | VoteTabulateResults,
schema.Field(discriminator="endpoint"),
]
@@ -453,36 +425,34 @@ def validator[T](t: type[T]) -> Callable[[Any], T]:
validate_checks_list = validator(ChecksListResults)
validate_checks_ongoing = validator(ChecksOngoingResults)
-validate_committees_get = validator(CommitteesGetResults)
-validate_committees_keys = validator(CommitteesKeysResults)
+validate_committee_get = validator(CommitteeGetResults)
+validate_committee_keys = validator(CommitteeKeysResults)
+validate_committee_projects = validator(CommitteeProjectsResults)
validate_committees_list = validator(CommitteesListResults)
-validate_committees_projects = validator(CommitteesProjectsResults)
-validate_draft_delete = validator(DraftDeleteResults)
validate_jwt_create = validator(JwtCreateResults)
-validate_keys_add = validator(KeysAddResults)
-validate_keys_delete = validator(KeysDeleteResults)
-validate_keys_get = validator(KeysGetResults)
+validate_key_add = validator(KeyAddResults)
+validate_key_delete = validator(KeyDeleteResults)
+validate_key_get = validator(KeyGetResults)
validate_keys_upload = validator(KeysUploadResults)
validate_keys_user = validator(KeysUserResults)
-validate_projects_get = validator(ProjectsGetResults)
+validate_project_get = validator(ProjectGetResults)
+validate_project_releases = validator(ProjectReleasesResults)
validate_projects_list = validator(ProjectsListResults)
-validate_projects_releases = validator(ProjectsReleasesResults)
validate_release_announce = validator(ReleaseAnnounceResults)
-validate_releases = validator(ReleasesResults)
-validate_releases_create = validator(ReleasesCreateResults)
-validate_releases_delete = validator(ReleasesDeleteResults)
-validate_releases_paths = validator(ReleasesPathsResults)
-validate_releases_project = validator(ReleasesProjectResults)
-validate_releases_version = validator(ReleasesVersionResults)
-validate_releases_revisions = validator(ReleasesRevisionsResults)
-validate_revisions = validator(RevisionsResults)
-validate_ssh_add = validator(SshAddResults)
-validate_ssh_delete = validator(SshDeleteResults)
-validate_ssh_list = validator(SshListResults)
+validate_release_create = validator(ReleaseCreateResults)
+validate_release_delete = validator(ReleaseDeleteResults)
+validate_release_draft_delete = validator(ReleaseDraftDeleteResults)
+validate_release_get = validator(ReleaseGetResults)
+validate_release_paths = validator(ReleasePathsResults)
+validate_release_revisions = validator(ReleaseRevisionsResults)
+validate_release_upload = validator(ReleaseUploadResults)
+validate_releases_list = validator(ReleasesListResults)
+validate_signature_provenance = validator(SignatureProvenanceResults)
+validate_ssh_key_add = validator(SshKeyAddResults)
+validate_ssh_key_delete = validator(SshKeyDeleteResults)
+validate_ssh_keys_list = validator(SshKeysListResults)
validate_tasks = validator(TasksResults)
validate_users_list = validator(UsersListResults)
-validate_verify_provenance = validator(VerifyProvenanceResults)
validate_vote_resolve = validator(VoteResolveResults)
validate_vote_start = validator(VoteStartResults)
validate_vote_tabulate = validator(VoteTabulateResults)
-validate_upload = validator(UploadResults)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]