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-releases.git
The following commit(s) were added to refs/heads/main by this push:
new 89ed0ca Move the API routes into the new blueprints layout
89ed0ca is described below
commit 89ed0cac09b8c0722dfdb313a48778c62bc6a596
Author: Sean B. Palmer <[email protected]>
AuthorDate: Sun Oct 26 17:09:01 2025 +0000
Move the API routes into the new blueprints layout
---
atr/{bps/api/api.py => api/__init__.py} | 94 +++++++++++++-------------
atr/{bps => blueprints}/__init__.py | 26 +++++--
atr/{bps/api/__init__.py => blueprints/api.py} | 26 ++++---
atr/bps/__init__.py | 2 -
atr/server.py | 2 +
5 files changed, 87 insertions(+), 63 deletions(-)
diff --git a/atr/bps/api/api.py b/atr/api/__init__.py
similarity index 95%
rename from atr/bps/api/api.py
rename to atr/api/__init__.py
index 1d78cf8..ac23fe7 100644
--- a/atr/bps/api/api.py
+++ b/atr/api/__init__.py
@@ -18,7 +18,7 @@
import hashlib
import pathlib
-from typing import Any
+from typing import Any, Final, Literal
import aiofiles.os
import asfquart.base as base
@@ -29,7 +29,7 @@ import sqlalchemy
import sqlmodel
import werkzeug.exceptions as exceptions
-import atr.bps.api as api
+import atr.blueprints.api as api
import atr.config as config
import atr.db as db
import atr.db.interaction as interaction
@@ -53,8 +53,10 @@ import atr.util as util
type DictResponse = tuple[dict[str, Any], int]
+ROUTES_MODULE: Final[Literal[True]] = True
[email protected]("/checks/list/<project>/<version>")
+
[email protected]("/checks/list/<project>/<version>")
@quart_schema.validate_response(models.api.ChecksListResults, 200)
async def checks_list(project: str, version: str) -> DictResponse:
"""
@@ -94,7 +96,7 @@ async def checks_list(project: str, version: str) ->
DictResponse:
).model_dump(), 200
[email protected]("/checks/list/<project>/<version>/<revision>")
[email protected]("/checks/list/<project>/<version>/<revision>")
@quart_schema.validate_response(models.api.ChecksListResults, 200)
async def checks_list_revision(project: str, version: str, revision: str) ->
DictResponse:
"""
@@ -132,8 +134,8 @@ async def checks_list_revision(project: str, version: str,
revision: str) -> Dic
).model_dump(), 200
[email protected]("/checks/ongoing/<project>/<version>",
defaults={"revision": None})
[email protected]("/checks/ongoing/<project>/<version>/<revision>")
[email protected]("/checks/ongoing/<project>/<version>", defaults={"revision": None})
[email protected]("/checks/ongoing/<project>/<version>/<revision>")
@quart_schema.validate_response(models.api.ChecksOngoingResults, 200)
async def checks_ongoing(
project: str,
@@ -169,7 +171,7 @@ async def checks_ongoing(
).model_dump(), 200
[email protected]("/committee/get/<name>")
[email protected]("/committee/get/<name>")
@quart_schema.validate_response(models.api.CommitteeGetResults, 200)
async def committee_get(name: str) -> DictResponse:
"""
@@ -189,7 +191,7 @@ async def committee_get(name: str) -> DictResponse:
).model_dump(), 200
[email protected]("/committee/keys/<name>")
[email protected]("/committee/keys/<name>")
@quart_schema.validate_response(models.api.CommitteeKeysResults, 200)
async def committee_keys(name: str) -> DictResponse:
"""
@@ -211,7 +213,7 @@ async def committee_keys(name: str) -> DictResponse:
).model_dump(), 200
[email protected]("/committee/projects/<name>")
[email protected]("/committee/projects/<name>")
@quart_schema.validate_response(models.api.CommitteeProjectsResults, 200)
async def committee_projects(name: str) -> DictResponse:
"""
@@ -233,7 +235,7 @@ async def committee_projects(name: str) -> DictResponse:
).model_dump(), 200
[email protected]("/committees/list")
[email protected]("/committees/list")
@quart_schema.validate_response(models.api.CommitteesListResults, 200)
async def committees_list() -> DictResponse:
"""
@@ -249,7 +251,7 @@ async def committees_list() -> DictResponse:
).model_dump(), 200
[email protected]("/distribution/record", methods=["POST"])
[email protected]("/distribution/record", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.DistributionRecordArgs)
@@ -288,7 +290,7 @@ async def distribution_record(data:
models.api.DistributionRecordArgs) -> DictRe
).model_dump(), 200
[email protected]("/ignore/add", methods=["POST"])
[email protected]("/ignore/add", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.IgnoreAddArgs)
@@ -317,7 +319,7 @@ async def ignore_add(data: models.api.IgnoreAddArgs) ->
DictResponse:
).model_dump(), 200
[email protected]("/ignore/delete", methods=["POST"])
[email protected]("/ignore/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.IgnoreDeleteArgs)
@@ -341,7 +343,7 @@ async def ignore_delete(data: models.api.IgnoreDeleteArgs)
-> DictResponse:
# TODO: Rename to ignores
[email protected]("/ignore/list/<committee_name>")
[email protected]("/ignore/list/<committee_name>")
@quart_schema.validate_response(models.api.IgnoreListResults, 200)
async def ignore_list(committee_name: str) -> DictResponse:
"""
@@ -356,7 +358,7 @@ async def ignore_list(committee_name: str) -> DictResponse:
).model_dump(), 200
[email protected]("/jwt/create", methods=["POST"])
[email protected]("/jwt/create", methods=["POST"])
@quart_schema.validate_request(models.api.JwtCreateArgs)
async def jwt_create(data: models.api.JwtCreateArgs) -> DictResponse:
"""
@@ -378,7 +380,7 @@ async def jwt_create(data: models.api.JwtCreateArgs) ->
DictResponse:
).model_dump(), 200
[email protected]("/key/add", methods=["POST"])
[email protected]("/key/add", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.KeyAddArgs)
@@ -411,7 +413,7 @@ async def key_add(data: models.api.KeyAddArgs) ->
DictResponse:
).model_dump(), 200
[email protected]("/key/delete", methods=["POST"])
[email protected]("/key/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.KeyDeleteArgs)
@@ -444,7 +446,7 @@ async def key_delete(data: models.api.KeyDeleteArgs) ->
DictResponse:
).model_dump(), 200
[email protected]("/key/get/<fingerprint>")
[email protected]("/key/get/<fingerprint>")
@quart_schema.validate_response(models.api.KeyGetResults, 200)
async def key_get(fingerprint: str) -> DictResponse:
"""
@@ -463,7 +465,7 @@ async def key_get(fingerprint: str) -> DictResponse:
).model_dump(), 200
[email protected]("/keys/upload", methods=["POST"])
[email protected]("/keys/upload", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.KeysUploadArgs)
@@ -520,7 +522,7 @@ async def keys_upload(data: models.api.KeysUploadArgs) ->
DictResponse:
).model_dump(), 200
[email protected]("/keys/user/<asf_uid>")
[email protected]("/keys/user/<asf_uid>")
@quart_schema.validate_response(models.api.KeysUserResults, 200)
async def keys_user(asf_uid: str) -> DictResponse:
"""
@@ -535,7 +537,7 @@ async def keys_user(asf_uid: str) -> DictResponse:
).model_dump(), 200
[email protected]("/project/get/<name>")
[email protected]("/project/get/<name>")
@quart_schema.validate_response(models.api.ProjectGetResults, 200)
async def project_get(name: str) -> DictResponse:
"""
@@ -550,7 +552,7 @@ async def project_get(name: str) -> DictResponse:
).model_dump(), 200
[email protected]("/project/releases/<name>")
[email protected]("/project/releases/<name>")
@quart_schema.validate_response(models.api.ProjectReleasesResults, 200)
async def project_releases(name: str) -> DictResponse:
"""
@@ -565,7 +567,7 @@ async def project_releases(name: str) -> DictResponse:
).model_dump(), 200
[email protected]("/projects/list")
[email protected]("/projects/list")
@quart_schema.validate_response(models.api.ProjectsListResults, 200)
async def projects_list() -> DictResponse:
"""
@@ -580,7 +582,7 @@ async def projects_list() -> DictResponse:
).model_dump(), 200
[email protected]("/publisher/distribution/record", methods=["POST"])
[email protected]("/publisher/distribution/record", methods=["POST"])
@quart_schema.validate_request(models.api.PublisherDistributionRecordArgs)
async def publisher_distribution_record(data:
models.api.PublisherDistributionRecordArgs) -> DictResponse:
"""
@@ -628,7 +630,7 @@ async def publisher_distribution_record(data:
models.api.PublisherDistributionRe
).model_dump(), 200
[email protected]("/publisher/release/announce", methods=["POST"])
[email protected]("/publisher/release/announce", methods=["POST"])
@quart_schema.validate_request(models.api.PublisherReleaseAnnounceArgs)
async def publisher_release_announce(data:
models.api.PublisherReleaseAnnounceArgs) -> DictResponse:
"""
@@ -663,7 +665,7 @@ async def publisher_release_announce(data:
models.api.PublisherReleaseAnnounceAr
).model_dump(), 200
[email protected]("/publisher/ssh/register", methods=["POST"])
[email protected]("/publisher/ssh/register", methods=["POST"])
@quart_schema.validate_request(models.api.PublisherSshRegisterArgs)
async def publisher_ssh_register(data: models.api.PublisherSshRegisterArgs) ->
DictResponse:
"""
@@ -688,7 +690,7 @@ async def publisher_ssh_register(data:
models.api.PublisherSshRegisterArgs) -> D
).model_dump(), 200
[email protected]("/publisher/vote/resolve", methods=["POST"])
[email protected]("/publisher/vote/resolve", methods=["POST"])
@quart_schema.validate_request(models.api.PublisherVoteResolveArgs)
async def publisher_vote_resolve(data: models.api.PublisherVoteResolveArgs) ->
DictResponse:
"""
@@ -717,7 +719,7 @@ async def publisher_vote_resolve(data:
models.api.PublisherVoteResolveArgs) -> D
).model_dump(), 200
[email protected]("/release/announce", methods=["POST"])
[email protected]("/release/announce", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.ReleaseAnnounceArgs)
@@ -756,7 +758,7 @@ async def release_announce(data:
models.api.ReleaseAnnounceArgs) -> DictResponse
).model_dump(), 201
[email protected]("/release/create", methods=["POST"])
[email protected]("/release/create", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.ReleaseCreateArgs)
@@ -783,7 +785,7 @@ async def release_create(data:
models.api.ReleaseCreateArgs) -> DictResponse:
# TODO: Duplicates the below
[email protected]("/release/delete", methods=["POST"])
[email protected]("/release/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.ReleaseDeleteArgs)
@@ -805,7 +807,7 @@ async def release_delete(data:
models.api.ReleaseDeleteArgs) -> DictResponse:
).model_dump(), 200
[email protected]("/release/get/<project>/<version>")
[email protected]("/release/get/<project>/<version>")
@quart_schema.validate_response(models.api.ReleaseGetResults, 200)
async def release_get(project: str, version: str) -> DictResponse:
"""
@@ -821,8 +823,8 @@ async def release_get(project: str, version: str) ->
DictResponse:
).model_dump(), 200
[email protected]("/release/paths/<project>/<version>")
[email protected]("/release/paths/<project>/<version>/<revision>")
[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:
"""
@@ -847,7 +849,7 @@ async def release_paths(project: str, version: str,
revision: str | None = None)
).model_dump(), 200
[email protected]("/release/revisions/<project>/<version>")
[email protected]("/release/revisions/<project>/<version>")
@quart_schema.validate_response(models.api.ReleaseRevisionsResults, 200)
async def release_revisions(project: str, version: str) -> DictResponse:
"""
@@ -866,7 +868,7 @@ async def release_revisions(project: str, version: str) ->
DictResponse:
).model_dump(), 200
[email protected]("/release/upload", methods=["POST"])
[email protected]("/release/upload", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.ReleaseUploadArgs)
@@ -892,7 +894,7 @@ async def release_upload(data:
models.api.ReleaseUploadArgs) -> DictResponse:
).model_dump(), 201
[email protected]("/releases/list")
[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:
@@ -934,7 +936,7 @@ async def releases_list(query_args:
models.api.ReleasesListQuery) -> DictRespons
).model_dump(), 200
[email protected]("/signature/provenance", methods=["POST"])
[email protected]("/signature/provenance", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.SignatureProvenanceArgs)
@@ -997,7 +999,7 @@ async def signature_provenance(data:
models.api.SignatureProvenanceArgs) -> Dict
).model_dump(), 200
[email protected]("/ssh-key/add", methods=["POST"])
[email protected]("/ssh-key/add", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.SshKeyAddArgs)
@@ -1018,7 +1020,7 @@ async def ssh_key_add(data: models.api.SshKeyAddArgs) ->
DictResponse:
).model_dump(), 201
[email protected]("/ssh-key/delete", methods=["POST"])
[email protected]("/ssh-key/delete", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.SshKeyDeleteArgs)
@@ -1039,7 +1041,7 @@ async def ssh_key_delete(data:
models.api.SshKeyDeleteArgs) -> DictResponse:
).model_dump(), 201
[email protected]("/ssh-keys/list/<asf_uid>")
[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:
"""
@@ -1068,7 +1070,7 @@ async def ssh_keys_list(asf_uid: str, query_args:
models.api.SshKeysListQuery) -
).model_dump(), 200
[email protected]("/tasks/list")
[email protected]("/tasks/list")
@quart_schema.validate_querystring(models.api.TasksListQuery)
async def tasks_list(query_args: models.api.TasksListQuery) -> DictResponse:
"""
@@ -1095,7 +1097,7 @@ async def tasks_list(query_args:
models.api.TasksListQuery) -> DictResponse:
).model_dump(), 200
[email protected]("/user/info")
[email protected]("/user/info")
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_response(models.api.UserInfoResults, 200)
@@ -1114,7 +1116,7 @@ async def user_info() -> DictResponse:
).model_dump(), 200
[email protected]("/users/list")
[email protected]("/users/list")
@quart_schema.validate_response(models.api.UsersListResults, 200)
async def users_list() -> DictResponse:
"""
@@ -1152,7 +1154,7 @@ async def users_list() -> DictResponse:
# TODO: Add endpoints to allow users to vote
[email protected]("/vote/resolve", methods=["POST"])
[email protected]("/vote/resolve", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.VoteResolveArgs)
@@ -1187,7 +1189,7 @@ async def vote_resolve(data: models.api.VoteResolveArgs)
-> DictResponse:
).model_dump(), 200
[email protected]("/vote/start", methods=["POST"])
[email protected]("/vote/start", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.VoteStartArgs)
@@ -1230,7 +1232,7 @@ async def vote_start(data: models.api.VoteStartArgs) ->
DictResponse:
).model_dump(), 201
[email protected]("/vote/tabulate", methods=["POST"])
[email protected]("/vote/tabulate", methods=["POST"])
@jwtoken.require
@quart_schema.security_scheme([{"BearerAuth": []}])
@quart_schema.validate_request(models.api.VoteTabulateArgs)
diff --git a/atr/bps/__init__.py b/atr/blueprints/__init__.py
similarity index 57%
copy from atr/bps/__init__.py
copy to atr/blueprints/__init__.py
index 0264e98..e9ae23f 100644
--- a/atr/bps/__init__.py
+++ b/atr/blueprints/__init__.py
@@ -15,14 +15,26 @@
# specific language governing permissions and limitations
# under the License.
+from types import ModuleType
+from typing import Protocol, runtime_checkable
+
import asfquart.base as base
+import atr.blueprints.api as api
+
+
+@runtime_checkable
+class RoutesModule(Protocol):
+ ROUTES_MODULE: bool = True
-def register(app: base.QuartApp) -> None:
- import atr.bps.admin.admin as admin
- import atr.bps.api.api as api
- import atr.bps.icons as icons
- app.register_blueprint(admin.admin.BLUEPRINT)
- app.register_blueprint(api.api.BLUEPRINT)
- app.register_blueprint(icons.BLUEPRINT)
+def check_module(module: ModuleType) -> None:
+ # We need to know that the routes were actually imported
+ # Otherwise ASFQuart will not know about them, even if the blueprint is
registered
+ # In other words, registering a blueprint does not automatically import
its routes
+ if not isinstance(module, RoutesModule):
+ raise ValueError(f"Module {module} is not a RoutesModule")
+
+
+def register(app: base.QuartApp) -> None:
+ check_module(api.register(app))
diff --git a/atr/bps/api/__init__.py b/atr/blueprints/api.py
similarity index 82%
rename from atr/bps/api/__init__.py
rename to atr/blueprints/api.py
index fefb170..d40d6d5 100644
--- a/atr/bps/api/__init__.py
+++ b/atr/blueprints/api.py
@@ -16,6 +16,7 @@
# under the License.
import sys
+from types import ModuleType
from typing import Any
import asfquart.base as base
@@ -25,37 +26,46 @@ import quart.blueprints as blueprints
import quart_schema
import werkzeug.exceptions as exceptions
-BLUEPRINT = quart.Blueprint("api_blueprint", __name__, url_prefix="/api")
+_BLUEPRINT = quart.Blueprint("api_blueprint", __name__, url_prefix="/api")
+
+route = _BLUEPRINT.route
+
+
+def register(app: base.QuartApp) -> ModuleType:
+ import atr.api as api
+
+ app.register_blueprint(_BLUEPRINT)
+ return api
def _exempt_blueprint(app: base.QuartApp) -> None:
csrf = app.extensions.get("csrf")
if csrf is not None:
- csrf.exempt(BLUEPRINT)
+ csrf.exempt(_BLUEPRINT)
[email protected](base.ASFQuartException)
+@_BLUEPRINT.errorhandler(base.ASFQuartException)
async def _handle_asfquart_exception(err: base.ASFQuartException) ->
tuple[quart.Response, int]:
status = getattr(err, "errorcode", 500)
return _json_error(str(err), status)
[email protected](Exception)
+@_BLUEPRINT.errorhandler(Exception)
async def _handle_generic_exception(err: Exception) -> tuple[quart.Response,
int]:
return _json_error(str(err), 500)
[email protected](exceptions.HTTPException)
+@_BLUEPRINT.errorhandler(exceptions.HTTPException)
async def _handle_http_exception(err: exceptions.HTTPException) ->
tuple[quart.Response, int]:
return _json_error(err.description or err.name, err.code)
[email protected](exceptions.NotFound)
+@_BLUEPRINT.errorhandler(exceptions.NotFound)
async def _handle_not_found(err: exceptions.NotFound) -> tuple[quart.Response,
int]:
return _json_error(err.description or err.name, 404)
[email protected](quart_schema.RequestSchemaValidationError)
+@_BLUEPRINT.errorhandler(quart_schema.RequestSchemaValidationError)
async def _handle_request_validation(err:
quart_schema.RequestSchemaValidationError) -> tuple[quart.Response, int]:
if not isinstance(err.validation_error, pydantic.ValidationError):
raise err.validation_error
@@ -78,7 +88,7 @@ def _json_error(
return quart.jsonify(payload), status_code or 500
[email protected]_once
+@_BLUEPRINT.record_once
def _setup(state: blueprints.BlueprintSetupState) -> None:
if isinstance(state.app, base.QuartApp):
_exempt_blueprint(state.app)
diff --git a/atr/bps/__init__.py b/atr/bps/__init__.py
index 0264e98..25f10ae 100644
--- a/atr/bps/__init__.py
+++ b/atr/bps/__init__.py
@@ -20,9 +20,7 @@ import asfquart.base as base
def register(app: base.QuartApp) -> None:
import atr.bps.admin.admin as admin
- import atr.bps.api.api as api
import atr.bps.icons as icons
app.register_blueprint(admin.admin.BLUEPRINT)
- app.register_blueprint(api.api.BLUEPRINT)
app.register_blueprint(icons.BLUEPRINT)
diff --git a/atr/server.py b/atr/server.py
index eba1715..b827078 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -38,6 +38,7 @@ import rich.logging as rich_logging
import werkzeug.routing as routing
import atr
+import atr.blueprints as blueprints
import atr.bps as bps
import atr.config as config
import atr.db as db
@@ -291,6 +292,7 @@ def create_app(app_config: type[config.AppConfig]) ->
base.QuartApp:
db.init_database(app)
register_routes(app)
bps.register(app)
+ blueprints.register(app)
filters.register_filters(app)
app_setup_context(app)
app_setup_lifecycle(app)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]