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 e4a07ad Move user and voting routes, and update the documentation
e4a07ad is described below
commit e4a07adf8cfab1bca4a54d9dc9f73f6dcffda996
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Oct 28 16:40:44 2025 +0000
Move user and voting routes, and update the documentation
---
atr/docs/overview-of-the-code.html | 4 +-
atr/docs/overview-of-the-code.md | 4 +-
atr/docs/storage-interface.html | 4 +-
atr/docs/storage-interface.md | 4 +-
atr/docs/user-interface.html | 10 ++--
atr/docs/user-interface.md | 10 ++--
atr/get/__init__.py | 4 ++
atr/{routes => get}/user.py | 77 +++-----------------------
atr/get/voting.py | 30 ++++++++++
atr/post/__init__.py | 4 ++
atr/post/user.py | 76 +++++++++++++++++++++++++
atr/post/voting.py | 31 +++++++++++
atr/routes/__init__.py | 70 -----------------------
atr/server.py | 10 +---
atr/shared/__init__.py | 4 ++
atr/shared/user.py | 27 +++++++++
atr/{routes => shared}/voting.py | 11 ++--
atr/templates/check-selected-release-info.html | 2 +-
atr/templates/voting-selected-revision.html | 4 +-
19 files changed, 210 insertions(+), 176 deletions(-)
diff --git a/atr/docs/overview-of-the-code.html
b/atr/docs/overview-of-the-code.html
index 2dfb08a..615f22d 100644
--- a/atr/docs/overview-of-the-code.html
+++ b/atr/docs/overview-of-the-code.html
@@ -20,11 +20,11 @@
</code></pre>
<p>The <a
href="/ref/atr/server.py:create_app"><code>server.create_app</code></a>
function performs a lot of setup, and if you're interested in how the server
works then you can read it and the functions it calls to understand the process
further. In general, however, when developing ATR we do not make modifications
at the ASFQuart, Quart, and Hypercorn levels very often.</p>
<h2 id="routes-and-database">Routes and database</h2>
-<p>Users request ATR pages over HTTPS, and the ATR server processes those
requests in route handlers. Most of those handlers are in <a
href="/ref/atr/routes/"><code>routes</code></a>, but not all. What each handler
does varies, of course, from handler to handler, but most perform at least one
access to the ATR SQLite database.</p>
+<p>Users request ATR pages over HTTPS, and the ATR server processes those
requests in route handlers. Most of those handlers are in <a
href="/ref/atr/get/"><code>get</code></a> and <a
href="/ref/atr/post/"><code>post</code></a>, with some helper code in <a
href="/ref/atr/shared/"><code>shared</code></a>. What each handler does varies,
of course, from handler to handler, but most perform at least one access to the
ATR SQLite database.</p>
<p>The path of the SQLite database is configured in <a
href="/ref/atr/config.py:SQLITE_DB_PATH"><code>config.AppConfig.SQLITE_DB_PATH</code></a>
by default, and will usually appear as <code>state/atr.db</code> with related
<code>shm</code> and <code>wal</code> files. We do not expect ATR to have so
many users that we need to scale beyond SQLite.</p>
<p>We use <a href="https://sqlmodel.tiangolo.com/">SQLModel</a>, an ORM
utilising <a href="https://docs.pydantic.dev/latest/">Pydantic</a> and <a
href="https://www.sqlalchemy.org/">SQLAlchemy</a>, to create Python models for
the ATR database. The core models file is <a
href="/ref/atr/models/sql.py"><code>models.sql</code></a>. The most important
individual SQLite models in this module are <a
href="/ref/atr/models/sql.py:Committee"><code>Committee</code></a>, <a
href="/ref/atr/models/sql. [...]
<p>It is technically possible to interact with SQLite directly, but we do not
do that in the ATR source. We use various interfaces in <a
href="/ref/atr/db/__init__.py"><code>db</code></a> for reads, and interfaces in
<a href="/ref/atr/storage/"><code>storage</code></a> for writes. We plan to
move the <code>db</code> code into <code>storage</code> too eventually, because
<code>storage</code> is designed to have read components and write components.
There is also a legacy <a href="/ref/atr [...]
-<p>These three interfaces, <a href="/ref/atr/routes/"><code>routes</code></a>,
<a href="/ref/atr/models/sql.py"><code>models.sql</code></a>, and <a
href="/ref/atr/storage/"><code>storage</code></a>, are where the majority of
activity happens when developing ATR.</p>
+<p>These interfaces, including route handlers in <a
href="/ref/atr/get/"><code>get</code></a>, <a
href="/ref/atr/post/"><code>post</code></a>, and <a
href="/ref/atr/shared/"><code>shared</code></a>, along with <a
href="/ref/atr/models/sql.py"><code>models.sql</code></a> and <a
href="/ref/atr/storage/"><code>storage</code></a>, are where the majority of
activity happens when developing ATR.</p>
<h2 id="user-interface">User interface</h2>
<p>ATR provides a web interface for users to interact with the platform, and
the implementation of that interface is split across several modules. The web
interface uses server-side rendering almost entirely, where HTML is generated
on the server and sent to the browser.</p>
<p>The template system in ATR is <a
href="https://jinja.palletsprojects.com/">Jinja2</a>, always accessed through
the ATR <a href="/ref/atr/template.py"><code>template</code></a> module.
Template files in Jinja2 syntax are stored in <a
href="/ref/atr/templates/"><code>templates/</code></a>, and route handlers
render them using the asynchronous <a
href="/ref/atr/template.py:render"><code>template.render</code></a>
function.</p>
diff --git a/atr/docs/overview-of-the-code.md b/atr/docs/overview-of-the-code.md
index b8b9718..c5d568e 100644
--- a/atr/docs/overview-of-the-code.md
+++ b/atr/docs/overview-of-the-code.md
@@ -32,7 +32,7 @@ The [`server.create_app`](/ref/atr/server.py:create_app)
function performs a lot
## Routes and database
-Users request ATR pages over HTTPS, and the ATR server processes those
requests in route handlers. Most of those handlers are in
[`routes`](/ref/atr/routes/), but not all. What each handler does varies, of
course, from handler to handler, but most perform at least one access to the
ATR SQLite database.
+Users request ATR pages over HTTPS, and the ATR server processes those
requests in route handlers. Most of those handlers are in
[`get`](/ref/atr/get/) and [`post`](/ref/atr/post/), with some helper code in
[`shared`](/ref/atr/shared/). What each handler does varies, of course, from
handler to handler, but most perform at least one access to the ATR SQLite
database.
The path of the SQLite database is configured in
[`config.AppConfig.SQLITE_DB_PATH`](/ref/atr/config.py:SQLITE_DB_PATH) by
default, and will usually appear as `state/atr.db` with related `shm` and `wal`
files. We do not expect ATR to have so many users that we need to scale beyond
SQLite.
@@ -40,7 +40,7 @@ We use [SQLModel](https://sqlmodel.tiangolo.com/), an ORM
utilising [Pydantic](h
It is technically possible to interact with SQLite directly, but we do not do
that in the ATR source. We use various interfaces in
[`db`](/ref/atr/db/__init__.py) for reads, and interfaces in
[`storage`](/ref/atr/storage/) for writes. We plan to move the `db` code into
`storage` too eventually, because `storage` is designed to have read components
and write components. There is also a legacy
[`db.interaction`](/ref/atr/db/interaction.py) module which we plan to migrate
into `storage`.
-These three interfaces, [`routes`](/ref/atr/routes/),
[`models.sql`](/ref/atr/models/sql.py), and [`storage`](/ref/atr/storage/), are
where the majority of activity happens when developing ATR.
+These interfaces, including route handlers in [`get`](/ref/atr/get/),
[`post`](/ref/atr/post/), and [`shared`](/ref/atr/shared/), along with
[`models.sql`](/ref/atr/models/sql.py) and [`storage`](/ref/atr/storage/), are
where the majority of activity happens when developing ATR.
## User interface
diff --git a/atr/docs/storage-interface.html b/atr/docs/storage-interface.html
index 539ccd5..184e070 100644
--- a/atr/docs/storage-interface.html
+++ b/atr/docs/storage-interface.html
@@ -18,7 +18,7 @@
<h2 id="how-do-we-read-from-storage">How do we read from storage?</h2>
<p>Reading from storage is a work in progress. There are some existing
methods, but most of the functionality is currently in <code>db</code> or
<code>db.interaction</code>, and much work is required to migrate this to the
storage interface. We have given this less priority because reads are generally
safe, with the exception of a few components such as user tokens, which should
be given greater migration priority.</p>
<h2 id="how-do-we-write-to-storage">How do we write to storage?</h2>
-<p>To write to storage we open a write session, request specific permissions,
use the exposed functionality, and then handle the outcome. Here is an actual
example from <a
href="/ref/atr/routes/start.py"><code>routes/start.py</code></a>:</p>
+<p>To write to storage we open a write session, request specific permissions,
use the exposed functionality, and then handle the outcome. Here is an actual
example from <a
href="/ref/atr/post/start.py"><code>post/start.py</code></a>:</p>
<pre><code class="language-python">async with storage.write(session) as write:
wacp = await write.as_project_committee_participant(project_name)
new_release, _project = await wacp.release.start(project_name, version)
@@ -84,7 +84,7 @@ class CommitteeMember(CommitteeParticipant):
<h2 id="how-do-we-use-outcomes">How do we use outcomes?</h2>
<p>Consider using <strong>outcome types</strong> from <a
href="/ref/atr/storage/outcome.py"><code>storage.outcome</code></a> when
returning results from writer methods. Outcomes let you represent both success
and failure without raising exceptions, which gives callers flexibility in how
they handle errors.</p>
<p>An <a
href="/ref/atr/storage/outcome.py:Outcome"><code>Outcome[T]</code></a> is
either a <a
href="/ref/atr/storage/outcome.py:Result"><code>Result[T]</code></a> wrapping a
successful value, or an <a
href="/ref/atr/storage/outcome.py:Error"><code>Error[T]</code></a> wrapping an
exception. You can check which it is with the <code>ok</code> property or
pattern matching, extract the value with <code>result_or_raise()</code>, or
extract the error with <code>error_or_raise()</code>.</p>
-<p>Here is an example from <a
href="/ref/atr/routes/keys.py"><code>routes/keys.py</code></a> that processes
multiple keys and collects outcomes:</p>
+<p>Here is an example from <a
href="/ref/atr/post/keys.py"><code>post/keys.py</code></a> that processes
multiple keys and collects outcomes:</p>
<pre><code class="language-python">async with storage.write() as write:
wacm = write.as_committee_member(selected_committee)
outcomes = await wacm.keys.ensure_associated(keys_text)
diff --git a/atr/docs/storage-interface.md b/atr/docs/storage-interface.md
index 675b876..97b508e 100644
--- a/atr/docs/storage-interface.md
+++ b/atr/docs/storage-interface.md
@@ -29,7 +29,7 @@ Reading from storage is a work in progress. There are some
existing methods, but
## How do we write to storage?
-To write to storage we open a write session, request specific permissions, use
the exposed functionality, and then handle the outcome. Here is an actual
example from [`routes/start.py`](/ref/atr/routes/start.py):
+To write to storage we open a write session, request specific permissions, use
the exposed functionality, and then handle the outcome. Here is an actual
example from [`post/start.py`](/ref/atr/post/start.py):
```python
async with storage.write(session) as write:
@@ -116,7 +116,7 @@ Consider using **outcome types** from
[`storage.outcome`](/ref/atr/storage/outco
An [`Outcome[T]`](/ref/atr/storage/outcome.py:Outcome) is either a
[`Result[T]`](/ref/atr/storage/outcome.py:Result) wrapping a successful value,
or an [`Error[T]`](/ref/atr/storage/outcome.py:Error) wrapping an exception.
You can check which it is with the `ok` property or pattern matching, extract
the value with `result_or_raise()`, or extract the error with
`error_or_raise()`.
-Here is an example from [`routes/keys.py`](/ref/atr/routes/keys.py) that
processes multiple keys and collects outcomes:
+Here is an example from [`post/keys.py`](/ref/atr/post/keys.py) that processes
multiple keys and collects outcomes:
```python
async with storage.write() as write:
diff --git a/atr/docs/user-interface.html b/atr/docs/user-interface.html
index 3c9a0f0..ceb2577 100644
--- a/atr/docs/user-interface.html
+++ b/atr/docs/user-interface.html
@@ -16,7 +16,7 @@
<p>The UI is built from three main pieces: <a
href="https://jinja.palletsprojects.com/">Jinja2</a> for templates, <a
href="https://wtforms.readthedocs.io/">WTForms</a> for HTML forms, and <a
href="https://htpy.dev/">htpy</a> for programmatic HTML generation. We style
everything with <a href="https://getbootstrap.com/">Bootstrap</a>, which we
customize slightly.</p>
<h2 id="jinja2-templates">Jinja2 templates</h2>
<p>Templates live in <a
href="/ref/atr/templates/"><code>templates/</code></a>. Each template is a
Jinja2 file that defines HTML structure with placeholders for dynamic content.
Route handlers render templates by calling <a
href="/ref/atr/template.py:render"><code>template.render</code></a>, which is
an alias for <a
href="/ref/atr/template.py:render_sync"><code>template.render_sync</code></a>.
The function is asynchronous and takes a template name plus keyword arguments
for the template [...]
-<p>Here is an example from <a
href="/ref/atr/routes/keys.py:add"><code>routes/keys.py</code></a>:</p>
+<p>Here is an example from <a
href="/ref/atr/get/keys.py:add"><code>get/keys.py</code></a>:</p>
<pre><code class="language-python">return await template.render(
"keys-add.html",
asf_id=session.uid,
@@ -31,7 +31,7 @@
<p>Template rendering happens in a thread pool to avoid blocking the async
event loop. The function <a
href="/ref/atr/template.py:_render_in_thread"><code>_render_in_thread</code></a>
uses <code>asyncio.to_thread</code> to execute Jinja2's synchronous
<code>render</code> method.</p>
<h2 id="forms">Forms</h2>
<p>HTML forms in ATR are handled by <a
href="https://wtforms.readthedocs.io/">WTForms</a>, accessed through our <a
href="/ref/atr/forms.py"><code>forms</code></a> module. Each form is a class
that inherits from <a
href="/ref/atr/forms.py:Typed"><code>forms.Typed</code></a>, which itself
inherits from <code>QuartForm</code> in <a
href="https://quart-wtf.readthedocs.io/">Quart-WTF</a>. Form fields are class
attributes created using helper functions from the <code>forms</code>
module.</p>
-<p>Here is a typical form definition from <a
href="/ref/atr/routes/keys.py:AddOpenPGPKeyForm"><code>routes/keys.py</code></a>:</p>
+<p>Here is a typical form definition from <a
href="/ref/atr/shared/keys.py:AddOpenPGPKeyForm"><code>shared/keys.py</code></a>:</p>
<pre><code class="language-python">class AddOpenPGPKeyForm(forms.Typed):
public_key = forms.textarea(
"Public OpenPGP key",
@@ -74,9 +74,9 @@ return div.collect()
<p>The block class provides properties for common HTML elements like
<code>h1</code>, <code>h2</code>, <code>p</code>, <code>div</code>,
<code>ul</code>, and so on. When you access these properties, you get back a <a
href="/ref/atr/htm.py:BlockElementCallable"><code>BlockElementCallable</code></a>,
which you can call to create an element with attributes or use square brackets
to add grandchildren of the block. The element is automatically appended to the
block's internal list of children.</p>
<p>The <code>collect</code> method assembles all of the elements into a single
htpy element. If you created the block with an outer element like
<code>htm.Block(htpy.div(".container"))</code>, that element wraps all the
children. If you created the block with no outer element, <code>collect</code>
wraps everything in a div. You can also pass a <code>separator</code> argument
to <code>collect</code>, which inserts a text separator between elements.</p>
<p>The block class is useful when you are building HTML in a loop or when you
have conditional elements. Instead of managing a list of elements manually, you
can let the block class do it for you: append elements as you go, and at the
end call <code>collect</code> to get the final result. This is cleaner than
concatenating strings or maintaining lists yourself.</p>
-<p>The block class also adds a <code>data-src</code> attribute to elements,
which records which function created the element. If you see an element in the
browser inspector with <code>data-src="atr.routes.keys:selected"</code>, you
know that it came from the <code>selected</code> function in
<code>routes/keys.py</code>. The source is extracted automatically using <a
href="/ref/atr/log.py:caller_name"><code>log.caller_name</code></a>.</p>
+<p>The block class also adds a <code>data-src</code> attribute to elements,
which records which function created the element. If you see an element in the
browser inspector with <code>data-src="atr.get.keys:keys"</code>, you know that
it came from the <code>keys</code> function in <code>get/keys.py</code>. The
source is extracted automatically using <a
href="/ref/atr/log.py:caller_name"><code>log.caller_name</code></a>.</p>
<h2 id="how-a-route-renders-ui">How a route renders UI</h2>
-<p>A typical route that renders UI first authenticates the user, loads data
from the database, creates and validates a form if necessary, and renders a
template with the data and form. Here is a simplified example from <a
href="/ref/atr/routes/keys.py:add"><code>routes/keys.py</code></a>:</p>
+<p>A typical route that renders UI first authenticates the user, loads data
from the database, creates and validates a form if necessary, and renders a
template with the data and form. Here is a simplified example from <a
href="/ref/atr/get/keys.py:add"><code>get/keys.py</code></a>:</p>
<pre><code class="language-python">@route.committer("/keys/add",
methods=["GET", "POST"])
async def add(session: route.CommitterSession) -> str:
async with storage.write() as write:
@@ -107,6 +107,6 @@ async def add(session: route.CommitterSession) -> str:
)
</code></pre>
<p>The route is decorated with <code>@route.committer</code>, which ensures
that the route fails before the function is even entered if authentication
fails. The function receives a <code>session</code> object, which is an
instance of <a
href="/ref/atr/route.py:CommitterSession"><code>route.CommitterSession</code></a>
with a range of useful properties and methods. The function then loads data,
creates a form, checks if the request is a POST, and either processes the form
or displays it. [...]
-<p>The template receives the form object and renders it by passing it to one
of the <code>forms.render_*</code> functions. We previously used Jinja2 macros
for this, but are migrating to the new rendering functions in Python (e.g. in
<a
href="/ref/atr/routes/distribution.py"><code>routes/distribution.py</code></a>
and <a href="/ref/atr/routes/ignores.py"><code>routes/ignores.py</code></a>).
The template also receives other data like <code>asf_id</code> and
<code>user_committees</code>, w [...]
+<p>The template receives the form object and renders it by passing it to one
of the <code>forms.render_*</code> functions. We previously used Jinja2 macros
for this, but are migrating to the new rendering functions in Python (e.g. in
<a href="/ref/atr/get/distribution.py"><code>get/distribution.py</code></a> and
<a href="/ref/atr/get/ignores.py"><code>get/ignores.py</code></a>). The
template also receives other data like <code>asf_id</code> and
<code>user_committees</code>, which it uses [...]
<p>If you use the programmatic rendering functions from <a
href="/ref/atr/forms.py"><code>forms</code></a>, you can skip the template
entirely. These functions return htpy elements, which you can combine with
other htpy elements and return directly from the route, which is often useful
for admin routes, for example. You can also use <a
href="/ref/atr/template.py:blank"><code>template.blank</code></a>, which
renders a minimal template with just a title and content area. This is useful
for [...]
<p>Bootstrap CSS classes are applied automatically by the form rendering
functions. The functions use classes like <code>form-control</code>,
<code>form-select</code>, <code>btn-primary</code>, <code>is-invalid</code>,
and <code>invalid-feedback</code>. We currently use Bootstrap 5. If you
generate HTML manually with htpy, you can apply Bootstrap classes yourself by
using the CSS selector syntax like <code>htpy.div(".container")</code> or the
class attribute like <code>htpy.div(class_="c [...]
diff --git a/atr/docs/user-interface.md b/atr/docs/user-interface.md
index 82815ad..d6e1aec 100644
--- a/atr/docs/user-interface.md
+++ b/atr/docs/user-interface.md
@@ -25,7 +25,7 @@ The UI is built from three main pieces:
[Jinja2](https://jinja.palletsprojects.c
Templates live in [`templates/`](/ref/atr/templates/). Each template is a
Jinja2 file that defines HTML structure with placeholders for dynamic content.
Route handlers render templates by calling
[`template.render`](/ref/atr/template.py:render), which is an alias for
[`template.render_sync`](/ref/atr/template.py:render_sync). The function is
asynchronous and takes a template name plus keyword arguments for the template
variables.
-Here is an example from [`routes/keys.py`](/ref/atr/routes/keys.py:add):
+Here is an example from [`get/keys.py`](/ref/atr/get/keys.py:add):
```python
return await template.render(
@@ -48,7 +48,7 @@ Template rendering happens in a thread pool to avoid blocking
the async event lo
HTML forms in ATR are handled by [WTForms](https://wtforms.readthedocs.io/),
accessed through our [`forms`](/ref/atr/forms.py) module. Each form is a class
that inherits from [`forms.Typed`](/ref/atr/forms.py:Typed), which itself
inherits from `QuartForm` in [Quart-WTF](https://quart-wtf.readthedocs.io/).
Form fields are class attributes created using helper functions from the
`forms` module.
-Here is a typical form definition from
[`routes/keys.py`](/ref/atr/routes/keys.py:AddOpenPGPKeyForm):
+Here is a typical form definition from
[`shared/keys.py`](/ref/atr/shared/keys.py:AddOpenPGPKeyForm):
```python
class AddOpenPGPKeyForm(forms.Typed):
@@ -111,11 +111,11 @@ The `collect` method assembles all of the elements into a
single htpy element. I
The block class is useful when you are building HTML in a loop or when you
have conditional elements. Instead of managing a list of elements manually, you
can let the block class do it for you: append elements as you go, and at the
end call `collect` to get the final result. This is cleaner than concatenating
strings or maintaining lists yourself.
-The block class also adds a `data-src` attribute to elements, which records
which function created the element. If you see an element in the browser
inspector with `data-src="atr.routes.keys:selected"`, you know that it came
from the `selected` function in `routes/keys.py`. The source is extracted
automatically using [`log.caller_name`](/ref/atr/log.py:caller_name).
+The block class also adds a `data-src` attribute to elements, which records
which function created the element. If you see an element in the browser
inspector with `data-src="atr.get.keys:keys"`, you know that it came from the
`keys` function in `get/keys.py`. The source is extracted automatically using
[`log.caller_name`](/ref/atr/log.py:caller_name).
## How a route renders UI
-A typical route that renders UI first authenticates the user, loads data from
the database, creates and validates a form if necessary, and renders a template
with the data and form. Here is a simplified example from
[`routes/keys.py`](/ref/atr/routes/keys.py:add):
+A typical route that renders UI first authenticates the user, loads data from
the database, creates and validates a form if necessary, and renders a template
with the data and form. Here is a simplified example from
[`get/keys.py`](/ref/atr/get/keys.py:add):
```python
@route.committer("/keys/add", methods=["GET", "POST"])
@@ -150,7 +150,7 @@ async def add(session: route.CommitterSession) -> str:
The route is decorated with `@route.committer`, which ensures that the route
fails before the function is even entered if authentication fails. The function
receives a `session` object, which is an instance of
[`route.CommitterSession`](/ref/atr/route.py:CommitterSession) with a range of
useful properties and methods. The function then loads data, creates a form,
checks if the request is a POST, and either processes the form or displays it.
After successful processing, it creates a fresh [...]
-The template receives the form object and renders it by passing it to one of
the `forms.render_*` functions. We previously used Jinja2 macros for this, but
are migrating to the new rendering functions in Python (e.g. in
[`routes/distribution.py`](/ref/atr/routes/distribution.py) and
[`routes/ignores.py`](/ref/atr/routes/ignores.py)). The template also receives
other data like `asf_id` and `user_committees`, which it uses to display
information or make decisions about what to show.
+The template receives the form object and renders it by passing it to one of
the `forms.render_*` functions. We previously used Jinja2 macros for this, but
are migrating to the new rendering functions in Python (e.g. in
[`get/distribution.py`](/ref/atr/get/distribution.py) and
[`get/ignores.py`](/ref/atr/get/ignores.py)). The template also receives other
data like `asf_id` and `user_committees`, which it uses to display information
or make decisions about what to show.
If you use the programmatic rendering functions from
[`forms`](/ref/atr/forms.py), you can skip the template entirely. These
functions return htpy elements, which you can combine with other htpy elements
and return directly from the route, which is often useful for admin routes, for
example. You can also use [`template.blank`](/ref/atr/template.py:blank), which
renders a minimal template with just a title and content area. This is useful
for simple pages that do not need the full templat [...]
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index 2340899..dd91e3b 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -44,7 +44,9 @@ import atr.get.sbom as sbom
import atr.get.start as start
import atr.get.tokens as tokens
import atr.get.upload as upload
+import atr.get.user as user
import atr.get.vote as vote
+import atr.get.voting as voting
ROUTES_MODULE: Final[Literal[True]] = True
@@ -74,5 +76,7 @@ __all__ = [
"start",
"tokens",
"upload",
+ "user",
"vote",
+ "voting",
]
diff --git a/atr/routes/user.py b/atr/get/user.py
similarity index 56%
rename from atr/routes/user.py
rename to atr/get/user.py
index 84476ae..539afa2 100644
--- a/atr/routes/user.py
+++ b/atr/get/user.py
@@ -15,34 +15,21 @@
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
import quart
+import atr.blueprints.get as get
import atr.forms as forms
import atr.htm as htm
-import atr.route as route
+import atr.shared as shared
import atr.template as template
import atr.util as util
+import atr.web as web
-if TYPE_CHECKING:
- import werkzeug.wrappers.response as response
-
-
-class CacheForm(forms.Typed):
- cache_submit = forms.submit("Cache me!")
-
-
-class DeleteCacheForm(forms.Typed):
- delete_submit = forms.submit("Delete my cache")
-
[email protected]("/user/cache", methods=["GET"])
-async def cache_get(session: route.CommitterSession) -> str:
- cache_form = await CacheForm.create_form()
- delete_cache_form = await DeleteCacheForm.create_form()
[email protected]("/user/cache")
+async def cache_get(session: web.Committer) -> str:
+ cache_form = await shared.user.CacheForm.create_form()
+ delete_cache_form = await shared.user.DeleteCacheForm.create_form()
cache_data = await util.session_cache_read()
user_cached = session.uid in cache_data
@@ -99,53 +86,3 @@ async def cache_get(session: route.CommitterSession) -> str:
block.append(cache_form_element)
return await template.blank("Session cache management",
content=block.collect())
-
-
[email protected]("/user/cache", methods=["POST"])
-async def session_post(session: route.CommitterSession) -> response.Response:
- form_data = await quart.request.form
-
- cache_form = await CacheForm.create_form(data=form_data)
- delete_cache_form = await DeleteCacheForm.create_form(data=form_data)
-
- if cache_form.cache_submit.data:
- await _cache_session(session)
- await quart.flash("Your session has been cached successfully",
"success")
- elif delete_cache_form.delete_submit.data:
- await _delete_session_cache(session)
- await quart.flash("Your cached session has been deleted", "success")
- else:
- await quart.flash("Invalid form submission", "error")
-
- return await session.redirect(cache_get)
-
-
-async def _cache_session(session: route.CommitterSession) -> None:
- cache_data = await util.session_cache_read()
-
- session_data = {
- "uid": session.uid,
- "dn": getattr(session, "dn", None),
- "fullname": getattr(session, "fullname", None),
- "email": getattr(session, "email", f"{session.uid}@apache.org"),
- "isMember": getattr(session, "isMember", False),
- "isChair": getattr(session, "isChair", False),
- "isRoot": getattr(session, "isRoot", False),
- "pmcs": getattr(session, "committees", []),
- "projects": getattr(session, "projects", []),
- "mfa": getattr(session, "mfa", False),
- "roleaccount": getattr(session, "isRole", False),
- "metadata": getattr(session, "metadata", {}),
- }
-
- cache_data[session.uid] = session_data
-
- await util.session_cache_write(cache_data)
-
-
-async def _delete_session_cache(session: route.CommitterSession) -> None:
- cache_data = await util.session_cache_read()
-
- if session.uid in cache_data:
- del cache_data[session.uid]
- await util.session_cache_write(cache_data)
diff --git a/atr/get/voting.py b/atr/get/voting.py
new file mode 100644
index 0000000..2d4dc05
--- /dev/null
+++ b/atr/get/voting.py
@@ -0,0 +1,30 @@
+# 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.
+
+
+import werkzeug.wrappers.response as response
+
+import atr.blueprints.get as get
+import atr.shared as shared
+import atr.web as web
+
+
[email protected]("/voting/<project_name>/<version_name>/<revision>")
+async def selected_revision(
+ session: web.Committer, project_name: str, version_name: str, revision: str
+) -> response.Response | str:
+ return await shared.voting.selected_revision(session, project_name,
version_name, revision)
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index 7e5238d..576ce08 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -32,7 +32,9 @@ import atr.post.sbom as sbom
import atr.post.start as start
import atr.post.tokens as tokens
import atr.post.upload as upload
+import atr.post.user as user
import atr.post.vote as vote
+import atr.post.voting as voting
ROUTES_MODULE: Final[Literal[True]] = True
@@ -52,5 +54,7 @@ __all__ = [
"start",
"tokens",
"upload",
+ "user",
"vote",
+ "voting",
]
diff --git a/atr/post/user.py b/atr/post/user.py
new file mode 100644
index 0000000..8db3963
--- /dev/null
+++ b/atr/post/user.py
@@ -0,0 +1,76 @@
+# 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.
+
+
+import quart
+import werkzeug.wrappers.response as response
+
+import atr.blueprints.post as post
+import atr.get as get
+import atr.shared as shared
+import atr.util as util
+import atr.web as web
+
+
[email protected]("/user/cache")
+async def session_post(session: web.Committer) -> response.Response:
+ form_data = await quart.request.form
+
+ cache_form = await shared.user.CacheForm.create_form(data=form_data)
+ delete_cache_form = await
shared.user.DeleteCacheForm.create_form(data=form_data)
+
+ if cache_form.cache_submit.data:
+ await _cache_session(session)
+ await quart.flash("Your session has been cached successfully",
"success")
+ elif delete_cache_form.delete_submit.data:
+ await _delete_session_cache(session)
+ await quart.flash("Your cached session has been deleted", "success")
+ else:
+ await quart.flash("Invalid form submission", "error")
+
+ return await session.redirect(get.user.cache_get)
+
+
+async def _cache_session(session: web.Committer) -> None:
+ cache_data = await util.session_cache_read()
+
+ session_data = {
+ "uid": session.uid,
+ "dn": getattr(session, "dn", None),
+ "fullname": getattr(session, "fullname", None),
+ "email": getattr(session, "email", f"{session.uid}@apache.org"),
+ "isMember": getattr(session, "isMember", False),
+ "isChair": getattr(session, "isChair", False),
+ "isRoot": getattr(session, "isRoot", False),
+ "pmcs": getattr(session, "committees", []),
+ "projects": getattr(session, "projects", []),
+ "mfa": getattr(session, "mfa", False),
+ "roleaccount": getattr(session, "isRole", False),
+ "metadata": getattr(session, "metadata", {}),
+ }
+
+ cache_data[session.uid] = session_data
+
+ await util.session_cache_write(cache_data)
+
+
+async def _delete_session_cache(session: web.Committer) -> None:
+ cache_data = await util.session_cache_read()
+
+ if session.uid in cache_data:
+ del cache_data[session.uid]
+ await util.session_cache_write(cache_data)
diff --git a/atr/post/voting.py b/atr/post/voting.py
new file mode 100644
index 0000000..69dd0f3
--- /dev/null
+++ b/atr/post/voting.py
@@ -0,0 +1,31 @@
+# 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.
+
+
+import werkzeug.wrappers.response as response
+
+import atr.blueprints.post as post
+import atr.shared as shared
+import atr.web as web
+
+
[email protected]("/voting/<project_name>/<version_name>/<revision>")
+async def selected_revision(
+ session: web.Committer, project_name: str, version_name: str, revision: str
+) -> response.Response | str:
+ """Show the vote initiation form for a release."""
+ return await shared.voting.selected_revision(session, project_name,
version_name, revision)
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
deleted file mode 100644
index a8e1b85..0000000
--- a/atr/routes/__init__.py
+++ /dev/null
@@ -1,70 +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.
-
-import atr.routes.user as user
-import atr.routes.voting as voting
-
-__all__ = [
- "user",
- "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/server.py b/atr/server.py
index 1129ed6..12ef32b 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -23,7 +23,6 @@ import datetime
import os
import queue
from collections.abc import Iterable
-from types import ModuleType
from typing import Any
import asfquart
@@ -132,7 +131,6 @@ def app_setup_context(app: base.QuartApp) -> None:
import atr.mapping as mapping
import atr.metadata as metadata
import atr.post as post
- import atr.routes as routes
return {
"admin": admin,
@@ -144,7 +142,6 @@ def app_setup_context(app: base.QuartApp) -> None:
"is_viewing_as_admin_fn": util.is_user_viewing_as_admin,
"is_committee_member_fn": user.is_committee_member,
"post": post,
- "routes": routes,
"static_url": util.static_url,
"unfinished_releases_fn": interaction.unfinished_releases,
# "user_committees_fn": interaction.user_committees,
@@ -385,10 +382,7 @@ def main() -> None:
app.run(port=8080, ssl_keyfile="key.pem", ssl_certfile="cert.pem")
-def register_routes(app: base.QuartApp) -> ModuleType:
- # NOTE: These imports are for their side effects only
- import atr.routes as routes
-
+def register_routes(app: base.QuartApp) -> None:
# Add a global error handler to show helpful error messages with tracebacks
@app.errorhandler(Exception)
async def handle_any_exception(error: Exception) -> Any:
@@ -424,8 +418,6 @@ 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 routes
-
# FIXME: when running in SSL mode, you will receive these exceptions upon
termination at times:
# ssl.SSLError: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application
data after close notify (_ssl.c:2706)
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index d7c5835..14f86a5 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -36,7 +36,9 @@ import atr.shared.resolve as resolve
import atr.shared.start as start
import atr.shared.tokens as tokens
import atr.shared.upload as upload
+import atr.shared.user as user
import atr.shared.vote as vote
+import atr.shared.voting as voting
import atr.storage as storage
import atr.template as template
import atr.util as util
@@ -194,5 +196,7 @@ __all__ = [
"start",
"tokens",
"upload",
+ "user",
"vote",
+ "voting",
]
diff --git a/atr/shared/user.py b/atr/shared/user.py
new file mode 100644
index 0000000..e9dbeff
--- /dev/null
+++ b/atr/shared/user.py
@@ -0,0 +1,27 @@
+# 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.
+
+
+import atr.forms as forms
+
+
+class CacheForm(forms.Typed):
+ cache_submit = forms.submit("Cache me!")
+
+
+class DeleteCacheForm(forms.Typed):
+ delete_submit = forms.submit("Delete my cache")
diff --git a/atr/routes/voting.py b/atr/shared/voting.py
similarity index 97%
rename from atr/routes/voting.py
rename to atr/shared/voting.py
index 0e84a10..f0057ac 100644
--- a/atr/routes/voting.py
+++ b/atr/shared/voting.py
@@ -30,11 +30,11 @@ import atr.get.compose as compose
import atr.get.vote as vote
import atr.log as log
import atr.models.sql as sql
-import atr.route as route
import atr.storage as storage
import atr.template as template
import atr.user as user
import atr.util as util
+import atr.web as web
class VoteInitiateForm(forms.Typed):
@@ -54,9 +54,8 @@ class VoteInitiateForm(forms.Typed):
submit = forms.submit("Send vote email")
[email protected]("/voting/<project_name>/<version_name>/<revision>",
methods=["GET", "POST"])
async def selected_revision(
- session: route.CommitterSession, project_name: str, version_name: str,
revision: str
+ session: web.Committer, 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)
@@ -113,8 +112,8 @@ async def selected_revision(
async def start_vote_manual(
release: sql.Release,
selected_revision_number: str,
- session: route.CommitterSession,
- data: db.Session,
+ session: web.Committer,
+ _data: db.Session,
) -> response.Response | str:
async with storage.write(session) as write:
wacp = await
write.as_project_committee_participant(release.project_name)
@@ -196,7 +195,7 @@ async def _selected_revision_data(
version_name: str,
revision: str,
data: db.Session,
- session: route.CommitterSession,
+ session: web.Committer,
) -> response.Response | str | VoteInitiateForm:
committee = release.committee
if committee is None:
diff --git a/atr/templates/check-selected-release-info.html
b/atr/templates/check-selected-release-info.html
index 65efe3b..09d1b6c 100644
--- a/atr/templates/check-selected-release-info.html
+++ b/atr/templates/check-selected-release-info.html
@@ -59,7 +59,7 @@
class="btn btn-secondary"><i class="bi bi-clock-history me-1"></i>
Revisions</a>
{% if revision_number %}
{% if has_files and (not strict_checking_errors) %}
- <a href="{{ as_url(routes.voting.selected_revision,
project_name=release.project.name, version_name=release.version,
revision=revision_number) }}"
+ <a href="{{ as_url(get.voting.selected_revision,
project_name=release.project.name, version_name=release.version,
revision=revision_number) }}"
title="Start a vote on this draft"
class="btn btn-success"><i class="bi bi-check-circle me-1"></i>
Start voting</a>
{% else %}
diff --git a/atr/templates/voting-selected-revision.html
b/atr/templates/voting-selected-revision.html
index d7436bd..ada9580 100644
--- a/atr/templates/voting-selected-revision.html
+++ b/atr/templates/voting-selected-revision.html
@@ -49,7 +49,7 @@
<form method="post"
id="vote-initiate-form"
class="atr-canary py-4 px-5"
- action="{{ as_url(routes.voting.selected_revision,
project_name=release.project.name, version_name=release.version,
revision=revision_number) }}"
+ action="{{ as_url(post.voting.selected_revision,
project_name=release.project.name, version_name=release.version,
revision=revision_number) }}"
novalidate>
{{ form.hidden_tag() }}
@@ -143,7 +143,7 @@
{% elif manual_vote_process_form %}
<p>This release has the manual vote process enabled. Press the button
below to start a vote.</p>
<form method="post"
- action="{{ as_url(routes.voting.selected_revision,
project_name=release.project.name, version_name=release.version,
revision=revision_number) }}"
+ action="{{ as_url(post.voting.selected_revision,
project_name=release.project.name, version_name=release.version,
revision=revision_number) }}"
novalidate>
{{ manual_vote_process_form.hidden_tag() }}
<div>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]