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]

Reply via email to