This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch taint_tracking_types
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 4b0213c8f5217ce0ce5afc4ae374431e6558f952
Author: Alastair McFarlane <[email protected]>
AuthorDate: Wed Feb 25 16:46:29 2026 +0000

    First cut of taint tracking types for project and version
---
 atr/blueprints/get.py | 32 ++++++++++++++++++++++----------
 atr/construct.py      | 25 ++++++++++++-------------
 atr/db/__init__.py    |  5 ++---
 atr/db/interaction.py |  9 +++------
 atr/get/announce.py   | 21 ++++++++++++---------
 atr/get/checklist.py  | 18 +++++++++---------
 atr/get/checks.py     | 29 +++++++++++++++++------------
 atr/get/compose.py    | 11 +++++++----
 atr/web.py            | 10 ++++++----
 9 files changed, 90 insertions(+), 70 deletions(-)

diff --git a/atr/blueprints/get.py b/atr/blueprints/get.py
index 1af5d8e4..707320bd 100644
--- a/atr/blueprints/get.py
+++ b/atr/blueprints/get.py
@@ -49,6 +49,14 @@ async def _authenticate() -> web.Committer:
     return web.Committer(web_session)
 
 
+async def _authenticate_public() -> web.Public:
+    web_session = await asfquart.session.read()
+    if web_session is None:
+        return None
+    else:
+        return await _authenticate()
+
+
 def _register(func: Callable[..., Any]) -> None:
     module_name = func.__module__.split(".")[-1]
     _routes.append(f"get.{module_name}.{func.__name__}")
@@ -87,25 +95,29 @@ _QUART_CONVERTERS: dict[type, str] = {
 _VALIDATED_TYPES: set[Any] = {safe.ProjectName, safe.VersionName}
 
 
-def _build_path(func: Callable[..., Any]) -> tuple[str, list[tuple[str, 
type]], dict[str, str]]:
+def _build_path(func: Callable[..., Any]) -> tuple[str, list[tuple[str, 
type]], dict[str, str], bool]:
     """Inspect a function's type hints to build a URL path and a validation 
plan.
 
-    Returns (path, validated_params, literal_params) where validated_params is 
a
+    Returns (path, validated_params, literal_params, public) where 
validated_params is a
     list of (param_name, param_type) for each parameter that needs async
-    validation, and literal_params maps parameter names to their values.
+    validation, literal_params maps parameter names to their values, and 
public is whether the route is public or not
     """
     hints = get_type_hints(func, include_extras=True)
     params = list(inspect.signature(func).parameters.keys())
+    public = False
     segments: list[str] = []
     validated_params: list[tuple[str, type]] = []
     literal_params: dict[str, str] = {}
 
-    for param_name in params:
-        # This is the session object
+    for ix, param_name in enumerate(params):
+        hint = hints.get(param_name)
+
+        # This is the session object, which should be first
         if param_name == "session":
+            if ix != 0:
+                raise TypeError(f"Parameter {param_name!r} in {func.__name__} 
must be first")
+            public = hint is web.Public
             continue
-
-        hint = hints.get(param_name)
         if hint is None:
             raise TypeError(f"Parameter {param_name!r} in {func.__name__} has 
no type annotation")
 
@@ -127,7 +139,7 @@ def _build_path(func: Callable[..., Any]) -> tuple[str, 
list[tuple[str, type]],
             raise TypeError(f"Parameter {param_name!r} in {func.__name__} has 
unsupported type {hint!r}")
 
     path = "/" + "/".join(segments)
-    return path, validated_params, literal_params
+    return path, validated_params, literal_params, public
 
 
 async def _run_validators(kwargs: dict[str, Any], validated_params: 
list[tuple[str, type]]) -> None:
@@ -149,10 +161,10 @@ def typed(func: Callable[..., Any]) -> 
web.RouteFunction[Any]:
     - int, float use Quart's built-in type converters
     - str parameters pass through as-is
     """
-    path, validated_params, literal_params = _build_path(func)
+    path, validated_params, literal_params, public = _build_path(func)
 
     async def wrapper(*_args: Any, **kwargs: Any) -> Any:
-        enhanced_session = await _authenticate()
+        enhanced_session = await _authenticate() if public else None
         await _run_validators(kwargs, validated_params)
         kwargs.update(literal_params)
 
diff --git a/atr/construct.py b/atr/construct.py
index 6a639f47..f36ba1a6 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -25,7 +25,6 @@ import quart
 
 import atr.config as config
 import atr.db as db
-import atr.models.safe as safe
 import atr.models.sql as sql
 import atr.paths as paths
 import atr.util as util
@@ -54,8 +53,8 @@ TEMPLATE_VARIABLES: list[tuple[str, str, set[Context]]] = [
 class AnnounceReleaseOptions:
     asfuid: str
     fullname: str
-    project_name: safe.ProjectName
-    version_name: safe.VersionName
+    project_name: str
+    version_name: str
     revision_number: str
 
 
@@ -69,11 +68,11 @@ class StartVoteOptions:
     vote_duration: int
 
 
-async def announce_release_default(project_name: safe.ProjectName) -> str:
+async def announce_release_default(project_name: str) -> str:
     async with db.session() as data:
-        project = await data.project(
-            name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_release_policy=True
-        ).demand(RuntimeError(f"Project {project_name} not found"))
+        project = await data.project(name=project_name, 
status=sql.ProjectStatus.ACTIVE, _release_policy=True).demand(
+            RuntimeError(f"Project {project_name} not found")
+        )
 
     return project.policy_announce_release_template
 
@@ -132,11 +131,11 @@ async def announce_release_subject_and_body(
     return subject, body
 
 
-async def announce_release_subject_default(project_name: safe.ProjectName) -> 
str:
+async def announce_release_subject_default(project_name: str) -> str:
     async with db.session() as data:
-        project = await data.project(
-            name=str(project_name), status=sql.ProjectStatus.ACTIVE, 
_release_policy=True
-        ).demand(RuntimeError(f"Project {project_name} not found"))
+        project = await data.project(name=project_name, 
status=sql.ProjectStatus.ACTIVE, _release_policy=True).demand(
+            RuntimeError(f"Project {project_name} not found")
+        )
 
     return project.policy_announce_release_subject
 
@@ -152,7 +151,7 @@ def announce_template_variables() -> list[tuple[str, str]]:
 def checklist_body(
     markdown: str,
     project: sql.Project,
-    version_name: safe.VersionName,
+    version_name: str,
     committee: sql.Committee,
     revision: sql.Revision | None,
 ) -> str:
@@ -173,7 +172,7 @@ def checklist_body(
     markdown = markdown.replace("{{REVIEW_URL}}", review_url)
     markdown = markdown.replace("{{REVISION}}", revision_number)
     markdown = markdown.replace("{{TAG}}", revision_tag)
-    markdown = markdown.replace("{{VERSION}}", str(version_name))
+    markdown = markdown.replace("{{VERSION}}", version_name)
     return markdown
 
 
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index a4c62e3a..97b1f967 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -34,7 +34,6 @@ import sqlmodel.sql.expression as expression
 
 import atr.config as config
 import atr.log as log
-import atr.models.safe as safe
 import atr.models.schema as schema
 import atr.models.sql as sql
 import atr.util as util
@@ -479,9 +478,9 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         name: Opt[str] = NOT_SET,
         phase: Opt[sql.ReleasePhase] = NOT_SET,
         created: Opt[datetime.datetime] = NOT_SET,
-        project_name: Opt[safe.ProjectName] = NOT_SET,
+        project_name: Opt[str] = NOT_SET,
         package_managers: Opt[list[str]] = NOT_SET,
-        version: Opt[safe.VersionName] = NOT_SET,
+        version: Opt[str] = NOT_SET,
         sboms: Opt[list[str]] = NOT_SET,
         release_policy_id: Opt[int] = NOT_SET,
         votes: Opt[list[sql.VoteEntry]] = NOT_SET,
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 3213bc74..6e7e17a5 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -32,7 +32,6 @@ import atr.jwtoken as jwtoken
 import atr.ldap as ldap
 import atr.log as log
 import atr.models.results as results
-import atr.models.safe as safe
 import atr.models.sql as sql
 import atr.user as user
 import atr.util as util
@@ -392,14 +391,12 @@ def task_recipient_get(latest_vote_task: sql.Task) -> str 
| None:
     return result.email_to
 
 
-async def tasks_ongoing(
-    project_name: safe.ProjectName, version_name: safe.VersionName, 
revision_number: str | None = None
-) -> int:
+async def tasks_ongoing(project_name: str, version_name: str, revision_number: 
str | None = None) -> int:
     tasks = sqlmodel.select(sqlalchemy.func.count()).select_from(sql.Task)
     async with db.session() as data:
         query = tasks.where(
-            sql.Task.project_name == str(project_name),
-            sql.Task.version_name == str(version_name),
+            sql.Task.project_name == project_name,
+            sql.Task.version_name == version_name,
             sql.Task.revision_number
             == (sql.RELEASE_LATEST_REVISION_NUMBER if (revision_number is 
None) else revision_number),
             
sql.validate_instrumented_attribute(sql.Task.status).in_([sql.TaskStatus.QUEUED,
 sql.TaskStatus.ACTIVE]),
diff --git a/atr/get/announce.py b/atr/get/announce.py
index 1f41617c..1999c05a 100644
--- a/atr/get/announce.py
+++ b/atr/get/announce.py
@@ -38,13 +38,16 @@ import atr.web as web
 
 @get.typed
 async def selected(
-    _announce: Literal["announce"],
     session: web.Committer,
+    _announce: Literal["announce"],
     project_name: safe.ProjectName,
     version_name: safe.VersionName,
 ) -> str | web.WerkzeugResponse:
-    """Allow the user to announce a release preview."""
-    await session.check_access(project_name)
+    """
+    URL: /announce/<project_name>/<version_name>
+    Allow the user to announce a release preview.
+    """
+    await session.check_access(str(project_name))
 
     release = await _get_page_data(session, project_name, version_name)
 
@@ -57,16 +60,16 @@ async def selected(
         )
 
     # Get the templates from the release policy
-    default_subject_template = await 
construct.announce_release_subject_default(project_name)
-    default_body_template = await 
construct.announce_release_default(project_name)
+    default_subject_template = await 
construct.announce_release_subject_default(str(project_name))
+    default_body_template = await 
construct.announce_release_default(str(project_name))
     subject_template_hash = construct.template_hash(default_subject_template)
 
     # Expand the templates
     options = construct.AnnounceReleaseOptions(
         asfuid=session.uid,
         fullname=session.fullname,
-        project_name=project_name,
-        version_name=version_name,
+        project_name=str(project_name),
+        version_name=str(version_name),
         revision_number=latest_revision_number,
     )
     default_subject, default_body = await 
construct.announce_release_subject_and_body(
@@ -112,8 +115,8 @@ async def _get_page_data(
     session: web.Committer, project_name: safe.ProjectName, version_name: 
safe.VersionName
 ) -> sql.Release:
     release = await session.release(
-        project_name,
-        version_name,
+        str(project_name),
+        str(version_name),
         with_committee=True,
         phase=sql.ReleasePhase.RELEASE_PREVIEW,
         with_distributions=True,
diff --git a/atr/get/checklist.py b/atr/get/checklist.py
index f1c4ddb1..3153114e 100644
--- a/atr/get/checklist.py
+++ b/atr/get/checklist.py
@@ -35,15 +35,15 @@ import atr.web as web
 
 @get.typed
 async def selected(
-    session: web.Committer | None,
+    session: web.Public,
     _checklist: Literal["checklist"],
     project_name: safe.ProjectName,
-    version_name: safe.VersionName
+    version_name: safe.VersionName,
 ) -> str:
     async with db.session() as data:
         release = await data.release(
-            project_name=project_name,
-            version=version_name,
+            project_name=str(project_name),
+            version=str(version_name),
             _project=True,
             _committee=True,
             _project_release_policy=True,
@@ -67,7 +67,7 @@ async def selected(
     substituted_markdown = construct.checklist_body(
         checklist_markdown,
         project=project,
-        version_name=version_name,
+        version_name=str(version_name),
         committee=committee,
         revision=latest_revision,
     )
@@ -76,8 +76,8 @@ async def selected(
     page = htm.Block()
     render.html_nav(
         page,
-        back_url=util.as_url(vote.selected, project_name=project_name, 
version_name=version_name),
-        back_anchor=f"Vote on {project.short_display_name} {version_name}",
+        back_url=util.as_url(vote.selected, project_name=str(project_name), 
version_name=str(version_name)),
+        back_anchor=f"Vote on {project.short_display_name} {version_name!s}",
         phase="VOTE",
     )
     page.h1["Release checklist"]
@@ -85,12 +85,12 @@ async def selected(
         "Checklist for ",
         htm.strong[project.short_display_name],
         " version ",
-        version_name,
+        str(version_name),
         ":",
     ]
     page.div(".checklist-content.mt-4")[checklist_html]
 
     return await template.blank(
-        title=f"Release checklist for {project.short_display_name} 
{version_name}",
+        title=f"Release checklist for {project.short_display_name} 
{version_name!s}",
         content=page.collect(),
     )
diff --git a/atr/get/checks.py b/atr/get/checks.py
index 820ca1ea..a989bf1b 100644
--- a/atr/get/checks.py
+++ b/atr/get/checks.py
@@ -100,16 +100,16 @@ async def get_file_totals(release: sql.Release, session: 
web.Committer | None) -
 
 @get.typed
 async def selected(
-    session: web.Committer | None,
-    _checks: Literal["checks"],
-    project_name: safe.ProjectName,
-    version_name: safe.VersionName
+    session: web.Public, _checks: Literal["checks"], project_name: 
safe.ProjectName, version_name: safe.VersionName
 ) -> str:
-    """Show the file checks for a release candidate."""
+    """
+    URL: /checks/<project_name>/<version_name>
+    Show the file checks for a release candidate.
+    """
     async with db.session() as data:
         release = await data.release(
-            project_name=project_name,
-            version=version_name,
+            project_name=str(project_name),
+            version=str(version_name),
             phase=sql.ReleasePhase.RELEASE_CANDIDATE,
             _committee=True,
         ).demand(base.ASFQuartException("Release does not exist", 
errorcode=404))
@@ -148,11 +148,14 @@ async def selected_revision(
     version_name: safe.VersionName,
     revision_number: str,
 ) -> web.QuartResponse:
-    """Return JSON with ongoing count and HTML fragments for dynamic 
updates."""
+    """
+    URL: /checks/<project_name>/<version_name>/<revision_number>
+    Return JSON with ongoing count and HTML fragments for dynamic updates.
+    """
     async with db.session() as data:
         release = await data.release(
-            project_name=project_name,
-            version=version_name,
+            project_name=str(project_name),
+            version=str(version_name),
             _committee=True,
             # _project=True is included in _project_release_policy=True
             _project_release_policy=True,
@@ -166,7 +169,7 @@ async def selected_revision(
         ragp = read.as_general_public()
         info = await ragp.releases.path_info(release, all_paths)
 
-    ongoing_count = await interaction.tasks_ongoing(project_name, 
version_name, revision_number)
+    ongoing_count = await interaction.tasks_ongoing(str(project_name), 
str(version_name), revision_number)
 
     checks_summary_elem = shared.web.render_checks_summary(info, project_name, 
version_name)
     checks_summary_html = str(checks_summary_elem) if checks_summary_elem else 
""
@@ -177,7 +180,9 @@ async def selected_revision(
             delete_file_forms[str(path)] = str(
                 form.render(
                     model_cls=draft.DeleteFileForm,
-                    action=util.as_url(post.draft.delete_file, 
project_name=str(project_name), version_name=str(version_name)),
+                    action=util.as_url(
+                        post.draft.delete_file, 
project_name=str(project_name), version_name=str(version_name)
+                    ),
                     form_classes=".d-inline-block.m-0",
                     submit_classes="btn-sm btn-outline-danger",
                     submit_label="Delete",
diff --git a/atr/get/compose.py b/atr/get/compose.py
index cd7a1a71..abe3e27a 100644
--- a/atr/get/compose.py
+++ b/atr/get/compose.py
@@ -35,13 +35,16 @@ async def selected(
     project_name: safe.ProjectName,
     version_name: safe.VersionName,
 ) -> web.WerkzeugResponse | str:
-    """Show the contents of the release candidate draft."""
-    await session.check_access(project_name)
+    """
+    URL: /compose/<project_name>/<version_name>
+    Show the contents of the release candidate draft.
+    """
+    await session.check_access(str(project_name))
 
     async with db.session() as data:
         release = await data.release(
-            project_name=project_name,
-            version=version_name,
+            project_name=str(project_name),
+            version=str(version_name),
             _committee=True,
             _project_release_policy=True,
         ).demand(base.ASFQuartException("Release does not exist", 
errorcode=404))
diff --git a/atr/web.py b/atr/web.py
index 2a2834f4..e7a999f5 100644
--- a/atr/web.py
+++ b/atr/web.py
@@ -32,7 +32,6 @@ import atr.config as config
 import atr.db as db
 import atr.form as form
 import atr.htm as htm
-import atr.models.safe as safe
 import atr.models.sql as sql
 import atr.user as user
 import atr.util as util
@@ -88,7 +87,7 @@ class Committer:
     def is_admin(self) -> bool:
         return user.is_admin(self.uid)
 
-    async def check_access(self, project_name: safe.ProjectName) -> None:
+    async def check_access(self, project_name: str) -> None:
         if not any((p.name == str(project_name)) for p in (await 
self.user_projects)):
             if self.is_admin:
                 # Admins can view all projects
@@ -162,8 +161,8 @@ class Committer:
 
     async def release(
         self,
-        project_name: safe.ProjectName,
-        version_name: safe.VersionName,
+        project_name: str,
+        version_name: str,
         phase: sql.ReleasePhase | db.NotSet | None = db.NOT_SET,
         latest_revision_number: str | db.NotSet | None = db.NOT_SET,
         data: db.Session | None = None,
@@ -258,6 +257,9 @@ class HeaderValue:
         return self.__value
 
 
+type Public = Committer | None
+
+
 class RouteFunction(Protocol[R]):
     """Protocol for @app_route decorated functions."""
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to