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]
