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 001250e Separate route helpers from routes
001250e is described below
commit 001250ebf74a0a9f20892e726454bcfd84cd9a1e
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Sep 16 18:36:40 2025 +0100
Separate route helpers from routes
---
atr/{routes/__init__.py => route.py} | 0
atr/routes/__init__.py | 598 ++++++-----------------------------
atr/routes/announce.py | 10 +-
atr/routes/candidate.py | 14 +-
atr/routes/committees.py | 12 +-
atr/routes/compose.py | 8 +-
atr/routes/distribution.py | 26 +-
atr/routes/download.py | 28 +-
atr/routes/draft.py | 44 +--
atr/routes/file.py | 6 +-
atr/routes/finish.py | 14 +-
atr/routes/ignores.py | 18 +-
atr/routes/keys.py | 50 +--
atr/routes/mapping.py | 4 +-
atr/routes/modules.py | 89 ------
atr/routes/preview.py | 18 +-
atr/routes/projects.py | 24 +-
atr/routes/published.py | 12 +-
atr/routes/release.py | 24 +-
atr/routes/report.py | 6 +-
atr/routes/resolve.py | 18 +-
atr/routes/revisions.py | 10 +-
atr/routes/root.py | 22 +-
atr/routes/sbom.py | 10 +-
atr/routes/start.py | 10 +-
atr/routes/tokens.py | 18 +-
atr/routes/upload.py | 6 +-
atr/routes/vote.py | 10 +-
atr/routes/voting.py | 10 +-
atr/server.py | 8 +-
pyproject.toml | 1 -
31 files changed, 318 insertions(+), 810 deletions(-)
diff --git a/atr/routes/__init__.py b/atr/route.py
similarity index 100%
copy from atr/routes/__init__.py
copy to atr/route.py
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index ab7bac1..9a04e8b 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -15,502 +15,102 @@
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-import asyncio
-import functools
-import logging
-import time
-from typing import TYPE_CHECKING, Any, Concatenate, Final, NoReturn,
ParamSpec, Protocol, TypeVar
-
-import aiofiles
-import aiofiles.os
-import asfquart
-import asfquart.auth as auth
-import asfquart.base as base
-import asfquart.session as session
-import quart
-
-import atr.config as config
-import atr.db as db
-import atr.models.sql as sql
-import atr.user as user
-import atr.util as util
-
-if TYPE_CHECKING:
- from collections.abc import Awaitable, Callable, Coroutine, Sequence
-
- import werkzeug.datastructures as datastructures
- import werkzeug.wrappers.response as response
-
-if asfquart.APP is ...:
- raise RuntimeError("APP is not set")
-
-P = ParamSpec("P")
-R = TypeVar("R", covariant=True)
-T = TypeVar("T")
-
-# TODO: Should get this from config, checking debug there
-_MEASURE_PERFORMANCE: Final[bool] = True
-
-
-# | 1 | RSA (Encrypt or Sign) [HAC] |
-# | 2 | RSA Encrypt-Only [HAC] |
-# | 3 | RSA Sign-Only [HAC] |
-# | 16 | Elgamal (Encrypt-Only) [ELGAMAL] [HAC] |
-# | 17 | DSA (Digital Signature Algorithm) [FIPS186] [HAC] |
-# | 18 | ECDH public key algorithm |
-# | 19 | ECDSA public key algorithm [FIPS186] |
-# | 20 | Reserved (formerly Elgamal Encrypt or Sign) |
-# | 21 | Reserved for Diffie-Hellman |
-# | | (X9.42, as defined for IETF-S/MIME) |
-# | 22 | EdDSA [I-D.irtf-cfrg-eddsa] |
-# - https://lists.gnupg.org/pipermail/gnupg-devel/2017-April/032762.html
-# TODO: (Obviously we should move this, but where to?)
-algorithms: Final[dict[int, str]] = {
- 1: "RSA",
- 2: "RSA",
- 3: "RSA",
- 16: "Elgamal",
- 17: "DSA",
- 18: "ECDH",
- 19: "ECDSA",
- 21: "Diffie-Hellman",
- 22: "EdDSA",
-}
-
-
-class AsyncFileHandler(logging.Handler):
- """A logging handler that writes logs asynchronously using aiofiles."""
-
- def __init__(self, filename, mode="w", encoding=None):
- super().__init__()
- self.filename = filename
-
- if mode != "w":
- raise RuntimeError("Only write mode is supported")
-
- self.encoding = encoding
- self.queue = asyncio.Queue()
- self.our_worker_task = None
-
- def our_worker_task_ensure(self):
- """Lazily create the worker task if it doesn't exist and there's an
event loop."""
- if self.our_worker_task is None:
- try:
- loop = asyncio.get_running_loop()
- self.our_worker_task = loop.create_task(self.our_worker())
- except RuntimeError:
- # No event loop running yet, try again on next emit
- ...
-
- async def our_worker(self):
- """Background task that writes queued log messages to file."""
- # Use a binary mode literal with aiofiles.open
- #
https://github.com/Tinche/aiofiles/blob/main/src/aiofiles/threadpool/__init__.py
- # We should be able to use any mode, but pyright requires a binary mode
- async with aiofiles.open(self.filename, "wb+") as f:
- while True:
- record = await self.queue.get()
- if record is None:
- break
-
- try:
- # Format the log record first
- formatted_message = self.format(record) + "\n"
- message_bytes = formatted_message.encode(self.encoding or
"utf-8")
- await f.write(message_bytes)
- await f.flush()
- except Exception:
- self.handleError(record)
- finally:
- self.queue.task_done()
-
- def emit(self, record):
- """Queue the record for writing by the worker task."""
- try:
- # Ensure worker task is running
- self.our_worker_task_ensure()
-
- # Queue the record, but handle the case where no event loop is
running yet
- try:
- self.queue.put_nowait(record)
- except RuntimeError:
- ...
- except Exception:
- self.handleError(record)
-
- def close(self):
- """Shut down the worker task cleanly."""
- if self.our_worker_task is not None and not
self.our_worker_task.done():
- try:
- self.queue.put_nowait(None)
- except RuntimeError:
- # No running event loop, no need to clean up
- ...
- super().close()
-
-
-# This is the type of functions to which we apply @committer_get
-# In other words, functions which accept CommitterSession as their first arg
-class CommitterRouteHandler(Protocol[R]):
- """Protocol for @committer_get decorated functions."""
-
- __name__: str
- __doc__: str | None
-
- def __call__(self, session: CommitterSession, *args: Any, **kwargs: Any)
-> Awaitable[R]: ...
-
-
-class CommitterSession:
- """Session with extra information about committers."""
-
- def __init__(self, web_session: session.ClientSession) -> None:
- self._projects: list[sql.Project] | None = None
- self._session = web_session
-
- @property
- def asf_uid(self) -> str:
- if self._session.uid is None:
- raise base.ASFQuartException("Not authenticated", errorcode=401)
- return self._session.uid
-
- def __getattr__(self, name: str) -> Any:
- # TODO: Not type safe, should subclass properly if possible
- # For example, we can access session.no_such_attr and the type
checkers won't notice
- return getattr(self._session, name)
-
- async def check_access(self, project_name: str) -> None:
- if not any((p.name == project_name) for p in (await
self.user_projects)):
- if user.is_admin(self.uid):
- # Admins can view all projects
- # But we must warn them when the project is not one of their
own
- # TODO: This code is difficult to test locally
- # TODO: This flash sometimes displays after deleting a
project, which is a bug
- await quart.flash("This is not your project, but you have
access as an admin", "warning")
- return
- raise base.ASFQuartException("You do not have access to this
project", errorcode=403)
-
- async def check_access_committee(self, committee_name: str) -> None:
- if committee_name not in self.committees:
- if user.is_admin(self.uid):
- # Admins can view all committees
- # But we must warn them when the committee is not one of their
own
- # TODO: As above, this code is difficult to test locally
- await quart.flash("This is not your committee, but you have
access as an admin", "warning")
- return
- raise base.ASFQuartException("You do not have access to this
committee", errorcode=403)
-
- @property
- def app_host(self) -> str:
- return config.get().APP_HOST
-
- @property
- def host(self) -> str:
- request_host = quart.request.host
- if ":" in request_host:
- domain, port = request_host.split(":")
- # Could be an IPv6 address, so need to check whether port is a
valid integer
- if port.isdigit():
- return domain
- return request_host
-
- def only_user_releases(self, releases: Sequence[sql.Release]) ->
list[sql.Release]:
- return util.user_releases(self.uid, releases)
-
- async def redirect(
- self, route: CommitterRouteHandler[R], success: str | None = None,
error: str | None = None, **kwargs: Any
- ) -> response.Response:
- """Redirect to a route with a success or error message."""
- if success is not None:
- await quart.flash(success, "success")
- elif error is not None:
- await quart.flash(error, "error")
- return quart.redirect(util.as_url(route, **kwargs))
-
- async def release(
- self,
- project_name: str,
- version_name: str,
- phase: sql.ReleasePhase | db.NotSet | None = db.NOT_SET,
- latest_revision_number: str | db.NotSet | None = db.NOT_SET,
- data: db.Session | None = None,
- with_committee: bool = True,
- with_project: bool = True,
- with_release_policy: bool = False,
- with_project_release_policy: bool = False,
- with_revisions: bool = False,
- ) -> sql.Release:
- # We reuse db.NOT_SET as an entirely different sentinel
- # TODO: We probably shouldn't do that, or should make it clearer
- if phase is None:
- phase_value = db.NOT_SET
- elif phase is db.NOT_SET:
- phase_value = sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
- else:
- phase_value = phase
- release_name = sql.release_name(project_name, version_name)
- if data is None:
- async with db.session() as data:
- release = await data.release(
- name=release_name,
- phase=phase_value,
- latest_revision_number=latest_revision_number,
- _committee=with_committee,
- _project=with_project,
- _release_policy=with_release_policy,
- _project_release_policy=with_project_release_policy,
- _revisions=with_revisions,
- ).demand(base.ASFQuartException("Release does not exist",
errorcode=404))
- else:
- release = await data.release(
- name=release_name,
- phase=phase_value,
- latest_revision_number=latest_revision_number,
- _committee=with_committee,
- _project=with_project,
- _release_policy=with_release_policy,
- _project_release_policy=with_project_release_policy,
- _revisions=with_revisions,
- ).demand(base.ASFQuartException("Release does not exist",
errorcode=404))
- return release
-
- @property
- async def user_candidate_drafts(self) -> list[sql.Release]:
- return await user.candidate_drafts(self.uid,
user_projects=self._projects)
-
- # @property
- # async def user_committees(self) -> list[models.Committee]:
- # return ...
-
- @property
- async def user_projects(self) -> list[sql.Project]:
- if self._projects is None:
- self._projects = await user.projects(self.uid)
- return self._projects[:]
-
-
-class FlashError(RuntimeError): ...
-
-
-class MicrosecondsFormatter(logging.Formatter):
- # Answers on a postcard if you know why Python decided to use a comma by
default
- default_msec_format = "%s.%03d"
-
-
-# Setup a dedicated logger for route performance metrics
-# NOTE: This code block must come after AsyncFileHandler and
MicrosecondsFormatter
-route_logger: Final = logging.getLogger("route.performance")
-# Use custom formatter that properly includes microseconds
-# TODO: Is this actually UTC?
-route_logger_handler: Final[AsyncFileHandler] =
AsyncFileHandler("route-performance.log")
-route_logger_handler.setFormatter(MicrosecondsFormatter("%(asctime)s -
%(message)s"))
-route_logger.addHandler(route_logger_handler)
-route_logger.setLevel(logging.INFO)
-# If we don't set propagate to False then it logs to the term as well
-route_logger.propagate = False
-
-
-# This is the type of functions to which we apply @app_route
-# In other words, functions which accept no session
-class RouteHandler(Protocol[R]):
- """Protocol for @app_route decorated functions."""
-
- __name__: str
- __doc__: str | None
-
- def __call__(self, *args: Any, **kwargs: Any) -> Awaitable[R]: ...
-
-
-def app_route(
- path: str, methods: list[str] | None = None, endpoint: str | None = None,
measure_performance: bool = True
-) -> Callable:
- """Register a route with the Flask app with built-in performance
logging."""
-
- def decorator(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P,
Awaitable[T]]:
- # First apply our performance measuring decorator
- if _MEASURE_PERFORMANCE and measure_performance:
- measured_func = app_route_performance_measure(path, methods)(f)
- else:
- measured_func = f
- # Then apply the original route decorator
- return asfquart.APP.route(path, methods=methods,
endpoint=endpoint)(measured_func)
-
- return decorator
-
-
-def app_route_performance_measure(route_path: str, http_methods: list[str] |
None = None) -> Callable:
- """Decorator that measures and logs route performance with path and method
information."""
-
- # def format_time(seconds: float) -> str:
- # """Format time in appropriate units (µs or ms)."""
- # microseconds = seconds * 1_000_000
- # if microseconds < 1000:
- # return f"{microseconds:.2f} µs"
- # else:
- # milliseconds = microseconds / 1000
- # return f"{milliseconds:.2f} ms"
-
- def decorator(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P,
Awaitable[T]]:
- @functools.wraps(f)
- async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
- # This wrapper is based on an outstanding idea by Mostafa Farzán
- # Farzán realised that we can step the event loop manually
- # That way, we can also divide it into synchronous and
asynchronous parts
- # The synchronous part is done using coro.send(None)
- # The asynchronous part is done using asyncio.sleep(0)
- # We use two methods for measuring the async part, and take the
largest
- # This performance measurement adds a bit of overhead, about
10-20ms
- # Therefore it should be avoided in production, or made more
efficient
- # We could perhaps use for a small portion of requests
- blocking_time = 0.0
- async_time = 0.0
- loop_time = 0.0
- total_start = time.perf_counter()
- coro = f(*args, **kwargs)
- try:
- while True:
- # Measure the synchronous part
- sync_start = time.perf_counter()
- future = coro.send(None)
- sync_end = time.perf_counter()
- blocking_time += sync_end - sync_start
-
- # Measure the asynchronous part in two different ways
- loop = asyncio.get_running_loop()
- wait_start = time.perf_counter()
- loop_start = loop.time()
- if future is not None:
- done = asyncio.Event()
- future.add_done_callback(lambda _: done.set())
- await done.wait()
- wait_end = time.perf_counter()
- loop_end = loop.time()
- async_time += wait_end - wait_start
- loop_time += loop_end - loop_start
-
- # Raise exception if any
- # future.result()
- except StopIteration as e:
- total_end = time.perf_counter()
- total_time = total_end - total_start
-
- methods_str = ",".join(http_methods) if http_methods else "GET"
-
- nonblocking_time = max(async_time, loop_time)
- # If async time is more than 10% different from loop time, log
it
- delta_symbol = "="
- nonblocking_delta = abs(async_time - loop_time)
- # Must check that nonblocking_time is not 0 to avoid division
by zero
- if nonblocking_time and ((nonblocking_delta /
nonblocking_time) > 0.1):
- delta_symbol = "!"
- route_logger.info(
- "%s %s %s %s %s %s %s",
- methods_str,
- route_path,
- f.__name__,
- delta_symbol,
- int(blocking_time * 1000),
- int(nonblocking_time * 1000),
- int(total_time * 1000),
- )
-
- return e.value
-
- return wrapper
-
- return decorator
-
-
-# This decorator is an adaptor between @committer_get and @app_route functions
-def committer(
- path: str, methods: list[str] | None = None, measure_performance: bool =
True
-) -> Callable[[CommitterRouteHandler[R]], RouteHandler[R]]:
- """Decorator for committer GET routes that provides an enhanced session
object."""
-
- def decorator(func: CommitterRouteHandler[R]) -> RouteHandler[R]:
- async def wrapper(*args: Any, **kwargs: Any) -> R:
- web_session = await session.read()
- if web_session is None:
- _authentication_failed()
-
- enhanced_session = CommitterSession(web_session)
- return await func(enhanced_session, *args, **kwargs)
-
- # Generate a unique endpoint name
- endpoint = func.__module__ + "_" + func.__name__
-
- # Set the name before applying decorators
- wrapper.__name__ = func.__name__
- wrapper.__doc__ = func.__doc__
- wrapper.__annotations__["endpoint"] = endpoint
-
- # Apply decorators in reverse order
- decorated = auth.require(auth.Requirements.committer)(wrapper)
- decorated = app_route(
- path, methods=methods or ["GET"], endpoint=endpoint,
measure_performance=measure_performance
- )(decorated)
-
- return decorated
-
- return decorator
-
-
-async def get_form(request: quart.Request) -> datastructures.MultiDict:
- # The request.form() method in Quart calls a synchronous tempfile method
- # It calls quart.wrappers.request.form _load_form_data
- # Which calls quart.formparser parse and parse_func and parser.parse
- # Which calls _write which calls tempfile, which is synchronous
- # It's getting a tempfile back from some prior call
- # We can't just make blockbuster ignore the call because then it ignores
it everywhere
- app = asfquart.APP
-
- if app is ...:
- raise RuntimeError("APP is not set")
-
- # Or quart.current_app?
- blockbuster = app.extensions.get("blockbuster")
-
- # Turn blockbuster off
- if blockbuster is not None:
- blockbuster.deactivate()
- form = await request.form
- # Turn blockbuster on
- if blockbuster is not None:
- blockbuster.activate()
- return form
-
-
-def public(
- path: str, methods: list[str] | None = None, measure_performance: bool =
True
-) -> Callable[[Callable[Concatenate[CommitterSession | None, P],
Awaitable[R]]], RouteHandler[R]]:
- """Decorator for public GET routes that provides an enhanced session
object."""
-
- def decorator(func: Callable[Concatenate[CommitterSession | None, P],
Awaitable[R]]) -> RouteHandler[R]:
- async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
- web_session = await session.read()
- enhanced_session = CommitterSession(web_session) if web_session
else None
- return await func(enhanced_session, *args, **kwargs)
-
- # Generate a unique endpoint name
- endpoint = func.__module__ + "_" + func.__name__
-
- # Set the name before applying decorators
- wrapper.__name__ = func.__name__
- wrapper.__doc__ = func.__doc__
- wrapper.__annotations__["endpoint"] = endpoint
-
- # Apply decorators in reverse order
- decorated = app_route(
- path, methods=methods or ["GET"], endpoint=endpoint,
measure_performance=measure_performance
- )(wrapper)
-
- return decorated
-
- return decorator
-
-
-def _authentication_failed() -> NoReturn:
- """Handle authentication failure with an exception."""
- # NOTE: This is a separate function to fix a problem with analysis flow in
mypy
- raise base.ASFQuartException("Not authenticated", errorcode=401)
+import atr.routes.announce as announce
+import atr.routes.candidate as candidate
+import atr.routes.committees as committees
+import atr.routes.compose as compose
+import atr.routes.distribution as distribution
+import atr.routes.download as download
+import atr.routes.draft as draft
+import atr.routes.file as file
+import atr.routes.finish as finish
+import atr.routes.ignores as ignores
+import atr.routes.keys as keys
+import atr.routes.preview as preview
+import atr.routes.projects as projects
+import atr.routes.published as published
+import atr.routes.release as release
+import atr.routes.report as report
+import atr.routes.resolve as resolve
+import atr.routes.revisions as revisions
+import atr.routes.root as root
+import atr.routes.sbom as sbom
+import atr.routes.start as start
+import atr.routes.tokens as tokens
+import atr.routes.upload as upload
+import atr.routes.vote as vote
+import atr.routes.voting as voting
+
+__all__ = [
+ "announce",
+ "candidate",
+ "committees",
+ "compose",
+ "distribution",
+ "download",
+ "draft",
+ "file",
+ "finish",
+ "ignores",
+ "keys",
+ "preview",
+ "projects",
+ "published",
+ "release",
+ "report",
+ "resolve",
+ "revisions",
+ "root",
+ "sbom",
+ "start",
+ "tokens",
+ "upload",
+ "vote",
+ "voting",
+]
+
+
+# Export data for a custom linter script
+def _export_routes() -> None:
+ import asyncio
+
+ async def _export_routes_async() -> None:
+ """Export all routes to a JSON file for static analysis."""
+ import json
+ import sys
+
+ import aiofiles
+
+ route_paths: list[str] = []
+ current_module = sys.modules[__name__]
+
+ for module_name in dir(current_module):
+ if module_name.startswith("_"):
+ # Not intended for external use
+ continue
+
+ module = getattr(current_module, module_name)
+ if not hasattr(module, "__file__"):
+ # Not a module
+ continue
+
+ # Get all callable interfaces that do not begin with an underscore
+ for attr_name in dir(module):
+ if attr_name.startswith("_"):
+ # Not intended for external use
+ continue
+ if not callable(getattr(module, attr_name)):
+ # Not callable
+ continue
+ route_path = f"{module_name}.{attr_name}"
+ route_paths.append(route_path)
+
+ async with aiofiles.open("routes.json", "w", encoding="utf-8") as f:
+ await f.write(json.dumps(route_paths, indent=2))
+
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(_export_routes_async())
+
+
+_export_routes()
+del _export_routes
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
index dd14908..d15909a 100644
--- a/atr/routes/announce.py
+++ b/atr/routes/announce.py
@@ -25,7 +25,7 @@ import atr.config as config
import atr.construct as construct
import atr.forms as forms
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.release as routes_release
import atr.storage as storage
import atr.template as template
@@ -60,8 +60,8 @@ class DeleteForm(forms.Typed):
submit = forms.submit("Delete preview")
[email protected]("/announce/<project_name>/<version_name>")
-async def selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> str | response.Response:
[email protected]("/announce/<project_name>/<version_name>")
+async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> str | response.Response:
"""Allow the user to announce a release preview."""
await session.check_access(project_name)
@@ -105,9 +105,9 @@ async def selected(session: routes.CommitterSession,
project_name: str, version_
)
[email protected]("/announce/<project_name>/<version_name>", methods=["POST"])
[email protected]("/announce/<project_name>/<version_name>", methods=["POST"])
async def selected_post(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> str | response.Response:
"""Handle the announcement form submission and promote the preview to
release."""
await session.check_access(project_name)
diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index 3fec17b..61621f1 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -24,7 +24,7 @@ import werkzeug.wrappers.response as response
import atr.db as db
import atr.log as log
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.root as root
import atr.template as template
import atr.util as util
@@ -33,15 +33,15 @@ if asfquart.APP is ...:
raise RuntimeError("APP is not set")
[email protected]("/candidate/delete", methods=["POST"])
-async def delete(session: routes.CommitterSession) -> response.Response:
[email protected]("/candidate/delete", methods=["POST"])
+async def delete(session: route.CommitterSession) -> response.Response:
"""Delete a release candidate."""
# TODO: We need to never retire revisions, if allowing release deletion
return await session.redirect(root.index, error="Not yet implemented")
[email protected]("/candidate/view/<project_name>/<version_name>")
-async def view(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/candidate/view/<project_name>/<version_name>")
+async def view(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""View all the files in the rsync upload directory for a release."""
await session.check_access(project_name)
@@ -75,9 +75,9 @@ async def view(session: routes.CommitterSession,
project_name: str, version_name
)
[email protected]("/candidate/view/<project_name>/<version_name>/<path:file_path>")
[email protected]("/candidate/view/<project_name>/<version_name>/<path:file_path>")
async def view_path(
- session: routes.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
) -> response.Response | str:
"""View the content of a specific file in the release candidate."""
await session.check_access(project_name)
diff --git a/atr/routes/committees.py b/atr/routes/committees.py
index 1008d63..f743ddc 100644
--- a/atr/routes/committees.py
+++ b/atr/routes/committees.py
@@ -23,7 +23,7 @@ import http.client
import atr.db as db
import atr.forms as forms
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.template as template
import atr.util as util
@@ -32,8 +32,8 @@ class UpdateCommitteeKeysForm(forms.Typed):
submit = forms.submit("Regenerate KEYS file")
[email protected]("/committees")
-async def directory(session: routes.CommitterSession | None) -> str:
[email protected]("/committees")
+async def directory(session: route.CommitterSession | None) -> str:
"""Main committee directory page."""
async with db.session() as data:
committees = await
data.committee(_projects=True).order_by(sql.Committee.name).all()
@@ -44,8 +44,8 @@ async def directory(session: routes.CommitterSession | None)
-> str:
)
[email protected]("/committees/<name>")
-async def view(session: routes.CommitterSession | None, name: str) -> str:
[email protected]("/committees/<name>")
+async def view(session: route.CommitterSession | None, name: str) -> str:
# TODO: Could also import this from keys.py
async with db.session() as data:
committee = await data.committee(
@@ -61,7 +61,7 @@ async def view(session: routes.CommitterSession | None, name:
str) -> str:
"committee-view.html",
committee=committee,
projects=project_list,
- algorithms=routes.algorithms,
+ algorithms=route.algorithms,
now=datetime.datetime.now(datetime.UTC),
email_from_key=util.email_from_uid,
update_committee_keys_form=await UpdateCommitteeKeysForm.create_form(),
diff --git a/atr/routes/compose.py b/atr/routes/compose.py
index b147808..bfa3c87 100644
--- a/atr/routes/compose.py
+++ b/atr/routes/compose.py
@@ -27,7 +27,7 @@ import atr.forms as forms
import atr.models.results as results
import atr.models.sql as sql
import atr.revision as revision
-import atr.routes as routes
+import atr.route as route
import atr.routes.draft as draft
import atr.routes.mapping as mapping
import atr.storage as storage
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
async def check(
- session: routes.CommitterSession,
+ session: route.CommitterSession,
release: sql.Release,
task_mid: str | None = None,
form: wtforms.Form | None = None,
@@ -119,8 +119,8 @@ async def check(
)
[email protected]("/compose/<project_name>/<version_name>")
-async def selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/compose/<project_name>/<version_name>")
+async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""Show the contents of the release candidate draft."""
await session.check_access(project_name)
diff --git a/atr/routes/distribution.py b/atr/routes/distribution.py
index 6c0c1b6..23508b8 100644
--- a/atr/routes/distribution.py
+++ b/atr/routes/distribution.py
@@ -29,7 +29,7 @@ import atr.forms as forms
import atr.htm as htm
import atr.models.distribution as distribution
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.routes.finish as finish
import atr.storage as storage
@@ -93,8 +93,8 @@ class FormProjectVersion:
version: str
[email protected]("/distribution/delete/<project>/<version>", methods=["POST"])
-async def delete(session: routes.CommitterSession, project: str, version: str)
-> response.Response:
[email protected]("/distribution/delete/<project>/<version>", methods=["POST"])
+async def delete(session: route.CommitterSession, project: str, version: str)
-> response.Response:
form = await DeleteForm.create_form(data=await quart.request.form)
dd = distribution.DeleteData.model_validate(form.data)
@@ -122,8 +122,8 @@ async def delete(session: routes.CommitterSession, project:
str, version: str) -
)
[email protected]("/distributions/list/<project>/<version>", methods=["GET"])
-async def list_get(session: routes.CommitterSession, project: str, version:
str) -> str:
[email protected]("/distributions/list/<project>/<version>", methods=["GET"])
+async def list_get(session: route.CommitterSession, project: str, version:
str) -> str:
async with db.session() as data:
distributions = await data.distribution(
release_name=sql.release_name(project, version),
@@ -205,15 +205,15 @@ async def list_get(session: routes.CommitterSession,
project: str, version: str)
return await template.blank(title, content=block.collect())
[email protected]("/distribution/record/<project>/<version>", methods=["GET"])
-async def record(session: routes.CommitterSession, project: str, version: str)
-> str:
[email protected]("/distribution/record/<project>/<version>", methods=["GET"])
+async def record(session: route.CommitterSession, project: str, version: str)
-> str:
form = await DistributeForm.create_form(data={"package": project,
"version": version})
fpv = FormProjectVersion(form=form, project=project, version=version)
return await _record_form_page(fpv)
[email protected]("/distribution/record/<project>/<version>", methods=["POST"])
-async def record_post(session: routes.CommitterSession, project: str, version:
str) -> str:
[email protected]("/distribution/record/<project>/<version>", methods=["POST"])
+async def record_post(session: route.CommitterSession, project: str, version:
str) -> str:
form = await DistributeForm.create_form(data=await quart.request.form)
fpv = FormProjectVersion(form=form, project=project, version=version)
if await form.validate():
@@ -229,15 +229,15 @@ async def record_post(session: routes.CommitterSession,
project: str, version: s
return await _record_form_page(fpv)
[email protected]("/distribution/stage/<project>/<version>", methods=["GET"])
-async def stage(session: routes.CommitterSession, project: str, version: str)
-> str:
[email protected]("/distribution/stage/<project>/<version>", methods=["GET"])
+async def stage(session: route.CommitterSession, project: str, version: str)
-> str:
form = await DistributeForm.create_form(data={"package": project,
"version": version})
fpv = FormProjectVersion(form=form, project=project, version=version)
return await _record_form_page(fpv, staging=True)
[email protected]("/distribution/stage/<project>/<version>", methods=["POST"])
-async def stage_post(session: routes.CommitterSession, project: str, version:
str) -> str:
[email protected]("/distribution/stage/<project>/<version>", methods=["POST"])
+async def stage_post(session: route.CommitterSession, project: str, version:
str) -> str:
form = await DistributeForm.create_form(data=await quart.request.form)
fpv = FormProjectVersion(form=form, project=project, version=version)
if await form.validate():
diff --git a/atr/routes/download.py b/atr/routes/download.py
index b1d9b25..974fc40 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -30,16 +30,16 @@ import zipstream
import atr.config as config
import atr.db as db
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.mapping as mapping
import atr.routes.root as root
import atr.template as template
import atr.util as util
[email protected]("/download/all/<project_name>/<version_name>")
[email protected]("/download/all/<project_name>/<version_name>")
async def all_selected(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> response.Response | str:
"""Display download commands for a release."""
async with db.session() as data:
@@ -64,25 +64,25 @@ async def all_selected(
)
[email protected]("/download/path/<project_name>/<version_name>/<path:file_path>")
[email protected]("/download/path/<project_name>/<version_name>/<path:file_path>")
async def path(
- session: routes.CommitterSession | None, project_name: str, version_name:
str, file_path: str
+ session: route.CommitterSession | None, project_name: str, version_name:
str, file_path: str
) -> response.Response | quart.Response:
"""Download a file or list a directory from a release in any phase."""
return await _download_or_list(project_name, version_name, file_path)
[email protected]("/download/path/<project_name>/<version_name>/")
[email protected]("/download/path/<project_name>/<version_name>/")
async def path_empty(
- session: routes.CommitterSession | None, project_name: str, version_name:
str
+ session: route.CommitterSession | None, project_name: str, version_name:
str
) -> response.Response | quart.Response:
"""List files at the root of a release directory for download."""
return await _download_or_list(project_name, version_name, ".")
[email protected]("/download/sh/<project_name>/<version_name>")
[email protected]("/download/sh/<project_name>/<version_name>")
async def sh_selected(
- session: routes.CommitterSession | None, project_name: str, version_name:
str
+ session: route.CommitterSession | None, project_name: str, version_name:
str
) -> response.Response | quart.Response:
"""Shell script to download a release."""
conf = config.get()
@@ -97,9 +97,9 @@ async def sh_selected(
return quart.Response(content, mimetype="text/x-shellscript")
[email protected]("/download/urls/<project_name>/<version_name>")
[email protected]("/download/urls/<project_name>/<version_name>")
async def urls_selected(
- session: routes.CommitterSession | None, project_name: str, version_name:
str
+ session: route.CommitterSession | None, project_name: str, version_name:
str
) -> response.Response | quart.Response:
try:
async with db.session() as data:
@@ -114,9 +114,9 @@ async def urls_selected(
return quart.Response(f"Internal server error: {e}", status=500,
mimetype="text/plain")
[email protected]("/download/zip/<project_name>/<version_name>")
[email protected]("/download/zip/<project_name>/<version_name>")
async def zip_selected(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> response.Response | quart.wrappers.response.Response:
try:
release = await session.release(project_name=project_name,
version_name=version_name, phase=None)
@@ -154,7 +154,7 @@ async def _download_or_list(project_name: str,
version_name: str, file_path: str
# Check that path is relative
original_path = pathlib.Path(file_path)
if (file_path != ".") and (not
original_path.is_relative_to(original_path.anchor)):
- raise routes.FlashError("Path must be relative")
+ raise route.FlashError("Path must be relative")
# We allow downloading files from any phase
async with db.session() as data:
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 001d31e..71b5f60 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -33,7 +33,7 @@ import atr.forms as forms
import atr.log as log
import atr.models.sql as sql
import atr.revision as revision
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.routes.root as root
import atr.routes.upload as upload
@@ -73,8 +73,8 @@ class VotePreviewForm(forms.Typed):
vote_duration = forms.integer("Vote duration")
[email protected]("/draft/delete", methods=["POST"])
-async def delete(session: routes.CommitterSession) -> response.Response:
[email protected]("/draft/delete", methods=["POST"])
+async def delete(session: route.CommitterSession) -> response.Response:
"""Delete a candidate draft and all its associated files."""
form = await DeleteForm.create_form(data=await quart.request.form)
if not await form.validate_on_submit():
@@ -117,8 +117,8 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(root.index, success="Candidate draft deleted
successfully")
[email protected]("/draft/delete-file/<project_name>/<version_name>",
methods=["POST"])
-async def delete_file(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response:
[email protected]("/draft/delete-file/<project_name>/<version_name>",
methods=["POST"])
+async def delete_file(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
"""Delete a specific file from the release candidate, creating a new
revision."""
await session.check_access(project_name)
@@ -152,8 +152,8 @@ async def delete_file(session: routes.CommitterSession,
project_name: str, versi
)
[email protected]("/draft/fresh/<project_name>/<version_name>",
methods=["POST"])
-async def fresh(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response:
[email protected]("/draft/fresh/<project_name>/<version_name>",
methods=["POST"])
+async def fresh(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
"""Restart all checks for a whole release candidate draft."""
# Admin only button, but it's okay if users find and use this manually
await session.check_access(project_name)
@@ -176,9 +176,9 @@ async def fresh(session: routes.CommitterSession,
project_name: str, version_nam
)
[email protected]("/draft/hashgen/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
[email protected]("/draft/hashgen/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
async def hashgen(
- session: routes.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
) -> response.Response:
"""Generate an sha256 or sha512 hash file for a candidate draft file,
creating a new revision."""
await session.check_access(project_name)
@@ -211,9 +211,9 @@ async def hashgen(
)
[email protected]("/draft/sbomgen/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
[email protected]("/draft/sbomgen/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
async def sbomgen(
- session: routes.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
) -> response.Response:
"""Generate a CycloneDX SBOM file for a candidate draft file, creating a
new revision."""
await session.check_access(project_name)
@@ -238,14 +238,14 @@ async def sbomgen(
# Check that the source file exists in the new revision
if not await aiofiles.os.path.exists(path_in_new_revision):
log.error(f"Source file {rel_path} not found in new revision
for SBOM generation.")
- raise routes.FlashError("Source artifact file not found in the
new revision.")
+ raise route.FlashError("Source artifact file not found in the
new revision.")
# Check that the SBOM file does not already exist in the new
revision
if await aiofiles.os.path.exists(sbom_path_in_new_revision):
raise base.ASFQuartException("SBOM file already exists",
errorcode=400)
if creating.new is None:
- raise routes.FlashError("Internal error: New revision not found")
+ raise route.FlashError("Internal error: New revision not found")
# Create and queue the task, using paths within the new revision
async with storage.write(session.uid) as write:
@@ -268,8 +268,8 @@ async def sbomgen(
)
[email protected]("/draft/svnload/<project_name>/<version_name>",
methods=["POST"])
-async def svnload(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/draft/svnload/<project_name>/<version_name>",
methods=["POST"])
+async def svnload(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""Import files from SVN into a draft."""
await session.check_access(project_name)
@@ -312,8 +312,8 @@ async def svnload(session: routes.CommitterSession,
project_name: str, version_n
)
[email protected]("/draft/tools/<project_name>/<version_name>/<path:file_path>")
-async def tools(session: routes.CommitterSession, project_name: str,
version_name: str, file_path: str) -> str:
[email protected]("/draft/tools/<project_name>/<version_name>/<path:file_path>")
+async def tools(session: route.CommitterSession, project_name: str,
version_name: str, file_path: str) -> str:
"""Show the tools for a specific file."""
await session.check_access(project_name)
@@ -348,8 +348,8 @@ async def tools(session: routes.CommitterSession,
project_name: str, version_nam
# TODO: Should we deprecate this and ensure compose covers it all?
# If we did that, we'd lose the exhaustive use of the abstraction
[email protected]("/draft/view/<project_name>/<version_name>")
-async def view(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/draft/view/<project_name>/<version_name>")
+async def view(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""View all the files in the rsync upload directory for a release."""
await session.check_access(project_name)
@@ -379,9 +379,9 @@ async def view(session: routes.CommitterSession,
project_name: str, version_name
)
[email protected]("/draft/vote/preview/<project_name>/<version_name>",
methods=["POST"])
[email protected]("/draft/vote/preview/<project_name>/<version_name>",
methods=["POST"])
async def vote_preview(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> quart.wrappers.response.Response | response.Response | str:
"""Show the vote email preview for a release."""
@@ -391,7 +391,7 @@ async def vote_preview(
release = await session.release(project_name, version_name)
if release.committee is None:
- raise routes.FlashError("Release has no associated committee")
+ raise route.FlashError("Release has no associated committee")
form_body: str = util.unwrap(form.body.data)
asfuid = session.uid
diff --git a/atr/routes/file.py b/atr/routes/file.py
index 61a9de8..1879ab6 100644
--- a/atr/routes/file.py
+++ b/atr/routes/file.py
@@ -17,14 +17,14 @@
import werkzeug.wrappers.response as response
-import atr.routes as routes
+import atr.route as route
import atr.template as template
import atr.util as util
[email protected]("/file/<project_name>/<version_name>/<path:file_path>")
[email protected]("/file/<project_name>/<version_name>/<path:file_path>")
async def selected_path(
- session: routes.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
) -> response.Response | str:
"""View the content of a specific file in the release candidate draft."""
# TODO: Make this independent of the release phase
diff --git a/atr/routes/finish.py b/atr/routes/finish.py
index 33c373f..e84dc58 100644
--- a/atr/routes/finish.py
+++ b/atr/routes/finish.py
@@ -34,7 +34,7 @@ import atr.db as db
import atr.forms as forms
import atr.log as log
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.mapping as mapping
import atr.routes.root as root
import atr.storage as storage
@@ -81,7 +81,7 @@ class RemoveRCTagsForm(forms.Typed):
@dataclasses.dataclass
class ProcessFormDataArgs:
formdata: datastructures.MultiDict
- session: routes.CommitterSession
+ session: route.CommitterSession
project_name: str
version_name: str
move_form: MoveFileForm
@@ -99,9 +99,9 @@ class RCTagAnalysisResult:
total_paths: int
[email protected]("/finish/<project_name>/<version_name>", methods=["GET",
"POST"])
[email protected]("/finish/<project_name>/<version_name>", methods=["GET",
"POST"])
async def selected(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> tuple[quart_response.Response, int] | response.Response | str:
"""Finish a release preview."""
await session.check_access(project_name)
@@ -245,7 +245,7 @@ async def _deletable_choices(latest_revision_dir:
pathlib.Path, target_dirs: set
async def _delete_empty_directory(
dir_to_delete_rel: pathlib.Path,
- session: routes.CommitterSession,
+ session: route.CommitterSession,
project_name: str,
version_name: str,
respond: Respond,
@@ -266,7 +266,7 @@ async def _delete_empty_directory(
async def _move_file_to_revision(
source_files_rel: list[pathlib.Path],
target_dir_rel: pathlib.Path,
- session: routes.CommitterSession,
+ session: route.CommitterSession,
project_name: str,
version_name: str,
respond: Respond,
@@ -307,7 +307,7 @@ async def _move_file_to_revision(
async def _remove_rc_tags(
- session: routes.CommitterSession,
+ session: route.CommitterSession,
project_name: str,
version_name: str,
respond: Respond,
diff --git a/atr/routes/ignores.py b/atr/routes/ignores.py
index f5b451a..8bd1c20 100644
--- a/atr/routes/ignores.py
+++ b/atr/routes/ignores.py
@@ -33,7 +33,7 @@ from htpy import (
import atr.forms as forms
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.storage as storage
import atr.template as template
import atr.util as util
@@ -104,8 +104,8 @@ class UpdateIgnoreForm(forms.Typed):
submit = forms.submit("Update ignore")
[email protected]("/ignores/<committee_name>", methods=["GET", "POST"])
-async def ignores(session: routes.CommitterSession, committee_name: str) ->
str | response.Response:
[email protected]("/ignores/<committee_name>", methods=["GET", "POST"])
+async def ignores(session: route.CommitterSession, committee_name: str) -> str
| response.Response:
async with storage.read() as read:
ragp = read.as_general_public()
ignores = await ragp.checks.ignores(committee_name)
@@ -121,8 +121,8 @@ async def ignores(session: routes.CommitterSession,
committee_name: str) -> str
return await template.blank("Ignored checks", content)
[email protected]("/ignores/<committee_name>/add", methods=["POST"])
-async def ignores_committee_add(session: routes.CommitterSession,
committee_name: str) -> str | response.Response:
[email protected]("/ignores/<committee_name>/add", methods=["POST"])
+async def ignores_committee_add(session: route.CommitterSession,
committee_name: str) -> str | response.Response:
data = await quart.request.form
form = await AddIgnoreForm.create_form(data=data)
if not (await form.validate_on_submit()):
@@ -149,8 +149,8 @@ async def ignores_committee_add(session:
routes.CommitterSession, committee_name
)
[email protected]("/ignores/<committee_name>/delete", methods=["POST"])
-async def ignores_committee_delete(session: routes.CommitterSession,
committee_name: str) -> str | response.Response:
[email protected]("/ignores/<committee_name>/delete", methods=["POST"])
+async def ignores_committee_delete(session: route.CommitterSession,
committee_name: str) -> str | response.Response:
data = await quart.request.form
form = await DeleteIgnoreForm.create_form(data=data)
if not (await form.validate_on_submit()):
@@ -179,8 +179,8 @@ async def ignores_committee_delete(session:
routes.CommitterSession, committee_n
)
[email protected]("/ignores/<committee_name>/update", methods=["POST"])
-async def ignores_committee_update(session: routes.CommitterSession,
committee_name: str) -> str | response.Response:
[email protected]("/ignores/<committee_name>/update", methods=["POST"])
+async def ignores_committee_update(session: route.CommitterSession,
committee_name: str) -> str | response.Response:
data = await quart.request.form
form = await UpdateIgnoreForm.create_form(data=data)
if not (await form.validate_on_submit()):
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index d3faf87..0a4bc29 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -33,7 +33,7 @@ import atr.db as db
import atr.forms as forms
import atr.log as log
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.storage as storage
import atr.storage.outcome as outcome
@@ -133,8 +133,8 @@ class UploadKeyFormBase(forms.Typed):
return True
[email protected]("/keys/add", methods=["GET", "POST"])
-async def add(session: routes.CommitterSession) -> str:
[email protected]("/keys/add", methods=["GET", "POST"])
+async def add(session: route.CommitterSession) -> str:
"""Add a new public signing key to the user's account."""
key_info = None
@@ -172,7 +172,7 @@ async def add(session: routes.CommitterSession) -> str:
form = await AddOpenPGPKeyForm.create_form()
forms.choices(form.selected_committees, committee_choices)
- except routes.FlashError as e:
+ except route.FlashError as e:
log.warning("FlashError adding OpenPGP key: %s", e)
await quart.flash(str(e), "error")
except Exception as e:
@@ -185,12 +185,12 @@ async def add(session: routes.CommitterSession) -> str:
user_committees=participant_of_committees,
form=form,
key_info=key_info,
- algorithms=routes.algorithms,
+ algorithms=route.algorithms,
)
[email protected]("/keys/delete", methods=["POST"])
-async def delete(session: routes.CommitterSession) -> response.Response:
[email protected]("/keys/delete", methods=["POST"])
+async def delete(session: route.CommitterSession) -> response.Response:
"""Delete a public signing key or SSH key from the user's account."""
form = await DeleteKeyForm.create_form(data=await quart.request.form)
@@ -221,8 +221,8 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(keys, error=f"Error deleting key:
{error}")
[email protected]("/keys/details/<fingerprint>", methods=["GET", "POST"])
-async def details(session: routes.CommitterSession, fingerprint: str) -> str |
response.Response:
[email protected]("/keys/details/<fingerprint>", methods=["GET", "POST"])
+async def details(session: route.CommitterSession, fingerprint: str) -> str |
response.Response:
"""Display details for a specific OpenPGP key."""
fingerprint = fingerprint.lower()
user_committees = []
@@ -274,14 +274,14 @@ async def details(session: routes.CommitterSession,
fingerprint: str) -> str | r
"keys-details.html",
key=key,
form=form,
- algorithms=routes.algorithms,
+ algorithms=route.algorithms,
now=datetime.datetime.now(datetime.UTC),
asf_id=session.uid,
)
[email protected]("/keys/export/<committee_name>")
-async def export(session: routes.CommitterSession, committee_name: str) ->
quart.Response:
[email protected]("/keys/export/<committee_name>")
+async def export(session: route.CommitterSession, committee_name: str) ->
quart.Response:
"""Export a KEYS file for a specific committee."""
async with storage.write() as write:
wafc = write.as_foundation_committer()
@@ -290,9 +290,9 @@ async def export(session: routes.CommitterSession,
committee_name: str) -> quart
return quart.Response(keys_file_text, mimetype="text/plain")
[email protected]("/keys/import/<project_name>/<version_name>",
methods=["POST"])
[email protected]("/keys/import/<project_name>/<version_name>",
methods=["POST"])
async def import_selected_revision(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> response.Response:
await util.validate_empty_form()
@@ -311,8 +311,8 @@ async def import_selected_revision(
)
[email protected]("/keys")
-async def keys(session: routes.CommitterSession) -> str:
[email protected]("/keys")
+async def keys(session: route.CommitterSession) -> str:
"""View all keys associated with the user's account."""
committees_to_query = list(set(session.committees + session.projects))
@@ -335,7 +335,7 @@ async def keys(session: routes.CommitterSession) -> str:
user_keys=user_keys,
user_ssh_keys=user_ssh_keys,
committees=user_committees_with_keys,
- algorithms=routes.algorithms,
+ algorithms=route.algorithms,
status_message=status_message,
status_type=status_type,
now=datetime.datetime.now(datetime.UTC),
@@ -346,8 +346,8 @@ async def keys(session: routes.CommitterSession) -> str:
)
[email protected]("/keys/ssh/add", methods=["GET", "POST"])
-async def ssh_add(session: routes.CommitterSession) -> response.Response | str:
[email protected]("/keys/ssh/add", methods=["GET", "POST"])
+async def ssh_add(session: route.CommitterSession) -> response.Response | str:
"""Add a new SSH key to the user's account."""
# TODO: Make an auth.require wrapper that gives the session automatically
# And the form if it's a POST handler? Might be hard to type
@@ -379,8 +379,8 @@ async def ssh_add(session: routes.CommitterSession) ->
response.Response | str:
)
[email protected]("/keys/update-committee-keys/<committee_name>",
methods=["POST"])
-async def update_committee_keys(session: routes.CommitterSession,
committee_name: str) -> response.Response:
[email protected]("/keys/update-committee-keys/<committee_name>",
methods=["POST"])
+async def update_committee_keys(session: route.CommitterSession,
committee_name: str) -> response.Response:
"""Generate and save the KEYS file for a specific committee."""
form = await UpdateCommitteeKeysForm.create_form()
if not await form.validate_on_submit():
@@ -399,8 +399,8 @@ async def update_committee_keys(session:
routes.CommitterSession, committee_name
return await session.redirect(keys)
[email protected]("/keys/upload", methods=["GET", "POST"])
-async def upload(session: routes.CommitterSession) -> str:
[email protected]("/keys/upload", methods=["GET", "POST"])
+async def upload(session: route.CommitterSession) -> str:
"""Upload a KEYS file containing multiple OpenPGP keys."""
async with storage.write() as write:
participant_of_committees = await write.participant_of_committees()
@@ -447,7 +447,7 @@ async def upload(session: routes.CommitterSession) -> str:
committee_map=committee_map,
form=form,
results=results,
- algorithms=routes.algorithms,
+ algorithms=route.algorithms,
submitted_committees=submitted_committees,
)
@@ -503,7 +503,7 @@ async def _get_keys_text(keys_url: str, render:
Callable[[str], Awaitable[str]])
async def _key_and_is_owner(
- data: db.Session, session: routes.CommitterSession, fingerprint: str
+ data: db.Session, session: route.CommitterSession, fingerprint: str
) -> tuple[sql.PublicSigningKey, bool]:
key = await data.public_signing_key(fingerprint=fingerprint,
_committees=True).get()
if not key:
diff --git a/atr/routes/mapping.py b/atr/routes/mapping.py
index 3f96462..c26ddcf 100644
--- a/atr/routes/mapping.py
+++ b/atr/routes/mapping.py
@@ -20,7 +20,7 @@ from collections.abc import Callable
import werkzeug.wrappers.response as response
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.routes.finish as finish
import atr.routes.release as routes_release
@@ -28,7 +28,7 @@ import atr.routes.vote as vote
import atr.util as util
-async def release_as_redirect(session: routes.CommitterSession, release:
sql.Release) -> response.Response:
+async def release_as_redirect(session: route.CommitterSession, release:
sql.Release) -> response.Response:
route = release_as_route(release)
if route is routes_release.finished:
return await session.redirect(route, project_name=release.project.name)
diff --git a/atr/routes/modules.py b/atr/routes/modules.py
deleted file mode 100644
index a63a4f6..0000000
--- a/atr/routes/modules.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# These are imported for their side effects and for access in templates
-import atr.routes.announce as announce
-import atr.routes.candidate as candidate
-import atr.routes.committees as committees
-import atr.routes.compose as compose
-import atr.routes.distribution as distribution
-import atr.routes.download as download
-import atr.routes.draft as draft
-import atr.routes.file as file
-import atr.routes.finish as finish
-import atr.routes.ignores as ignores
-import atr.routes.keys as keys
-import atr.routes.preview as preview
-import atr.routes.projects as projects
-import atr.routes.published as published
-import atr.routes.release as release
-import atr.routes.report as report
-import atr.routes.resolve as resolve
-import atr.routes.revisions as revisions
-import atr.routes.root as root
-import atr.routes.sbom as sbom
-import atr.routes.start as start
-import atr.routes.tokens as tokens
-import atr.routes.upload as upload
-import atr.routes.vote as vote
-import atr.routes.voting as voting
-
-
-# Export data for a custom linter script
-def _export_routes() -> None:
- import asyncio
-
- async def _export_routes_async() -> None:
- """Export all routes to a JSON file for static analysis."""
- import json
- import sys
-
- import aiofiles
-
- route_paths: list[str] = []
- current_module = sys.modules[__name__]
-
- for module_name in dir(current_module):
- if module_name.startswith("_"):
- # Not intended for external use
- continue
-
- module = getattr(current_module, module_name)
- if not hasattr(module, "__file__"):
- # Not a module
- continue
-
- # Get all callable interfaces that do not begin with an underscore
- for attr_name in dir(module):
- if attr_name.startswith("_"):
- # Not intended for external use
- continue
- if not callable(getattr(module, attr_name)):
- # Not callable
- continue
- route_path = f"{module_name}.{attr_name}"
- route_paths.append(route_path)
-
- async with aiofiles.open("routes.json", "w", encoding="utf-8") as f:
- await f.write(json.dumps(route_paths, indent=2))
-
- loop = asyncio.get_event_loop()
- loop.run_until_complete(_export_routes_async())
-
-
-_export_routes()
-del _export_routes
diff --git a/atr/routes/preview.py b/atr/routes/preview.py
index 927ab61..d3c832d 100644
--- a/atr/routes/preview.py
+++ b/atr/routes/preview.py
@@ -25,7 +25,7 @@ import atr.construct as construct
import atr.forms as forms
import atr.log as log
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.root as root
import atr.storage as storage
import atr.template as template
@@ -52,9 +52,9 @@ class DeleteForm(forms.Typed):
submit = forms.submit("Delete preview")
[email protected]("/preview/announce/<project_name>/<version_name>",
methods=["POST"])
[email protected]("/preview/announce/<project_name>/<version_name>",
methods=["POST"])
async def announce_preview(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> quart.wrappers.response.Response | str:
"""Generate a preview of the announcement email body."""
@@ -83,8 +83,8 @@ async def announce_preview(
return quart.Response(f"Error generating preview: {e!s}", status=500,
mimetype="text/plain")
[email protected]("/preview/delete", methods=["POST"])
-async def delete(session: routes.CommitterSession) -> response.Response:
[email protected]("/preview/delete", methods=["POST"])
+async def delete(session: route.CommitterSession) -> response.Response:
"""Delete a preview and all its associated files."""
form = await DeleteForm.create_form(data=await quart.request.form)
@@ -110,8 +110,8 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(root.index, success="Preview deleted
successfully")
[email protected]("/preview/view/<project_name>/<version_name>")
-async def view(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/preview/view/<project_name>/<version_name>")
+async def view(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""View all the files in the rsync upload directory for a release."""
await session.check_access(project_name)
@@ -141,9 +141,9 @@ async def view(session: routes.CommitterSession,
project_name: str, version_name
)
[email protected]("/preview/view/<project_name>/<version_name>/<path:file_path>")
[email protected]("/preview/view/<project_name>/<version_name>/<path:file_path>")
async def view_path(
- session: routes.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
) -> response.Response | str:
"""View the content of a specific file in the release preview."""
await session.check_access(project_name)
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 6217591..80270a7 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -34,7 +34,7 @@ import atr.log as log
import atr.models.policy as policy
import atr.models.sql as sql
import atr.registry as registry
-import atr.routes as routes
+import atr.route as route
import atr.storage as storage
import atr.template as template
import atr.user as user
@@ -229,8 +229,8 @@ class ReleasePolicyForm(forms.Typed):
return not self.errors
[email protected]("/project/add/<committee_name>", methods=["GET", "POST"])
-async def add_project(session: routes.CommitterSession, committee_name: str)
-> response.Response | str:
[email protected]("/project/add/<committee_name>", methods=["GET", "POST"])
+async def add_project(session: route.CommitterSession, committee_name: str) ->
response.Response | str:
await session.check_access_committee(committee_name)
async with db.session() as data:
@@ -254,8 +254,8 @@ You must start with your committee label, and you must use
lower case.
return await template.render("project-add-project.html", form=form,
committee_name=committee.display_name)
[email protected]("/project/delete", methods=["POST"])
-async def delete(session: routes.CommitterSession) -> response.Response:
[email protected]("/project/delete", methods=["POST"])
+async def delete(session: route.CommitterSession) -> response.Response:
"""Delete a project created by the user."""
# TODO: This is not truly empty, so make a form object for this
await util.validate_empty_form()
@@ -276,16 +276,16 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(projects, success=f"Project '{project_name}'
deleted successfully.")
[email protected]("/projects")
-async def projects(session: routes.CommitterSession | None) -> str:
[email protected]("/projects")
+async def projects(session: route.CommitterSession | None) -> str:
"""Main project directory page."""
async with db.session() as data:
projects = await
data.project(_committee=True).order_by(sql.Project.full_name).all()
return await template.render("projects.html", projects=projects,
empty_form=await forms.Empty.create_form())
[email protected]("/project/select")
-async def select(session: routes.CommitterSession) -> str:
[email protected]("/project/select")
+async def select(session: route.CommitterSession) -> str:
"""Select a project to work on."""
user_projects = []
if session.uid:
@@ -307,8 +307,8 @@ async def select(session: routes.CommitterSession) -> str:
return await template.render("project-select.html",
user_projects=user_projects)
[email protected]("/projects/<name>", methods=["GET", "POST"])
-async def view(session: routes.CommitterSession, name: str) ->
response.Response | str:
[email protected]("/projects/<name>", methods=["GET", "POST"])
+async def view(session: route.CommitterSession, name: str) ->
response.Response | str:
policy_form = None
metadata_form = None
can_edit = False
@@ -357,7 +357,7 @@ async def view(session: routes.CommitterSession, name: str)
-> response.Response
return await template.render(
"project-view.html",
project=project,
- algorithms=routes.algorithms,
+ algorithms=route.algorithms,
candidate_drafts=candidate_drafts,
candidates=candidates,
previews=previews,
diff --git a/atr/routes/published.py b/atr/routes/published.py
index a3d9ed7..175ddd7 100644
--- a/atr/routes/published.py
+++ b/atr/routes/published.py
@@ -22,12 +22,12 @@ from datetime import datetime
import aiofiles.os
import quart
-import atr.routes as routes
+import atr.route as route
import atr.util as util
[email protected]("/published/<path:path>")
-async def path(session: routes.CommitterSession, path: str) -> quart.Response:
[email protected]("/published/<path:path>")
+async def path(session: route.CommitterSession, path: str) -> quart.Response:
"""View the content of a specific file in the downloads directory."""
# This route is for debugging
# When developing locally, there is no proxy to view the downloads
directory
@@ -35,8 +35,8 @@ async def path(session: routes.CommitterSession, path: str)
-> quart.Response:
return await _path(session, path)
[email protected]("/published/")
-async def root(session: routes.CommitterSession) -> quart.Response:
[email protected]("/published/")
+async def root(session: route.CommitterSession) -> quart.Response:
return await _path(session, "")
@@ -91,7 +91,7 @@ async def _file_content(full_path: pathlib.Path) ->
quart.Response:
return await quart.send_file(full_path)
-async def _path(session: routes.CommitterSession, path: str) -> quart.Response:
+async def _path(session: route.CommitterSession, path: str) -> quart.Response:
downloads_path = util.get_downloads_dir()
full_path = downloads_path / path
if await aiofiles.os.path.isdir(full_path):
diff --git a/atr/routes/release.py b/atr/routes/release.py
index 0b90a7b..5b2cc4e 100644
--- a/atr/routes/release.py
+++ b/atr/routes/release.py
@@ -26,7 +26,7 @@ import werkzeug.wrappers.response as response
import atr.db as db
import atr.db.interaction as interaction
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.template as template
import atr.util as util
@@ -34,8 +34,8 @@ if asfquart.APP is ...:
raise RuntimeError("APP is not set")
[email protected]("/releases/finished/<project_name>")
-async def finished(session: routes.CommitterSession | None, project_name: str)
-> str:
[email protected]("/releases/finished/<project_name>")
+async def finished(session: route.CommitterSession | None, project_name: str)
-> str:
"""View all finished releases for a project."""
async with db.session() as data:
project = await data.project(name=project_name,
status=sql.ProjectStatus.ACTIVE).demand(
@@ -58,8 +58,8 @@ async def finished(session: routes.CommitterSession | None,
project_name: str) -
)
[email protected]("/releases")
-async def releases(session: routes.CommitterSession | None) -> str:
[email protected]("/releases")
+async def releases(session: route.CommitterSession | None) -> str:
"""View all releases."""
# Releases are public, so we don't need to filter by user
async with db.session() as data:
@@ -83,8 +83,8 @@ async def releases(session: routes.CommitterSession | None)
-> str:
)
[email protected]("/release/select/<project_name>")
-async def select(session: routes.CommitterSession, project_name: str) -> str:
[email protected]("/release/select/<project_name>")
+async def select(session: route.CommitterSession, project_name: str) -> str:
"""Show releases in progress for a project."""
await session.check_access(project_name)
@@ -98,10 +98,8 @@ async def select(session: routes.CommitterSession,
project_name: str) -> str:
)
[email protected]("/release/view/<project_name>/<version_name>")
-async def view(
- session: routes.CommitterSession | None, project_name: str, version_name:
str
-) -> response.Response | str:
[email protected]("/release/view/<project_name>/<version_name>")
+async def view(session: route.CommitterSession | None, project_name: str,
version_name: str) -> response.Response | str:
"""View all the files in the rsync upload directory for a release."""
async with db.session() as data:
release_name = sql.release_name(project_name, version_name)
@@ -127,9 +125,9 @@ async def view(
)
[email protected]("/release/view/<project_name>/<version_name>/<path:file_path>")
[email protected]("/release/view/<project_name>/<version_name>/<path:file_path>")
async def view_path(
- session: routes.CommitterSession | None, project_name: str, version_name:
str, file_path: str
+ session: route.CommitterSession | None, project_name: str, version_name:
str, file_path: str
) -> response.Response | str:
"""View the content of a specific file in the final release."""
async with db.session() as data:
diff --git a/atr/routes/report.py b/atr/routes/report.py
index 05b52ec..70b349e 100644
--- a/atr/routes/report.py
+++ b/atr/routes/report.py
@@ -23,14 +23,14 @@ import asfquart.base as base
import atr.forms as forms
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.storage as storage
import atr.template as template
import atr.util as util
[email protected]("/report/<project_name>/<version_name>/<path:rel_path>")
-async def selected_path(session: routes.CommitterSession, project_name: str,
version_name: str, rel_path: str) -> str:
[email protected]("/report/<project_name>/<version_name>/<path:rel_path>")
+async def selected_path(session: route.CommitterSession, project_name: str,
version_name: str, rel_path: str) -> str:
"""Show the report for a specific file."""
await session.check_access(project_name)
diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index c50d635..9a255c1 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -21,7 +21,7 @@ import werkzeug.wrappers.response as response
import atr.forms as forms
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.routes.finish as finish
import atr.routes.vote as vote
@@ -60,8 +60,8 @@ class ResolveVoteManualForm(forms.Typed):
submit = forms.submit("Resolve vote")
[email protected]("/resolve/manual/<project_name>/<version_name>")
-async def manual_selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> str:
[email protected]("/resolve/manual/<project_name>/<version_name>")
+async def manual_selected(session: route.CommitterSession, project_name: str,
version_name: str) -> str:
"""Get the manual vote resolution page."""
await session.check_access(project_name)
@@ -82,9 +82,9 @@ async def manual_selected(session: routes.CommitterSession,
project_name: str, v
)
[email protected]("/resolve/manual/<project_name>/<version_name>",
methods=["POST"])
[email protected]("/resolve/manual/<project_name>/<version_name>",
methods=["POST"])
async def manual_selected_post(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> response.Response | str:
"""Post the manual vote resolution page."""
await session.check_access(project_name)
@@ -122,9 +122,9 @@ async def manual_selected_post(
)
[email protected]("/resolve/submit/<project_name>/<version_name>",
methods=["POST"])
[email protected]("/resolve/submit/<project_name>/<version_name>",
methods=["POST"])
async def submit_selected(
- session: routes.CommitterSession, project_name: str, version_name: str
+ session: route.CommitterSession, project_name: str, version_name: str
) -> response.Response | str:
"""Resolve a vote."""
await session.check_access(project_name)
@@ -164,8 +164,8 @@ async def submit_selected(
)
[email protected]("/resolve/tabulated/<project_name>/<version_name>",
methods=["POST"])
-async def tabulated_selected_post(session: routes.CommitterSession,
project_name: str, version_name: str) -> str:
[email protected]("/resolve/tabulated/<project_name>/<version_name>",
methods=["POST"])
+async def tabulated_selected_post(session: route.CommitterSession,
project_name: str, version_name: str) -> str:
"""Tabulate votes."""
await session.check_access(project_name)
asf_uid = session.uid
diff --git a/atr/routes/revisions.py b/atr/routes/revisions.py
index c805f15..25dfc16 100644
--- a/atr/routes/revisions.py
+++ b/atr/routes/revisions.py
@@ -31,13 +31,13 @@ import atr.forms as forms
import atr.models.schema as schema
import atr.models.sql as sql
import atr.revision as revision
-import atr.routes as routes
+import atr.route as route
import atr.template as template
import atr.util as util
[email protected]("/revisions/<project_name>/<version_name>")
-async def selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> str:
[email protected]("/revisions/<project_name>/<version_name>")
+async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> str:
"""Show the revision history for a release candidate draft or release
preview."""
await session.check_access(project_name)
@@ -92,8 +92,8 @@ async def selected(session: routes.CommitterSession,
project_name: str, version_
)
[email protected]("/revisions/<project_name>/<version_name>", methods=["POST"])
-async def selected_post(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response:
[email protected]("/revisions/<project_name>/<version_name>", methods=["POST"])
+async def selected_post(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
"""Set a specific revision as the latest for a candidate draft or release
preview."""
await session.check_access(project_name)
diff --git a/atr/routes/root.py b/atr/routes/root.py
index 60f160e..6cdc4af 100644
--- a/atr/routes/root.py
+++ b/atr/routes/root.py
@@ -27,7 +27,7 @@ import sqlmodel
import atr.db as db
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.template as template
import atr.user as user
import atr.util as util
@@ -64,14 +64,14 @@ _POLICIES: Final = htpy.div[
]
[email protected]("/about")
-async def about(session: routes.CommitterSession) -> str:
[email protected]("/about")
+async def about(session: route.CommitterSession) -> str:
"""About page."""
return await template.render("about.html")
[email protected]("/")
-async def index(session: routes.CommitterSession | None) -> response.Response
| str:
[email protected]("/")
+async def index(session: route.CommitterSession | None) -> response.Response |
str:
"""Show public info or an entry portal for participants."""
session_data = await asfquart.session.read()
if session_data:
@@ -145,18 +145,18 @@ async def index(session: routes.CommitterSession | None)
-> response.Response |
return await template.render("index-public.html")
[email protected]("/policies")
-async def policies(session: routes.CommitterSession | None) -> str:
[email protected]("/policies")
+async def policies(session: route.CommitterSession | None) -> str:
return await template.blank("Policies", content=_POLICIES)
[email protected]("/todo", methods=["POST"])
-async def todo(session: routes.CommitterSession) -> str:
[email protected]("/todo", methods=["POST"])
+async def todo(session: route.CommitterSession) -> str:
"""POST target for development."""
return await template.render("todo.html")
[email protected]("/tutorial")
-async def tutorial(session: routes.CommitterSession) -> str:
[email protected]("/tutorial")
+async def tutorial(session: route.CommitterSession) -> str:
"""Tutorial page."""
return await template.render("tutorial.html")
diff --git a/atr/routes/sbom.py b/atr/routes/sbom.py
index a0f6531..4ac1a73 100644
--- a/atr/routes/sbom.py
+++ b/atr/routes/sbom.py
@@ -32,7 +32,7 @@ import atr.htm as htm
import atr.log as log
import atr.models.results as results
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.sbomtool as sbomtool
import atr.storage as storage
import atr.template as template
@@ -42,9 +42,9 @@ if TYPE_CHECKING:
import werkzeug.wrappers.response as response
[email protected]("/sbom/augment/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
[email protected]("/sbom/augment/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
async def augment(
- session: routes.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
) -> response.Response:
"""Augment a CycloneDX SBOM file."""
await session.check_access(project_name)
@@ -87,8 +87,8 @@ async def augment(
)
[email protected]("/sbom/report/<project>/<version>/<path:file_path>")
-async def report(session: routes.CommitterSession, project: str, version: str,
file_path: str) -> str:
[email protected]("/sbom/report/<project>/<version>/<path:file_path>")
+async def report(session: route.CommitterSession, project: str, version: str,
file_path: str) -> str:
await session.check_access(project)
await session.release(project, version)
async with db.session() as data:
diff --git a/atr/routes/start.py b/atr/routes/start.py
index a794a14..d7b146a 100644
--- a/atr/routes/start.py
+++ b/atr/routes/start.py
@@ -24,7 +24,7 @@ import atr.db as db
import atr.db.interaction as interaction
import atr.forms as forms
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.storage as storage
import atr.template as template
@@ -40,8 +40,8 @@ class StartReleaseForm(forms.Typed):
submit = forms.submit("Start new release")
[email protected]("/start/<project_name>", methods=["GET", "POST"])
-async def selected(session: routes.CommitterSession, project_name: str) ->
response.Response | str:
[email protected]("/start/<project_name>", methods=["GET", "POST"])
+async def selected(session: route.CommitterSession, project_name: str) ->
response.Response | str:
"""Allow the user to start a new release draft, or handle its
submission."""
await session.check_access(project_name)
@@ -71,7 +71,7 @@ async def selected(session: routes.CommitterSession,
project_name: str) -> respo
version_name=new_release.version,
success="Release candidate draft created successfully",
)
- except (routes.FlashError, base.ASFQuartException) as e:
+ except (route.FlashError, base.ASFQuartException) as e:
# Flash the error and let the code fall through to render the
template below
await quart.flash(str(e), "error")
@@ -79,4 +79,4 @@ async def selected(session: routes.CommitterSession,
project_name: str) -> respo
releases = await interaction.all_releases(project)
# Render the template for GET requests or POST requests with validation
errors
- return await template.render("start-selected.html", project=project,
form=form, routes=routes, releases=releases)
+ return await template.render("start-selected.html", project=project,
form=form, releases=releases)
diff --git a/atr/routes/tokens.py b/atr/routes/tokens.py
index 1653fda..3926bd9 100644
--- a/atr/routes/tokens.py
+++ b/atr/routes/tokens.py
@@ -51,7 +51,7 @@ import atr.forms as forms
import atr.jwtoken as jwtoken
import atr.log as log
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.storage as storage
import atr.template as templates
import atr.util as util
@@ -76,16 +76,16 @@ class IssueJWTForm(forms.Typed):
submit = forms.submit("Generate JWT")
[email protected]("/tokens/jwt", methods=["POST"])
-async def jwt_post(session: routes.CommitterSession) -> quart.Response:
[email protected]("/tokens/jwt", methods=["POST"])
+async def jwt_post(session: route.CommitterSession) -> quart.Response:
await util.validate_empty_form()
jwt_token = jwtoken.issue(session.uid)
return quart.Response(jwt_token, mimetype="text/plain")
[email protected]("/tokens", methods=["GET", "POST"])
-async def tokens(session: routes.CommitterSession) -> str | response.Response:
[email protected]("/tokens", methods=["GET", "POST"])
+async def tokens(session: route.CommitterSession) -> str | response.Response:
request_form = await quart.request.form
if is_post := quart.request.method == "POST":
@@ -265,7 +265,7 @@ async def _fetch_tokens(data: db.Session, uid: str) ->
list[sql.PersonalAccessTo
async def _handle_post(
- session: routes.CommitterSession, request_form: datastructures.MultiDict
+ session: route.CommitterSession, request_form: datastructures.MultiDict
) -> response.Response | None:
if "token_id" in request_form:
return await _handle_delete_token_post(session, request_form)
@@ -277,7 +277,7 @@ async def _handle_post(
async def _handle_add_token_post(
- session: routes.CommitterSession, request_form: datastructures.MultiDict
+ session: route.CommitterSession, request_form: datastructures.MultiDict
) -> response.Response | None:
add_form = await AddTokenForm.create_form(data=request_form)
if await add_form.validate_on_submit():
@@ -298,7 +298,7 @@ async def _handle_add_token_post(
async def _handle_delete_token_post(
- session: routes.CommitterSession, request_form: datastructures.MultiDict
+ session: route.CommitterSession, request_form: datastructures.MultiDict
) -> response.Response | None:
del_form = await DeleteTokenForm.create_form(data=request_form)
if await del_form.validate_on_submit():
@@ -312,7 +312,7 @@ async def _handle_delete_token_post(
async def _handle_issue_jwt_post(
- session: routes.CommitterSession, request_form: datastructures.MultiDict
+ session: route.CommitterSession, request_form: datastructures.MultiDict
) -> response.Response | None:
issue_form = await IssueJWTForm.create_form(data=request_form)
if await issue_form.validate_on_submit():
diff --git a/atr/routes/upload.py b/atr/routes/upload.py
index 21ad2d3..d2010bf 100644
--- a/atr/routes/upload.py
+++ b/atr/routes/upload.py
@@ -24,7 +24,7 @@ import wtforms
import atr.db as db
import atr.forms as forms
import atr.log as log
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.storage as storage
import atr.template as template
@@ -61,8 +61,8 @@ class SvnImportForm(forms.Typed):
submit = forms.submit("Queue SVN import task")
[email protected]("/upload/<project_name>/<version_name>", methods=["GET",
"POST"])
-async def selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/upload/<project_name>/<version_name>", methods=["GET",
"POST"])
+async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""Show a page to allow the user to add files to a candidate draft."""
await session.check_access(project_name)
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index b9d685a..ace6331 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -25,7 +25,7 @@ import atr.forms as forms
import atr.log as log
import atr.models.results as results
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.routes.mapping as mapping
import atr.storage as storage
@@ -40,8 +40,8 @@ class CastVoteForm(forms.Typed):
submit = forms.submit("Submit vote")
[email protected]("/vote/<project_name>/<version_name>")
-async def selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/vote/<project_name>/<version_name>")
+async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
"""Show the contents of the release candidate draft."""
await session.check_access(project_name)
@@ -115,8 +115,8 @@ async def selected(session: routes.CommitterSession,
project_name: str, version_
)
[email protected]("/vote/<project_name>/<version_name>", methods=["POST"])
-async def selected_post(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response:
[email protected]("/vote/<project_name>/<version_name>", methods=["POST"])
+async def selected_post(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
"""Handle submission of a vote."""
await session.check_access(project_name)
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index 51f12d3..4fe4787 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -28,7 +28,7 @@ import atr.db.interaction as interaction
import atr.forms as forms
import atr.log as log
import atr.models.sql as sql
-import atr.routes as routes
+import atr.route as route
import atr.routes.compose as compose
import atr.routes.root as root
import atr.routes.vote as vote
@@ -55,9 +55,9 @@ class VoteInitiateForm(forms.Typed):
submit = forms.submit("Send vote email")
[email protected]("/voting/<project_name>/<version_name>/<revision>",
methods=["GET", "POST"])
[email protected]("/voting/<project_name>/<version_name>/<revision>",
methods=["GET", "POST"])
async def selected_revision(
- session: routes.CommitterSession, project_name: str, version_name: str,
revision: str
+ session: route.CommitterSession, project_name: str, version_name: str,
revision: str
) -> response.Response | str:
"""Show the vote initiation form for a release."""
await session.check_access(project_name)
@@ -114,7 +114,7 @@ async def selected_revision(
async def start_vote_manual(
release: sql.Release,
selected_revision_number: str,
- session: routes.CommitterSession,
+ session: route.CommitterSession,
data: db.Session,
) -> response.Response | str:
# This verifies the state and sets the phase to RELEASE_CANDIDATE
@@ -193,7 +193,7 @@ async def _selected_revision_data(
version_name: str,
revision: str,
data: db.Session,
- session: routes.CommitterSession,
+ session: route.CommitterSession,
) -> response.Response | str | VoteInitiateForm:
committee = release.committee
if committee is None:
diff --git a/atr/server.py b/atr/server.py
index b8e3727..a46320e 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -125,8 +125,8 @@ def app_setup_context(app: base.QuartApp) -> None:
@app.context_processor
async def app_wide() -> dict[str, Any]:
import atr.metadata as metadata
+ import atr.routes as routes
import atr.routes.mapping as mapping
- import atr.routes.modules as modules
return {
"as_url": util.as_url,
@@ -135,7 +135,7 @@ def app_setup_context(app: base.QuartApp) -> None:
"is_admin_fn": user.is_admin,
"is_viewing_as_admin_fn": util.is_user_viewing_as_admin,
"is_committee_member_fn": user.is_committee_member,
- "routes": modules,
+ "routes": routes,
"unfinished_releases_fn": interaction.unfinished_releases,
# "user_committees_fn": interaction.user_committees,
"user_projects_fn": interaction.user_projects,
@@ -313,7 +313,7 @@ def main() -> None:
def register_routes(app: base.QuartApp) -> ModuleType:
# NOTE: These imports are for their side effects only
- import atr.routes.modules as modules
+ import atr.routes as routes
# Add a global error handler to show helpful error messages with tracebacks
@app.errorhandler(Exception)
@@ -350,7 +350,7 @@ def register_routes(app: base.QuartApp) -> ModuleType:
return quart.jsonify({"error": "404 Not Found"}), 404
return await template.render("notfound.html", error="404 Not Found",
traceback="", status_code=404), 404
- return modules
+ return routes
# FIXME: when running in SSL mode, you will receive these exceptions upon
termination at times:
diff --git a/pyproject.toml b/pyproject.toml
index b975f46..6f08b1f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -133,6 +133,5 @@ select = [
"atr/db/__init__.py" = ["C901"]
"atr/models/cyclonedx/__init__.py" = ["N815"]
"atr/models/cyclonedx/spdx.py" = ["N815"]
-"atr/routes/modules.py" = ["F401"]
"atr/sbomtool.py" = ["C901"]
"migrations/env.py" = ["E402"]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]