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]

Reply via email to