You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by wa...@apache.org on 2026/06/02 19:38:22 UTC
(tooling-trusted-releases) branch fix-code-refs-in-docs created (now b1fe354b)
This is an automated email from the ASF dual-hosted git repository.
dave2wave pushed a change to branch fix-code-refs-in-docs
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
at b1fe354b Fix code refs in docs
This branch includes the following new commits:
new b1fe354b Fix code refs in docs
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org
(tooling-trusted-releases) 01/01: Fix code refs in docs
Posted by wa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dave2wave pushed a commit to branch fix-code-refs-in-docs
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit b1fe354bc3e91f93963490befcaeab249e220a23
Author: Dave Fisher <da...@comcast.net>
AuthorDate: Tue Jun 2 12:38:12 2026 -0700
Fix code refs in docs
---
atr/docs/authentication-security.md | 2 +-
atr/docs/authorization-security.md | 6 ++---
atr/docs/build-processes.md | 6 ++---
atr/docs/database.md | 26 +++++++++---------
atr/docs/input-validation.md | 42 ++++++++++++++---------------
atr/docs/overview-of-the-code.md | 28 ++++++++++----------
atr/docs/running-and-creating-tests.md | 4 +--
atr/docs/storage-interface.md | 16 ++++++------
atr/docs/user-interface.md | 48 +++++++++++++++++-----------------
9 files changed, 89 insertions(+), 89 deletions(-)
diff --git a/atr/docs/authentication-security.md b/atr/docs/authentication-security.md
index cdc86c75..5a3f021d 100644
--- a/atr/docs/authentication-security.md
+++ b/atr/docs/authentication-security.md
@@ -93,7 +93,7 @@ Session data includes the user's ASF UID and is used to authorize requests. The
### Session caching
-Authorization data fetched from LDAP (committee memberships, project participation) is cached in [`principal.Cache`](/ref/atr/principal.py:Cache) for performance. The cache has a TTL of 300 seconds, defined by `cache_for_at_most_seconds`. After the TTL expires, the next request will refresh the cache from LDAP.
+Authorization data fetched from LDAP (committee memberships, project participation) is cached in [`principal.Cache`](/ref/atr/principal.py) (Cache) for performance. The cache has a TTL of 300 seconds, defined by `cache_for_at_most_seconds`. After the TTL expires, the next request will refresh the cache from LDAP.
## API authentication
diff --git a/atr/docs/authorization-security.md b/atr/docs/authorization-security.md
index 76fca759..129ac560 100644
--- a/atr/docs/authorization-security.md
+++ b/atr/docs/authorization-security.md
@@ -62,7 +62,7 @@ Authorization data is fetched from ASF LDAP using the [`principal`](/ref/atr/pri
* `cn=infrastructure-root,ou=groups,ou=services,dc=apache,dc=org` - Infrastructure root
* `cn=tooling,ou=groups,ou=services,dc=apache,dc=org` - Tooling team
-The [`Committer`](/ref/atr/principal.py:Committer) class fetches a user's full authorization profile from LDAP, including their committee memberships (PMC membership) and project participations (committer access).
+The [`Committer`](/ref/atr/principal.py) (Committer) class fetches a user's full authorization profile from LDAP, including their committee memberships (PMC membership) and project participations (committer access).
## Access control for releases
@@ -189,7 +189,7 @@ projects = auth.participant_of() # Returns frozenset of project names
### Web vs API authorization
-For web requests, the [`Authorisation`](/ref/atr/principal.py:Authorisation) class reads the session automatically:
+For web requests, the [`Authorisation`](/ref/atr/principal.py) (Authorisation) class reads the session automatically:
```python
auth = await Authorisation() # Uses ASFQuart session
@@ -205,7 +205,7 @@ Both paths use the same authorization logic and caching.
## Caching behavior
-LDAP queries are expensive, so authorization data is cached in [`principal.Cache`](/ref/atr/principal.py:Cache). The cache stores:
+LDAP queries are expensive, so authorization data is cached in [`principal.Cache`](/ref/atr/principal.py) (Cache). The cache stores:
* `member_of` - Set of committees where the user is a PMC member
* `participant_of` - Set of projects where the user is a committer
diff --git a/atr/docs/build-processes.md b/atr/docs/build-processes.md
index 7368141f..787159c6 100644
--- a/atr/docs/build-processes.md
+++ b/atr/docs/build-processes.md
@@ -18,8 +18,8 @@ The ATR documentation that you're reading right now is structured like a book, w
The script reads the table of contents in [`atr/docs/index.md`](/ref/atr/docs/index.md), extracts the hierarchy of pages, and then updates every referenced page to include the correct navigation links, page numbers, and section listings. This means that when we want to reorganize the documentation (say, inserting a new chapter or moving sections around) we only need to edit the table of contents, run the script, and all the navigation is updated automatically.
-The implementation is straightforward. The [`parse_toc`](/ref/scripts/docs_build.py:parse_toc) function extracts entries from the table of contents section in the index, and [`build_navigation`](/ref/scripts/docs_build.py:build_navigation) computes the up, previous, and next relationships for each page. The [`update_document`](/ref/scripts/docs_build.py:update_document) function is then called for each page, which rewrites the page heading to match the canonical numbering from the `index [...]
+The implementation is straightforward. The [`parse_toc`](/ref/scripts/docs_build.py) (parse_toc) function extracts entries from the table of contents section in the index, and [`build_navigation`](/ref/scripts/docs_build.py) (build_navigation) computes the up, previous, and next relationships for each page. The [`update_document`](/ref/scripts/docs_build.py) (update_document) function is then called for each page, which rewrites the page heading to match the canonical numbering from the [...]
-The navigation block itself is generated by [`generate_navigation_block`](/ref/scripts/docs_build.py:generate_navigation_block), which formats the up, previous, and next links, adds a list of subpages if any exist, and includes a table of contents for the page's sections as extracted by [`extract_h2_headings`](/ref/scripts/docs_build.py:extract_h2_headings). This keeps all of the navigational machinery separate from the actual content, which starts at the first `##` heading.
+The navigation block itself is generated by [`generate_navigation_block`](/ref/scripts/docs_build.py) (generate_navigation_block), which formats the up, previous, and next links, adds a list of subpages if any exist, and includes a table of contents for the page's sections as extracted by [`extract_h2_headings`](/ref/scripts/docs_build.py) (extract_h2_headings). This keeps all of the navigational machinery separate from the actual content, which starts at the first `##` heading.
-We also validate that every page in the table of contents exists, and that there are no unlinked Markdown files in the documentation directory. The [`validate_files`](/ref/scripts/docs_build.py:validate_files) function performs these checks and fails with a descriptive error if anything is wrong. This prevents us from accidentally forgetting to add a page to the table of contents, or from leaving old pages lying around that we meant to delete.
+We also validate that every page in the table of contents exists, and that there are no unlinked Markdown files in the documentation directory. The [`validate_files`](/ref/scripts/docs_build.py) (validate_files) function performs these checks and fails with a descriptive error if anything is wrong. This prevents us from accidentally forgetting to add a page to the table of contents, or from leaving old pages lying around that we meant to delete.
diff --git a/atr/docs/database.md b/atr/docs/database.md
index f7695679..a60e4044 100644
--- a/atr/docs/database.md
+++ b/atr/docs/database.md
@@ -20,13 +20,13 @@ ATR stores all of its data in a SQLite database. The database schema is defined
## Core models
-The three most important models in ATR are [`Committee`](/ref/atr/models/sql.py:Committee), [`Project`](/ref/atr/models/sql.py:Project), and [`Release`](/ref/atr/models/sql.py:Release).
+The three most important models in ATR are [`Committee`](/ref/atr/models/sql.py) (Committee), [`Project`](/ref/atr/models/sql.py) (Project), and [`Release`](/ref/atr/models/sql.py) (Release).
-A [`Committee`](/ref/atr/models/sql.py:Committee) represents a PMC or PPMC at the ASF. Each committee has a key, which is the primary key, and an optional display name. Committees can have child committees, which is used for the relationship between the Incubator PMC and individual podling PPMCs. Committees also have lists of committee members and committers stored as JSON arrays.
+A [`Committee`](/ref/atr/models/sql.py) (Committee) represents a PMC or PPMC at the ASF. Each committee has a key, which is the primary key, and an optional display name. Committees can have child committees, which is used for the relationship between the Incubator PMC and individual podling PPMCs. Committees also have lists of committee members and committers stored as JSON arrays.
-A [`Project`](/ref/atr/models/sql.py:Project) belongs to a committee and can have multiple releases. Projects have a key as the primary key, along with metadata such as a description and category and programming language tags. Each project can optionally have a [`ReleasePolicy`](/ref/atr/models/sql.py:ReleasePolicy) that defines how releases should be handled, including e.g. vote templates and GitHub workflow configuration.
+A [`Project`](/ref/atr/models/sql.py) (Project) belongs to a committee and can have multiple releases. Projects have a key as the primary key, along with metadata such as a description and category and programming language tags. Each project can optionally have a [`ReleasePolicy`](/ref/atr/models/sql.py) (ReleasePolicy) that defines how releases should be handled, including e.g. vote templates and GitHub workflow configuration.
-A [`Release`](/ref/atr/models/sql.py:Release) belongs to a project and represents a specific version of software which is voted on by a committee. The primary key is a key derived from the project key and version. Releases have a phase that indicates their current state in the release process, from draft composition to final publication. Each release can have multiple [`Revision`](/ref/atr/models/sql.py:Revision) instances before final publication, representing iterations of the underlyi [...]
+A [`Release`](/ref/atr/models/sql.py) (Release) belongs to a project and represents a specific version of software which is voted on by a committee. The primary key is a key derived from the project key and version. Releases have a phase that indicates their current state in the release process, from draft composition to final publication. Each release can have multiple [`Revision`](/ref/atr/models/sql.py) (Revision) instances before final publication, representing iterations of the unde [...]
## Other features
@@ -34,33 +34,33 @@ The models themselves are the most important components, but to support those mo
### Enumerations
-ATR uses Python enumerations to ensure that certain fields only contain valid values. The most important enumeration is [`ReleasePhase`](/ref/atr/models/sql.py:ReleasePhase), which defines the four phases of a release: `RELEASE_CANDIDATE_DRAFT` for composing, `RELEASE_CANDIDATE` for voting, `RELEASE_PREVIEW` for finishing, and `RELEASE` for completed releases.
+ATR uses Python enumerations to ensure that certain fields only contain valid values. The most important enumeration is [`ReleasePhase`](/ref/atr/models/sql.py) (ReleasePhase), which defines the four phases of a release: `RELEASE_CANDIDATE_DRAFT` for composing, `RELEASE_CANDIDATE` for voting, `RELEASE_PREVIEW` for finishing, and `RELEASE` for completed releases.
-The [`TaskStatus`](/ref/atr/models/sql.py:TaskStatus) enumeration defines the states a task can be in: `QUEUED`, `ACTIVE`, `COMPLETED`, or `FAILED`. The [`TaskType`](/ref/atr/models/sql.py:TaskType) enumeration lists all the different types of background tasks that ATR can execute, from signature checks to SBOM generation.
+The [`TaskStatus`](/ref/atr/models/sql.py) (TaskStatus) enumeration defines the states a task can be in: `QUEUED`, `ACTIVE`, `COMPLETED`, or `FAILED`. The [`TaskType`](/ref/atr/models/sql.py) (TaskType) enumeration lists all the different types of background tasks that ATR can execute, from signature checks to SBOM generation.
-The [`DistributionPlatform`](/ref/atr/models/sql.py:DistributionPlatform) enumeration is more complex, as each value contains not just a name but a [`DistributionPlatformValue`](/ref/atr/models/sql.py:DistributionPlatformValue) with template URLs and configuration for different package distribution platforms like PyPI, npm, and Maven Central.
+The [`DistributionPlatform`](/ref/atr/models/sql.py) (DistributionPlatform) enumeration is more complex, as each value contains not just a name but a [`DistributionPlatformValue`](/ref/atr/models/sql.py) (DistributionPlatformValue) with template URLs and configuration for different package distribution platforms like PyPI, npm, and Maven Central.
### Special column types
-SQLite does not support all the data types we need, so we use SQLAlchemy type decorators to handle conversions. The [`UTCDateTime`](/ref/atr/models/sql.py:UTCDateTime) type ensures that all datetime values are stored in UTC and returned as timezone-aware datetime objects. When Python code provides a datetime with timezone information, the type decorator converts it to UTC before storing. When reading from the database, it adds back the UTC timezone information.
+SQLite does not support all the data types we need, so we use SQLAlchemy type decorators to handle conversions. The [`UTCDateTime`](/ref/atr/models/sql.py) (UTCDateTime) type ensures that all datetime values are stored in UTC and returned as timezone-aware datetime objects. When Python code provides a datetime with timezone information, the type decorator converts it to UTC before storing. When reading from the database, it adds back the UTC timezone information.
-The [`ResultsJSON`](/ref/atr/models/sql.py:ResultsJSON) type handles storing task results. It automatically serializes Pydantic models to JSON when writing to the database, and deserializes them back to the appropriate result model when reading.
+The [`ResultsJSON`](/ref/atr/models/sql.py) (ResultsJSON) type handles storing task results. It automatically serializes Pydantic models to JSON when writing to the database, and deserializes them back to the appropriate result model when reading.
### Automatic field population
-Some fields are populated automatically using SQLAlchemy event listeners. When a new [`Revision`](/ref/atr/models/sql.py:Revision) is created, the [`populate_revision_sequence_and_name`](/ref/atr/models/sql.py:populate_revision_sequence_and_name) function runs before the database insert. This function queries for the highest existing sequence number for the release, increments it, and sets both the `seq` and `number` fields. It also constructs the revision name by combining the release n [...]
+Some fields are populated automatically using SQLAlchemy event listeners. When a new [`Revision`](/ref/atr/models/sql.py) (Revision) is created, the [`populate_revision_sequence_and_name`](/ref/atr/models/sql.py) (populate_revision_sequence_and_name) function runs before the database insert. This function queries for the highest existing sequence number for the release, increments it, and sets both the `seq` and `number` fields. It also constructs the revision name by combining the relea [...]
-The [`check_release_key`](/ref/atr/models/sql.py:check_release_key) function runs before inserting a release. If the release name is empty, it automatically generates it from the project name and version using the [`release_key`](/ref/atr/models/sql.py:release_key) helper function.
+The [`check_release_key`](/ref/atr/models/sql.py) (check_release_key) function runs before inserting a release. If the release name is empty, it automatically generates it from the project name and version using the [`release_key`](/ref/atr/models/sql.py) (release_key) helper function.
### Computed properties
-Some properties are computed dynamically rather than stored in the database. The `Release.latest_revision_number` property is implemented as a SQLAlchemy column property using a correlated subquery. This means that when you access `release.latest_revision_number`, SQLAlchemy automatically executes a query to find the highest revision number for that release. The query is defined once in [`RELEASE_LATEST_REVISION_NUMBER`](/ref/atr/models/sql.py:RELEASE_LATEST_REVISION_NUMBER) and attached [...]
+Some properties are computed dynamically rather than stored in the database. The `Release.latest_revision_number` property is implemented as a SQLAlchemy column property using a correlated subquery. This means that when you access `release.latest_revision_number`, SQLAlchemy automatically executes a query to find the highest revision number for that release. The query is defined once in [`RELEASE_LATEST_REVISION_NUMBER`](/ref/atr/models/sql.py) (RELEASE_LATEST_REVISION_NUMBER) and attach [...]
Projects have many computed properties that provide access to release policy settings with appropriate defaults. For example, `Project.policy_start_vote_template` returns the custom vote template if one is configured, or falls back to the default vote template if not. This pattern allows projects to customize their release process while providing sensible defaults.
### Constraints and validation
-Database constraints ensure data integrity. The [`Task`](/ref/atr/models/sql.py:Task) model includes a check constraint that validates the status transitions. A task must start in `QUEUED` state, can only transition to `ACTIVE` when `started` and `pid` are set, and can only reach `COMPLETED` or `FAILED` when the `completed` timestamp is set. These constraints prevent invalid state transitions at the database level.
+Database constraints ensure data integrity. The [`Task`](/ref/atr/models/sql.py) (Task) model includes a check constraint that validates the status transitions. A task must start in `QUEUED` state, can only transition to `ACTIVE` when `started` and `pid` are set, and can only reach `COMPLETED` or `FAILED` when the `completed` timestamp is set. These constraints prevent invalid state transitions at the database level.
Unique constraints ensure that certain combinations of fields are unique. The `Release` model has a unique constraint on `(project_key, version)` to prevent creating duplicate releases for the same project version. The `Revision` model has two unique constraints: one on `(release_key, seq)` and another on `(release_key, number)`, ensuring that revision numbers are unique within a release.
diff --git a/atr/docs/input-validation.md b/atr/docs/input-validation.md
index e169bbb1..c7b6348e 100644
--- a/atr/docs/input-validation.md
+++ b/atr/docs/input-validation.md
@@ -40,11 +40,11 @@ Each layer provides independent protection, so a failure in one layer does not c
## Form validation with Pydantic
-All form inputs in ATR are validated through [Pydantic](https://docs.pydantic.dev/latest/) models defined in [`form.py`](/ref/atr/form.py). The base class for forms is [`Form`](/ref/atr/form.py:Form), which extends Pydantic's `BaseModel`.
+All form inputs in ATR are validated through [Pydantic](https://docs.pydantic.dev/latest/) models defined in [`form.py`](/ref/atr/form.py). The base class for forms is [`Form`](/ref/atr/form.py) (Form), which extends Pydantic's `BaseModel`.
### Defining form fields
-Form fields are defined using Python type annotations and the [`label`](/ref/atr/form.py:label) function:
+Form fields are defined using Python type annotations and the [`label`](/ref/atr/form.py) (label) function:
```python
class ExampleForm(Form):
@@ -59,10 +59,10 @@ The `label` function accepts a description (shown to users), optional documentat
When a form is submitted, ATR:
-1. Extracts form data from the request via [`quart_request`](/ref/atr/form.py:quart_request)
+1. Extracts form data from the request via [`quart_request`](/ref/atr/form.py) (quart_request)
2. Passes the data to the Pydantic model for validation
-3. If validation fails, collects errors via [`flash_error_data`](/ref/atr/form.py:flash_error_data)
-4. Displays errors to the user with [`flash_error_summary`](/ref/atr/form.py:flash_error_summary)
+3. If validation fails, collects errors via [`flash_error_data`](/ref/atr/form.py) (flash_error_data)
+4. Displays errors to the user with [`flash_error_summary`](/ref/atr/form.py) (flash_error_summary)
5. If validation succeeds, proceeds with the validated data
Pydantic provides built-in validators for common types (strings, integers, emails, URLs) and supports custom validators via decorators.
@@ -86,7 +86,7 @@ class ReleaseForm(Form):
## CSRF protection
-All POST forms must include a CSRF token. The token is generated by [`csrf_input`](/ref/atr/form.py:csrf_input) and validated automatically by Quart-WTF:
+All POST forms must include a CSRF token. The token is generated by [`csrf_input`](/ref/atr/form.py) (csrf_input) and validated automatically by Quart-WTF:
```python
def csrf_input() -> htm.VoidElement:
@@ -166,11 +166,11 @@ Beyond input validation, ATR performs data integrity validation on database reco
### Committee validation
-Committee records are not created through a form. They are synced from LDAP and Whimsy by [`_update_committees`](/ref/atr/datasources/apache.py:_update_committees), so their fields have no Pydantic input layer, and the data integrity validators are the main place each field is confirmed.
+Committee records are not created through a form. They are synced from LDAP and Whimsy by [`_update_committees`](/ref/atr/datasources/apache.py) (_update_committees), so their fields have no Pydantic input layer, and the data integrity validators are the main place each field is confirmed.
-The [`committee`](/ref/atr/validate.py:committee) function checks:
+The [`committee`](/ref/atr/validate.py) (committee) function checks:
-* `key` must be a valid committee key (the [`safe.CommitteeKey`](/ref/atr/models/safe.py:CommitteeKey) character set)
+* `key` must be a valid committee key (the [`safe.CommitteeKey`](/ref/atr/models/safe.py) (CommitteeKey) character set)
* `name` must be set, trimmed, and not prefixed with "Apache "
* `committee_members`, `committers`, and `release_managers` entries must each look like an ASF UID
* `child_committees` must be empty (not used)
@@ -190,11 +190,11 @@ Per-field coverage:
### Project validation
-Projects do have an input layer. [`AddProjectForm`](/ref/atr/shared/projects.py:AddProjectForm) validates the key and display name at creation, and [`EditVersionSchemeForm`](/ref/atr/shared/projects.py:EditVersionSchemeForm) rejects a `version_pattern` or `cycle_match` that is not a valid regex (and a `cycle_match` with no capture group). The data integrity validators mirror these and cover fields that have no form.
+Projects do have an input layer. [`AddProjectForm`](/ref/atr/shared/projects.py) (AddProjectForm) validates the key and display name at creation, and [`EditVersionSchemeForm`](/ref/atr/shared/projects.py) (EditVersionSchemeForm) rejects a `version_pattern` or `cycle_match` that is not a valid regex (and a `cycle_match` with no capture group). The data integrity validators mirror these and cover fields that have no form.
-The [`project`](/ref/atr/validate.py:project) function checks:
+The [`project`](/ref/atr/validate.py) (project) function checks:
-* `key` must be a valid project key (the [`safe.ProjectKey`](/ref/atr/models/safe.py:ProjectKey) character set)
+* `key` must be a valid project key (the [`safe.ProjectKey`](/ref/atr/models/safe.py) (ProjectKey) character set)
* `category` must use comma-separated labels without colons
* `committee_key` must be set (project must be linked to a committee)
* `created` timestamp must be in the past
@@ -225,7 +225,7 @@ Per-field coverage:
### Release validation
-The [`release`](/ref/atr/validate.py:release) function checks:
+The [`release`](/ref/atr/validate.py) (release) function checks:
* `created` timestamp must be in the past
* `name` must match the expected pattern for project and version
@@ -253,19 +253,19 @@ Field validation is only the first step. ATR also checks whether an action still
### Vote initiation
-Before a vote can start, [`release_ready_to_start_vote`](/ref/atr/db/interaction.py:release_ready_to_start_vote) checks that the release still has a latest revision and that the release is still attached to a committee. It also checks that the requested vote mode agrees with the stored project policy, so ATR does not let a user start a manual vote for a project configured for standard voting, or the reverse.
+Before a vote can start, [`release_ready_to_start_vote`](/ref/atr/db/interaction.py) (release_ready_to_start_vote) checks that the release still has a latest revision and that the release is still attached to a committee. It also checks that the requested vote mode agrees with the stored project policy, so ATR does not let a user start a manual vote for a project configured for standard voting, or the reverse.
-That same validation step then checks the surrounding release state. The user must be a committee member for the project or an ATR administrator. The latest revision must have no blocker results, and the release candidate draft must contain files. When ATR actually promotes the release into the voting phase, [`promote_to_candidate`](/ref/atr/storage/writers/release.py:promote_to_candidate) adds a task state check and refuses the transition while queued or active tasks still exist for tha [...]
+That same validation step then checks the surrounding release state. The user must be a committee member for the project or an ATR administrator. The latest revision must have no blocker results, and the release candidate draft must contain files. When ATR actually promotes the release into the voting phase, [`promote_to_candidate`](/ref/atr/storage/writers/release.py) (promote_to_candidate) adds a task state check and refuses the transition while queued or active tasks still exist for t [...]
### Trusted Publishing
-Trusted Publishing settings are validated when they are stored and again when they are used. On write, [`validate_trusted_publishing_constraints`](/ref/atr/util.py:validate_trusted_publishing_constraints) and [`policy.py`](/ref/atr/storage/writers/policy.py) normalize the configured repository name, branch, and workflow paths and reject incomplete or impossible combinations. A workflow path cannot be stored without a repository name. A branch cannot be stored without a repository name. R [...]
+Trusted Publishing settings are validated when they are stored and again when they are used. On write, [`validate_trusted_publishing_constraints`](/ref/atr/util.py) (validate_trusted_publishing_constraints) and [`policy.py`](/ref/atr/storage/writers/policy.py) normalize the configured repository name, branch, and workflow paths and reject incomplete or impossible combinations. A workflow path cannot be stored without a repository name. A branch cannot be stored without a repository name. [...]
-At request time, [`_trusted_project_checks`](/ref/atr/db/interaction.py:_trusted_project_checks) and [`_trusted_project`](/ref/atr/db/interaction.py:_trusted_project) compare the GitHub token claims with the stored policy. The repository must be under `apache`. The workflow reference must begin with that same repository, must include a git ref, and must resolve to a workflow path under `.github/workflows/`. ATR then looks up the project by repository name and by the phase specific workfl [...]
+At request time, [`_trusted_project_checks`](/ref/atr/db/interaction.py) (_trusted_project_checks) and [`_trusted_project`](/ref/atr/db/interaction.py) (_trusted_project) compare the GitHub token claims with the stored policy. The repository must be under `apache`. The workflow reference must begin with that same repository, must include a git ref, and must resolve to a workflow path under `.github/workflows/`. ATR then looks up the project by repository name and by the phase specific wo [...]
### Email delivery
-Email validation in ATR also depends on context. [`validate_email_recipients`](/ref/atr/util.py:validate_email_recipients) requires a primary recipient and rejects duplicate addresses across `To`, `Cc`, and `Bcc`. [`send`](/ref/atr/mail.py:send) then requires the sender to use `@apache.org`, and [`_validate_recipient`](/ref/atr/mail.py:_validate_recipient) rejects any envelope recipient outside `@apache.org` and its subdomains. This means that vote and release mail must go to ASF control [...]
+Email validation in ATR also depends on context. [`validate_email_recipients`](/ref/atr/util.py) (validate_email_recipients) requires a primary recipient and rejects duplicate addresses across `To`, `Cc`, and `Bcc`. [`send`](/ref/atr/mail.py) (send) then requires the sender to use `@apache.org`, and [`_validate_recipient`](/ref/atr/mail.py) (_validate_recipient) rejects any envelope recipient outside `@apache.org` and its subdomains. This means that vote and release mail must go to ASF c [...]
## Output encoding
@@ -305,7 +305,7 @@ Uploaded files are stored outside the application in configured directories (e.g
### File handling
-Files are processed via [`quart.datastructures.FileStorage`](/ref/atr/form.py:quart_request) and validated before being written to disk. Empty files (where the browser sends a file input with no selection) are filtered out.
+Files are processed via [`quart.datastructures.FileStorage`](/ref/atr/form.py) (quart_request) and validated before being written to disk. Empty files (where the browser sends a file input with no selection) are filtered out.
## Injection prevention
@@ -338,9 +338,9 @@ Path traversal is prevented by:
* Validating that paths remain within expected directories
* Rejecting file names containing path separators
-For form fields that accept file or directory paths, always use `form.RelPath` (or `form.RelPathList` for multiple paths). These types automatically call [`to_relpath()`](/ref/atr/form.py:to_relpath), which rejects path traversal sequences, absolute paths, and empty values at the Pydantic validation layer. This is the preferred approach because it prevents path traversal before the handler code runs.
+For form fields that accept file or directory paths, always use `form.RelPath` (or `form.RelPathList` for multiple paths). These types automatically call [`to_relpath()`](/ref/atr/form.py) (to_relpath), which rejects path traversal sequences, absolute paths, and empty values at the Pydantic validation layer. This is the preferred approach because it prevents path traversal before the handler code runs.
-For cases outside of form validation (e.g., URL route parameters), use [`form.to_relpath()`](/ref/atr/form.py:to_relpath) directly, or validate manually:
+For cases outside of form validation (e.g., URL route parameters), use [`form.to_relpath()`](/ref/atr/form.py) (to_relpath) directly, or validate manually:
```python
import pathlib
diff --git a/atr/docs/overview-of-the-code.md b/atr/docs/overview-of-the-code.md
index 21ebb58a..ea7b2d71 100644
--- a/atr/docs/overview-of-the-code.md
+++ b/atr/docs/overview-of-the-code.md
@@ -18,25 +18,25 @@
## Introduction
-This page is a high level view of the ATR code. References to symbols in this section are given without their `atr.` prefix, for brevity, and are linked to the respective source code. You should understand e.g. [`server.app`](/ref/atr/server.py:app) to mean `atr.server.app`.
+This page is a high level view of the ATR code. References to symbols in this section are given without their `atr.` prefix, for brevity, and are linked to the respective source code. You should understand e.g. [`server.app`](/ref/atr/server.py) (app) to mean `atr.server.app`.
## Hypercorn and ASGI
-ATR is an [ASFQuart](https://github.com/apache/infrastructure-asfquart) application, running in [Hypercorn](https://hypercorn.readthedocs.io/en/latest/index.html). The entry point for Hypercorn is [`server.app`](/ref/atr/server.py:app), which is then called with a few standard arguments [as per the ASGI specification](https://asgi.readthedocs.io/en/latest/specs/main.html#overview). We create the `app` object using the following code:
+ATR is an [ASFQuart](https://github.com/apache/infrastructure-asfquart) application, running in [Hypercorn](https://hypercorn.readthedocs.io/en/latest/index.html). The entry point for Hypercorn is [`server.app`](/ref/atr/server.py) (app), which is then called with a few standard arguments [as per the ASGI specification](https://asgi.readthedocs.io/en/latest/specs/main.html#overview). We create the `app` object using the following code:
```python
app = create_app(config.get())
```
-The [`server.create_app`](/ref/atr/server.py:create_app) 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.
+The [`server.create_app`](/ref/atr/server.py) (create_app) 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.
## 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 [`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.
+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.
-We use [SQLModel](https://sqlmodel.tiangolo.com/), an ORM utilising [Pydantic](https://docs.pydantic.dev/latest/) and [SQLAlchemy](https://www.sqlalchemy.org/), to create Python models for the ATR database. The core models file is [`models.sql`](/ref/atr/models/sql.py). The most important individual SQLite models in this module are [`Committee`](/ref/atr/models/sql.py:Committee), [`Project`](/ref/atr/models/sql.py:Project), and [`Release`](/ref/atr/models/sql.py:Release).
+We use [SQLModel](https://sqlmodel.tiangolo.com/), an ORM utilising [Pydantic](https://docs.pydantic.dev/latest/) and [SQLAlchemy](https://www.sqlalchemy.org/), to create Python models for the ATR database. The core models file is [`models.sql`](/ref/atr/models/sql.py). The most important individual SQLite models in this module are [`Committee`](/ref/atr/models/sql.py) (Committee), [`Project`](/ref/atr/models/sql.py) (Project), and [`Release`](/ref/atr/models/sql.py) (Release).
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`.
@@ -46,13 +46,13 @@ These interfaces, including route handlers in [`get`](/ref/atr/get/), [`post`](/
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.
-The template system in ATR is [Jinja2](https://jinja.palletsprojects.com/), always accessed through the ATR [`template`](/ref/atr/template.py) module. Template files in Jinja2 syntax are stored in [`templates/`](/ref/atr/templates/), and route handlers render them using the asynchronous [`template.render`](/ref/atr/template.py:render) function.
+The template system in ATR is [Jinja2](https://jinja.palletsprojects.com/), always accessed through the ATR [`template`](/ref/atr/template.py) module. Template files in Jinja2 syntax are stored in [`templates/`](/ref/atr/templates/), and route handlers render them using the asynchronous [`template.render`](/ref/atr/template.py) (render) function.
-Template rendering can be slow if templates are loaded from disk on every request. To address this, we use [`preload`](/ref/atr/preload.py) to load all templates into memory before the server starts serving requests. The [`preload.setup_template_preloading`](/ref/atr/preload.py:setup_template_preloading) function registers a startup hook that finds and caches every template file.
+Template rendering can be slow if templates are loaded from disk on every request. To address this, we use [`preload`](/ref/atr/preload.py) to load all templates into memory before the server starts serving requests. The [`preload.setup_template_preloading`](/ref/atr/preload.py) (setup_template_preloading) function registers a startup hook that finds and caches every template file.
-The ATR user interface includes many HTML forms. We use [Pydantic](https://docs.pydantic.dev/latest/) for form handling, accessed through the ATR [`form`](/ref/atr/form.py) module. The [`form.Form`](/ref/atr/form.py:Form) base class extends `pydantic.BaseModel` to provide form-specific functionality. Each form field is defined using Pydantic type annotations along with the [`form.label`](/ref/atr/form.py:label) function for metadata like labels and documentation. Validation happens autom [...]
+The ATR user interface includes many HTML forms. We use [Pydantic](https://docs.pydantic.dev/latest/) for form handling, accessed through the ATR [`form`](/ref/atr/form.py) module. The [`form.Form`](/ref/atr/form.py) (Form) base class extends `pydantic.BaseModel` to provide form-specific functionality. Each form field is defined using Pydantic type annotations along with the [`form.label`](/ref/atr/form.py) (label) function for metadata like labels and documentation. Validation happens a [...]
-In addition to templates, we sometimes need to generate HTML programmatically in Python. For this we use [htpy](https://htpy.dev/), another third party library, for building HTML using Python syntax. The ATR [`htm`](/ref/atr/htm.py) module extends htpy with a [`Block`](/ref/atr/htm.py:Block) class that makes it easier to build complex HTML structures incrementally. Using htpy means that we get type checking for our HTML generation, and can compose HTML elements just like any other Python [...]
+In addition to templates, we sometimes need to generate HTML programmatically in Python. For this we use [htpy](https://htpy.dev/), another third party library, for building HTML using Python syntax. The ATR [`htm`](/ref/atr/htm.py) module extends htpy with a [`Block`](/ref/atr/htm.py) (Block) class that makes it easier to build complex HTML structures incrementally. Using htpy means that we get type checking for our HTML generation, and can compose HTML elements just like any other Pyth [...]
Refer to the [full user interface development guide](user-interface) for more information about this topic.
@@ -60,22 +60,22 @@ Refer to the [full user interface development guide](user-interface) for more in
Many operations in ATR are too slow to run during an HTTP request, so we run them asynchronously in background worker processes. The task scheduling system in ATR is built from three components: a task queue stored in the SQLite database, a worker manager that spawns and monitors worker processes, and the worker processes themselves that claim and execute tasks.
-The task queue is stored in a table defined by the [`Task`](/ref/atr/models/sql.py:Task) model in [`models.sql`](/ref/atr/models/sql.py). Each task has a status, a type, arguments encoded as JSON, and metadata such as when it was added and which user created it. When route handlers need to perform slow operations, they create a new `Task` row with status `QUEUED` and commit it to the database.
+The task queue is stored in a table defined by the [`Task`](/ref/atr/models/sql.py) (Task) model in [`models.sql`](/ref/atr/models/sql.py). Each task has a status, a type, arguments encoded as JSON, and metadata such as when it was added and which user created it. When route handlers need to perform slow operations, they create a new `Task` row with status `QUEUED` and commit it to the database.
-The ATR [`manager`](/ref/atr/manager.py) module provides the [`WorkerManager`](/ref/atr/manager.py:WorkerManager) class, which maintains a pool of worker processes. When the ATR server starts, the manager spawns a configurable number of worker processes and monitors them continuously. The manager checks every few seconds whether workers are still running, whether any tasks have exceeded their time limits, and whether the worker pool needs to be replenished. If a worker process exits afte [...]
+The ATR [`manager`](/ref/atr/manager.py) module provides the [`WorkerManager`](/ref/atr/manager.py) (WorkerManager) class, which maintains a pool of worker processes. When the ATR server starts, the manager spawns a configurable number of worker processes and monitors them continuously. The manager checks every few seconds whether workers are still running, whether any tasks have exceeded their time limits, and whether the worker pool needs to be replenished. If a worker process exits af [...]
-The ATR [`worker`](/ref/atr/worker.py) module implements the workers. Each worker process runs in a loop. It claims the oldest queued task from the database, executes it, records the result, and then claims the next task atomically using an `UPDATE ... WHERE` statement. After a worker has processed a fixed number of tasks, it exits voluntarily to help to avoid memory leaks. The manager then spawns a fresh worker to replace it. Task execution happens in the [`_task_process`](/ref/atr/work [...]
+The ATR [`worker`](/ref/atr/worker.py) module implements the workers. Each worker process runs in a loop. It claims the oldest queued task from the database, executes it, records the result, and then claims the next task atomically using an `UPDATE ... WHERE` statement. After a worker has processed a fixed number of tasks, it exits voluntarily to help to avoid memory leaks. The manager then spawns a fresh worker to replace it. Task execution happens in the [`_task_process`](/ref/atr/work [...]
Tasks themselves are defined in the ATR [`tasks`](/ref/atr/tasks/) directory. The [`tasks`](/ref/atr/tasks/__init__.py) module contains functions for queueing tasks and resolving task types to their handler functions. Task types include operations such as importing keys, generating SBOMs, sending messages, and importing files from SVN. The most common category of task is automated checks on release artifacts. These checks are implemented in [`tasks/checks/`](/ref/atr/tasks/checks/), and [...]
## API
-The ATR API provides programmatic access to most ATR functionality. API endpoints are defined in [`api`](/ref/atr/api/__init__.py), and their URL paths are prefixed with `/api/`. The API uses [OpenAPI](https://www.openapis.org/) for documentation, which is automatically generated from the endpoint definitions and served at `/api/docs`. Users send requests with a [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) created from a [PAT](https://en.wikipedia.org/wiki/Personal_access_token). [...]
+The ATR API provides programmatic access to most ATR functionality. API endpoints are defined in [`api`](/ref/atr/api/__init__.py), and their URL paths are prefixed with `/api/`. The API uses [OpenAPI](https://www.openapis.org/) for documentation, which is automatically generated from the endpoint definitions and served at `/api/docs`. Users send requests with a [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) created from a [PAT](https://en.wikipedia.org/wiki/Personal_access_token). [...]
API request and response models are defined in [`models.api`](/ref/atr/models/api.py) using Pydantic. Each endpoint has an associated request model that validates incoming data, and a response model that validates outgoing data. The API returns JSON in all cases, with appropriate HTTP status codes.
## Other important interfaces
-ATR uses ASF OAuth for user login, and then determines what actions each user can perform based on their committee memberships. The ATR [`principal`](/ref/atr/principal.py) module handles authorization by checking whether users are members of relevant committees. It queries and caches LDAP to get committee membership information. The [`Authorisation`](/ref/atr/principal.py:Authorisation) class provides methods to check whether a user is a member of a committee or a project participant, w [...]
+ATR uses ASF OAuth for user login, and then determines what actions each user can perform based on their committee memberships. The ATR [`principal`](/ref/atr/principal.py) module handles authorization by checking whether users are members of relevant committees. It queries and caches LDAP to get committee membership information. The [`Authorisation`](/ref/atr/principal.py) (Authorisation) class provides methods to check whether a user is a member of a committee or a project participant, [...]
The server configuration in [`config`](/ref/atr/config.py) determines a lot of global state, and the [`util`](/ref/atr/util.py) module contains lots of useful code which is used throughout ATR.
diff --git a/atr/docs/running-and-creating-tests.md b/atr/docs/running-and-creating-tests.md
index f50abd25..6471ea52 100644
--- a/atr/docs/running-and-creating-tests.md
+++ b/atr/docs/running-and-creating-tests.md
@@ -60,11 +60,11 @@ The browser tests use [Playwright](https://playwright.dev/), which is a cross-br
We use the official Playwright OCI container, install a few dependencies (`apt-get` is available in the container), and then run `test.py`.
-The `test.py` script calls [`run_tests`](/ref/playwright/test.py:run_tests) from its `main`, which sets up all the context, but the main action takes place in [`test_all`](/ref/playwright/test.py:test_all). This function removes any state accidentally left over from a previous run, then runs tests of certain components. Because ATR is stateful, the order of the tests is important. When adding a test, please be careful to ensure that you use the correct state and that you try not to modif [...]
+The `test.py` script calls [`run_tests`](/ref/playwright/test.py) (run_tests) from its `main`, which sets up all the context, but the main action takes place in [`test_all`](/ref/playwright/test.py) (test_all). This function removes any state accidentally left over from a previous run, then runs tests of certain components. Because ATR is stateful, the order of the tests is important. When adding a test, please be careful to ensure that you use the correct state and that you try not to m [...]
We want to make it more clear which Playwright tests depend on which, and have more isolated tests. Reusing context, however, helps to speed up the tests.
-The actual test cases themselves tend to use helpers such as [`go_to_path`](/ref/playwright/test.py:go_to_path) and [`wait_for_path`](/ref/playwright/test.py:wait_for_path), and then call [`logging.info`](https://docs.python.org/3/library/logging.html#logging.info) to print information to the console. Try to keep logging messages terse and informative.
+The actual test cases themselves tend to use helpers such as [`go_to_path`](/ref/playwright/test.py) (go_to_path) and [`wait_for_path`](/ref/playwright/test.py) (wait_for_path), and then call [`logging.info`](https://docs.python.org/3/library/logging.html#logging.info) to print information to the console. Try to keep logging messages terse and informative.
## Running end-to-end tests
diff --git a/atr/docs/storage-interface.md b/atr/docs/storage-interface.md
index 5f1b6a47..50404d8b 100644
--- a/atr/docs/storage-interface.md
+++ b/atr/docs/storage-interface.md
@@ -44,9 +44,9 @@ async with storage.write(session) as write:
The `wacp` object, short for `w`rite `a`s `c`ommittee `p`articipant, provides access to domain-specific writers: `announce`, `checks`, `distributions`, `keys`, `policy`, `project`, `release`, `sbom`, `ssh`, `tokens`, and `vote`.
-The write session takes an optional [`Committer`](/ref/atr/web.py:Committer) or ASF UID, typically `session.uid` from the logged-in user. If you omit the UID, the session determines it automatically from the current request context. The write object checks LDAP memberships and raises [`storage.AccessError`](/ref/atr/storage/__init__.py:AccessError) if the user is not authorized for the requested permission level.
+The write session takes an optional [`Committer`](/ref/atr/web.py) (Committer) or ASF UID, typically `session.uid` from the logged-in user. If you omit the UID, the session determines it automatically from the current request context. The write object checks LDAP memberships and raises [`storage.AccessError`](/ref/atr/storage/__init__.py) (AccessError) if the user is not authorized for the requested permission level.
-Because projects belong to committees, we provide [`write.as_project_committee_member(project_name)`](/ref/atr/storage/__init__.py:as_project_committee_member) and [`write.as_project_committee_participant(project_name)`](/ref/atr/storage/__init__.py:as_project_committee_participant), which look up the project's committee and authenticate the user as a member or participant of that committee. This is convenient when, for example, the URL provides a project name.
+Because projects belong to committees, we provide [`write.as_project_committee_member(project_name)`](/ref/atr/storage/__init__.py) (as_project_committee_member) and [`write.as_project_committee_participant(project_name)`](/ref/atr/storage/__init__.py) (as_project_committee_participant), which look up the project's committee and authenticate the user as a member or participant of that committee. This is convenient when, for example, the URL provides a project name.
Some storage writers perform additional authorization validation beyond what `as_project_committee_member` provides. The check ignores writer, for example, validates that the target project belongs to the committee for which the user is authorized. Therefore even if a caller mistakenly passes an incorrect project name, the writer will reject the operation.
@@ -113,15 +113,15 @@ class CommitteeMember(CommitteeParticipant):
This hierarchy that this creates is: `GeneralPublic` → `FoundationCommitter` → `CommitteeParticipant` → `CommitteeMember`. You can add methods at any level. A method on `CommitteeMember` is only available to committee members, while a method on `FoundationCommitter` is available to everyone who has logged in.
-Use `__private_methods` for helper code that is not part of the public interface. Use `public_methods` for operations that should be available to callers at the appropriate permission level. Consider returning [`Outcome`](/ref/atr/storage/outcome.py:Outcome) types to allow callers flexibility in error handling. Refer to the [section on using outcomes](#how-do-we-use-outcomes) for more details.
+Use `__private_methods` for helper code that is not part of the public interface. Use `public_methods` for operations that should be available to callers at the appropriate permission level. Consider returning [`Outcome`](/ref/atr/storage/outcome.py) (Outcome) types to allow callers flexibility in error handling. Refer to the [section on using outcomes](#how-do-we-use-outcomes) for more details.
-After adding a new writer module, register it in the appropriate `WriteAs*` classes in [`storage/__init__.py`](/ref/atr/storage/__init__.py). For example, when adding the `distributions` writer, it was necessary to add `self.distributions = writers.distributions.CommitteeMember(write, self, data, committee_key)` to the [`WriteAsCommitteeMember`](/ref/atr/storage/__init__.py:WriteAsCommitteeMember) class.
+After adding a new writer module, register it in the appropriate `WriteAs*` classes in [`storage/__init__.py`](/ref/atr/storage/__init__.py). For example, when adding the `distributions` writer, it was necessary to add `self.distributions = writers.distributions.CommitteeMember(write, self, data, committee_key)` to the [`WriteAsCommitteeMember`](/ref/atr/storage/__init__.py) (WriteAsCommitteeMember) class.
## How do we use outcomes?
Consider using **outcome types** from [`storage.outcome`](/ref/atr/storage/outcome.py) 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.
-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()`.
+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 [`post/keys.py`](/ref/atr/post/keys.py) that processes multiple keys and collects outcomes:
@@ -134,15 +134,15 @@ success_count = outcomes.result_count
error_count = outcomes.error_count
```
-The `ensure_associated` method returns an [`outcome.List`](/ref/atr/storage/outcome.py:List), which is a collection of outcomes. Some keys might import successfully, and others might fail because they are malformed or already exist. The caller can inspect the list to see how many succeeded and how many failed, and present that information to the user.
+The `ensure_associated` method returns an [`outcome.List`](/ref/atr/storage/outcome.py) (List), which is a collection of outcomes. Some keys might import successfully, and others might fail because they are malformed or already exist. The caller can inspect the list to see how many succeeded and how many failed, and present that information to the user.
-The `outcome.List` class provides many useful methods: [`results()`](/ref/atr/storage/outcome.py:results) to get only the successful values, [`errors()`](/ref/atr/storage/outcome.py:errors) to get only the exceptions, [`result_count`](/ref/atr/storage/outcome.py:result_count) and [`error_count`](/ref/atr/storage/outcome.py:error_count) to count them, and [`results_or_raise()`](/ref/atr/storage/outcome.py:results_or_raise) to extract all values or raise on the first error.
+The `outcome.List` class provides many useful methods: [`results()`](/ref/atr/storage/outcome.py) (results) to get only the successful values, [`errors()`](/ref/atr/storage/outcome.py) (errors) to get only the exceptions, [`result_count`](/ref/atr/storage/outcome.py) (result_count) and [`error_count`](/ref/atr/storage/outcome.py) (error_count) to count them, and [`results_or_raise()`](/ref/atr/storage/outcome.py) (results_or_raise) to extract all values or raise on the first error.
Use outcomes when an operation might fail for some items but succeed for others, or when you want to give the caller control over error handling. Do not use them when failure should always raise an exception, such as authorization failures or database connection errors. Those should be raised immediately.
## What about audit logging?
-Storage write operations can be logged to [`config.AppConfig.STORAGE_AUDIT_LOG_FILE`](/ref/atr/config.py:STORAGE_AUDIT_LOG_FILE), which is `state/storage-audit.log` by default. Each log entry is a JSON object containing the timestamp, the action name, and relevant parameters. When you write a storage method that should be audited, call `self.__write_as.append_to_audit_log(**kwargs)` with whatever parameters are relevant to that specific operation. The action name is extracted automatical [...]
+Storage write operations can be logged to [`config.AppConfig.STORAGE_AUDIT_LOG_FILE`](/ref/atr/config.py) (STORAGE_AUDIT_LOG_FILE), which is `state/storage-audit.log` by default. Each log entry is a JSON object containing the timestamp, the action name, and relevant parameters. When you write a storage method that should be audited, call `self.__write_as.append_to_audit_log(**kwargs)` with whatever parameters are relevant to that specific operation. The action name is extracted automatic [...]
Audit logging must be done manually because the values to log are often those computed during method execution, not just those passed as arguments which could be logged automatically. When deleting a release, for example, we log `asf_uid` (instance attribute), `project_name` (argument), and `version` (argument), but when issuing a JWT from a PAT, we log `asf_uid` (instance attribute) and `pat_hash` (_computed_). Each operation logs what makes sense for that operation.
diff --git a/atr/docs/user-interface.md b/atr/docs/user-interface.md
index 8b4f83ab..b2241c50 100644
--- a/atr/docs/user-interface.md
+++ b/atr/docs/user-interface.md
@@ -23,9 +23,9 @@ The UI is built from three main pieces: [Jinja2](https://jinja.palletsprojects.c
## Jinja2 templates
-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.
+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 [`get/keys.py`](/ref/atr/get/keys.py:add):
+Here is an example from [`get/keys.py`](/ref/atr/get/keys.py) (add):
```python
return await template.render(
@@ -40,13 +40,13 @@ return await template.render(
The template receives these variables and can access them directly. If you pass a variable called `form`, the template can use `{{ form }}` to render it. [Jinja2 has control structures](https://jinja.palletsprojects.com/en/stable/templates/#list-of-control-structures) like `{% for %}` and `{% if %}`, which you use when iterating over data or conditionally showing content.
-Templates are loaded into memory at server startup by [`preload.setup_template_preloading`](/ref/atr/preload.py:setup_template_preloading). This means that changing a template requires restarting the server in development, which can be configured to happen automatically, but it also means that rendering is fast because we never do a disk read during request handling. The preloading scans [`templates/`](/ref/atr/templates/) recursively and caches every file.
+Templates are loaded into memory at server startup by [`preload.setup_template_preloading`](/ref/atr/preload.py) (setup_template_preloading). This means that changing a template requires restarting the server in development, which can be configured to happen automatically, but it also means that rendering is fast because we never do a disk read during request handling. The preloading scans [`templates/`](/ref/atr/templates/) recursively and caches every file.
-Template rendering happens in a thread pool to avoid blocking the async event loop. The function [`_render_in_thread`](/ref/atr/template.py:_render_in_thread) uses `asyncio.to_thread` to execute Jinja2's synchronous `render` method.
+Template rendering happens in a thread pool to avoid blocking the async event loop. The function [`_render_in_thread`](/ref/atr/template.py) (_render_in_thread) uses `asyncio.to_thread` to execute Jinja2's synchronous `render` method.
## Forms
-HTML forms in ATR are handled by [Pydantic](https://docs.pydantic.dev/latest/) models accessed through our [`form`](/ref/atr/form.py) module. Each form is a class that inherits from [`form.Form`](/ref/atr/form.py:Form), which itself inherits from `pydantic.BaseModel`. Form fields are defined as class attributes using Pydantic type annotations, with the [`form.label`](/ref/atr/form.py:label) function providing field metadata like labels and documentation.
+HTML forms in ATR are handled by [Pydantic](https://docs.pydantic.dev/latest/) models accessed through our [`form`](/ref/atr/form.py) module. Each form is a class that inherits from [`form.Form`](/ref/atr/form.py) (Form), which itself inherits from `pydantic.BaseModel`. Form fields are defined as class attributes using Pydantic type annotations, with the [`form.label`](/ref/atr/form.py) (label) function providing field metadata like labels and documentation.
Here is a typical form definition from [`shared/keys.py`](/ref/atr/shared/keys.py):
@@ -71,7 +71,7 @@ class AddOpenPGPKeyForm(form.Form):
### Field types and labels
-The [`form.label`](/ref/atr/form.py:label) function is used to add metadata to Pydantic fields. The first argument is the label text, the second (optional) argument is documentation text that appears below the field, and you can pass additional keyword arguments like `widget=form.Widget.TEXTAREA` to specify the HTML widget type.
+The [`form.label`](/ref/atr/form.py) (label) function is used to add metadata to Pydantic fields. The first argument is the label text, the second (optional) argument is documentation text that appears below the field, and you can pass additional keyword arguments like `widget=form.Widget.TEXTAREA` to specify the HTML widget type.
Fields use Pydantic type annotations to define their data type:
@@ -90,32 +90,32 @@ Fields use Pydantic type annotations to define their data type:
Empty values for fields are allowed by default in most cases, but URL is an exception.
-The `widget` parameter in [`form.label`](/ref/atr/form.py:label) lets you override the default widget for a field type. Available widgets include: `TEXTAREA`, `CHECKBOXES`, `SELECT`, `RADIO`, `HIDDEN`, and others from the `form.Widget` enum. Common reasons to override:
+The `widget` parameter in [`form.label`](/ref/atr/form.py) (label) lets you override the default widget for a field type. Available widgets include: `TEXTAREA`, `CHECKBOXES`, `SELECT`, `RADIO`, `HIDDEN`, and others from the `form.Widget` enum. Common reasons to override:
* HIDDEN: for values passed from the route, not entered by the user
* TEXTAREA: for multi-line text input
* RADIO: for mutually exclusive choices
* CUSTOM: for fully custom rendering
-From [`projects.AddProjectForm`](/ref/atr/shared/projects.py:AddProjectForm):
+From [`projects.AddProjectForm`](/ref/atr/shared/projects.py) (AddProjectForm):
```python
committee_key: str = form.label("Committee name", widget=form.Widget.HIDDEN)
```
-From [`resolve.SubmitForm`](/ref/atr/shared/resolve.py:SubmitForm):
+From [`resolve.SubmitForm`](/ref/atr/shared/resolve.py) (SubmitForm):
```python
email_body: str = form.label("Email body", widget=form.Widget.TEXTAREA)
```
-From [`resolve.SubmitForm`](/ref/atr/shared/resolve.py:SubmitForm):
+From [`resolve.SubmitForm`](/ref/atr/shared/resolve.py) (SubmitForm):
```python
vote_result: Literal["Passed", "Failed"] = form.label("Vote result", widget=form.Widget.RADIO)
```
-From [`vote.CastVoteForm`](/ref/atr/shared/vote.py:CastVoteForm):
+From [`vote.CastVoteForm`](/ref/atr/shared/vote.py) (CastVoteForm):
```python
decision: Literal["+1", "0", "-1"] = form.label("Your vote", widget=form.Widget.CUSTOM)
@@ -123,7 +123,7 @@ decision: Literal["+1", "0", "-1"] = form.label("Your vote", widget=form.Widget.
### Using forms in routes
-To use a form in a route, use the [`@post.committer()`](/ref/atr/blueprints/post.py:committer) decorator to get the session and auth the user, and the [`@post.form()`](/ref/atr/blueprints/post.py:form) decorator to parse and validate input data:
+To use a form in a route, use the [`@post.committer()`](/ref/atr/blueprints/post.py) (committer) decorator to get the session and auth the user, and the [`@post.form()`](/ref/atr/blueprints/post.py) (form) decorator to parse and validate input data:
```python
@post.committer("/keys/add")
@@ -148,13 +148,13 @@ async def add(session: web.Committer, add_openpgp_key_form: shared.keys.AddOpenP
return await session.redirect(get.keys.keys)
```
-The [`form.validate`](/ref/atr/form.py:validate) function should only be called manually when the request comes from JavaScript, as in [`body_preview`](/ref/atr/post/voting.py:body_preview). It takes the form class, the form data dictionary, and an optional context dictionary. If validation succeeds, it returns an instance of your form class with validated data. If validation fails, it raises a `pydantic.ValidationError`.
+The [`form.validate`](/ref/atr/form.py) (validate) function should only be called manually when the request comes from JavaScript, as in [`body_preview`](/ref/atr/post/voting.py) (body_preview). It takes the form class, the form data dictionary, and an optional context dictionary. If validation succeeds, it returns an instance of your form class with validated data. If validation fails, it raises a `pydantic.ValidationError`.
-The error handling uses [`form.flash_error_data`](/ref/atr/form.py:flash_error_data) to prepare error information for display, and [`form.flash_error_summary`](/ref/atr/form.py:flash_error_summary) to create a user-friendly summary of all validation errors.
+The error handling uses [`form.flash_error_data`](/ref/atr/form.py) (flash_error_data) to prepare error information for display, and [`form.flash_error_summary`](/ref/atr/form.py) (flash_error_summary) to create a user-friendly summary of all validation errors.
### Rendering forms
-The `form` module provides the [`form.render`](/ref/atr/form.py:render) function (or [`form.render_block`](/ref/atr/form.py:render_block) for use with [`htm.Block`](/ref/atr/htm.py:Block)) that generates Bootstrap-styled HTML. This function creates a two-column layout with labels on the left and inputs on the right:
+The `form` module provides the [`form.render`](/ref/atr/form.py) (render) function (or [`form.render_block`](/ref/atr/form.py) (render_block) for use with [`htm.Block`](/ref/atr/htm.py) (Block)) that generates Bootstrap-styled HTML. This function creates a two-column layout with labels on the left and inputs on the right:
```python
form.render_block(
@@ -202,7 +202,7 @@ The htpy library provides type annotations for HTML elements. It does not valida
## The htm.Block class
-The ATR [`htm`](/ref/atr/htm.py) module extends htpy with a [`Block`](/ref/atr/htm.py:Block) class that makes it easier to build complex HTML structures incrementally. You create a block, append elements to it, and then collect them into a final element. Here is the typical usage pattern:
+The ATR [`htm`](/ref/atr/htm.py) module extends htpy with a [`Block`](/ref/atr/htm.py) (Block) class that makes it easier to build complex HTML structures incrementally. You create a block, append elements to it, and then collect them into a final element. Here is the typical usage pattern:
```python
import atr.htm as htm
@@ -215,17 +215,17 @@ if release.released:
return div.collect()
```
-The block class provides properties for common HTML elements like `h1`, `h2`, `p`, `div`, `ul`, and so on. When you access these properties, you get back a [`BlockElementCallable`](/ref/atr/htm.py:BlockElementCallable), 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.
+The block class provides properties for common HTML elements like `h1`, `h2`, `p`, `div`, `ul`, and so on. When you access these properties, you get back a [`BlockElementCallable`](/ref/atr/htm.py) (BlockElementCallable), 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.
The `collect` method assembles all of the elements into a single htpy element. If you created the block with an outer element like `htm.Block(htpy.div(".container"))`, that element wraps all the children. If you created the block with no outer element, `collect` wraps everything in a div. You can also pass a `separator` argument to `collect`, which inserts a text separator between elements.
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.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).
+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, builds HTML using htpy, and renders it using a template. GET and POST requests are handled by separate routes, with form validation automatically handled by the [`@post.form()`](/ref/atr/blueprints/post.py:form) decorator. Here is a simplified example from [`get/keys.py`](/ref/atr/get/keys.py:add):
+A typical route that renders UI first authenticates the user, loads data from the database, builds HTML using htpy, and renders it using a template. GET and POST requests are handled by separate routes, with form validation automatically handled by the [`@post.form()`](/ref/atr/blueprints/post.py) (form) decorator. Here is a simplified example from [`get/keys.py`](/ref/atr/get/keys.py) (add):
```python
@get.typed
@@ -264,12 +264,12 @@ async def add(session: web.Committer, _keys_add: Literal["keys/add"]) -> str:
)
```
-The route is decorated with [`@get.typed`](/ref/atr/blueprints/get.py:typed), which handles authentication and provides a `session` object that is an instance of [`web.Committer`](/ref/atr/web.py:Committer) with a range of useful properties and methods.
-[`@get.typed`](/ref/atr/blueprints/get.py:typed) also validates `project_name` and `version_name` route params, and checks that the user is authorised to access the project (if a project name is required).
+The route is decorated with [`@get.typed`](/ref/atr/blueprints/get.py) (typed), which handles authentication and provides a `session` object that is an instance of [`web.Committer`](/ref/atr/web.py) (Committer) with a range of useful properties and methods.
+[`@get.typed`](/ref/atr/blueprints/get.py) (typed) also validates `project_name` and `version_name` route params, and checks that the user is authorised to access the project (if a project name is required).
-The function builds the UI using an [`htm.Block`](/ref/atr/htm.py:Block) object, which provides a convenient API for incrementally building HTML. The form is rendered directly into the block using [`form.render_block()`](/ref/atr/form.py:render_block), which generates all the necessary HTML with Bootstrap styling.
+The function builds the UI using an [`htm.Block`](/ref/atr/htm.py) (Block) object, which provides a convenient API for incrementally building HTML. The form is rendered directly into the block using [`form.render_block()`](/ref/atr/form.py) (render_block), which generates all the necessary HTML with Bootstrap styling.
-Finally, the route returns the rendered HTML using [`template.blank()`](/ref/atr/template.py:blank), which renders a minimal template with just a title and content area.
+Finally, the route returns the rendered HTML using [`template.blank()`](/ref/atr/template.py) (blank), which renders a minimal template with just a title and content area.
Form submission is handled by a separate POST route:
@@ -291,6 +291,6 @@ async def add(session: web.Committer, add_openpgp_key_form: shared.keys.AddOpenP
return await session.redirect(get.keys.keys)
```
-The [`@post.form()`](/ref/atr/blueprints/post.py:form) decorator handles form validation automatically. If validation fails, it flashes error messages and redirects back to the GET route. If validation succeeds, the validated form instance is injected into the route handler as a parameter.
+The [`@post.form()`](/ref/atr/blueprints/post.py) (form) decorator handles form validation automatically. If validation fails, it flashes error messages and redirects back to the GET route. If validation succeeds, the validated form instance is injected into the route handler as a parameter.
Bootstrap CSS classes are applied automatically by the form rendering functions. The functions use classes like `form-control`, `form-select`, `btn-primary`, `is-invalid`, and `invalid-feedback`. 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 `htpy.div(".container")` or the class attribute like `htpy.div(class_="container")`.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org