This is an automated email from the ASF dual-hosted git repository.
tn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new a36122f introduce a NotSet type to indicate the absence of a value,
adapt session interface to use typed arguments using Opt
a36122f is described below
commit a36122fb86c891dcc95be8a70961bd0306015487
Author: Thomas Neidhart <[email protected]>
AuthorDate: Thu Mar 20 09:00:03 2025 +0100
introduce a NotSet type to indicate the absence of a value, adapt session
interface to use typed arguments using Opt
---
atr/blueprints/admin/admin.py | 4 +-
atr/db/__init__.py | 288 +++++++++++++++++++++++-------------------
atr/routes/candidate.py | 2 +-
atr/routes/keys.py | 11 +-
atr/routes/package.py | 2 +-
atr/routes/projects.py | 2 +-
6 files changed, 172 insertions(+), 137 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index e103b86..120a6bd 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -32,6 +32,7 @@ import atr.blueprints.admin as admin
import atr.datasources.apache as apache
import atr.db as db
import atr.db.models as models
+import atr.util as util
@admin.BLUEPRINT.route("/performance")
@@ -280,12 +281,13 @@ async def admin_keys_delete_all() -> str:
web_session = await session.read()
if web_session is None:
raise base.ASFQuartException("Not authenticated", errorcode=401)
+ uid = util.unwrap(web_session.uid)
async with db.session() as data:
async with data.begin():
# Get all keys for the user
# TODO: Use session.apache_uid instead of session.uid?
- keys = await
data.public_signing_key(apache_uid=web_session.uid).all()
+ keys = await data.public_signing_key(apache_uid=uid).all()
count = len(keys)
# Delete all keys
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 49e5448..b3478b1 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import os
-from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar
+from typing import TYPE_CHECKING, Any, Final, Generic, TypeGuard, TypeVar
import quart
import sqlalchemy
@@ -35,9 +35,12 @@ import atr.util as util
if TYPE_CHECKING:
from collections.abc import Sequence
+ from datetime import datetime
import asfquart.base as base
+ from atr.db.models import ReleasePhase, ReleaseStage, TaskStatus, VoteEntry
+
_LOGGER: Final = logging.getLogger(__name__)
_global_async_sessionmaker: sqlalchemy.ext.asyncio.async_sessionmaker | None =
None
@@ -45,13 +48,42 @@ _global_atr_sessionmaker:
sqlalchemy.ext.asyncio.async_sessionmaker | None = Non
_global_sync_engine: sqlalchemy.Engine | None = None
-class _DefaultArgument: ...
+T = TypeVar("T")
-_DEFAULT: Final = _DefaultArgument()
+class _NotSetType:
+ """
+ A marker class to indicate that a value is not set and thus should
+ not be considered. This is different to None.
+ """
+ _instance = None
-T = TypeVar("T")
+ def __new__(cls): # type: ignore
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ return cls._instance
+
+ def __repr__(self) -> str:
+ return "<NotSet>"
+
+ def __copy__(self): # type: ignore
+ return NotSet
+
+ def __deepcopy__(self, memo: dict[int, Any]): # type: ignore
+ return NotSet
+
+
+NotSet = _NotSetType()
+type Opt[T] = T | _NotSetType
+
+
+def is_defined(v: T | _NotSetType) -> TypeGuard[T]:
+ return not isinstance(v, _NotSetType)
+
+
+def is_undefined(v: T | _NotSetType) -> TypeGuard[_NotSetType]: # pyright:
ignore [reportInvalidTypeVarUse]
+ return isinstance(v, _NotSetType)
class Query(Generic[T]):
@@ -82,41 +114,41 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
# TODO: Need to type all of these arguments correctly
def committee(
self,
- id: Any = _DEFAULT,
- name: Any = _DEFAULT,
- full_name: Any = _DEFAULT,
- is_podling: Any = _DEFAULT,
- parent_committee_id: Any = _DEFAULT,
- committee_members: Any = _DEFAULT,
- committers: Any = _DEFAULT,
- release_managers: Any = _DEFAULT,
- vote_policy_id: Any = _DEFAULT,
- name_in: list[str] | _DefaultArgument = _DEFAULT,
+ id: Opt[int] = NotSet,
+ name: Opt[str] = NotSet,
+ full_name: Opt[str] = NotSet,
+ is_podling: Opt[bool] = NotSet,
+ parent_committee_id: Opt[int] = NotSet,
+ committee_members: Opt[list[str]] = NotSet,
+ committers: Opt[list[str]] = NotSet,
+ release_managers: Opt[list[str]] = NotSet,
+ vote_policy_id: Opt[int] = NotSet,
+ name_in: Opt[list[str]] = NotSet,
_public_signing_keys: bool = False,
_vote_policy: bool = False,
) -> Query[models.Committee]:
query = sqlmodel.select(models.Committee)
- if id is not _DEFAULT:
+ if is_defined(id):
query = query.where(models.Committee.id == id)
- if name is not _DEFAULT:
+ if is_defined(name):
query = query.where(models.Committee.name == name)
- if full_name is not _DEFAULT:
+ if is_defined(full_name):
query = query.where(models.Committee.full_name == full_name)
- if is_podling is not _DEFAULT:
+ if is_defined(is_podling):
query = query.where(models.Committee.is_podling == is_podling)
- if parent_committee_id is not _DEFAULT:
+ if is_defined(parent_committee_id):
query = query.where(models.Committee.parent_committee_id ==
parent_committee_id)
- if committee_members is not _DEFAULT:
+ if is_defined(committee_members):
query = query.where(models.Committee.committee_members ==
committee_members)
- if committers is not _DEFAULT:
+ if is_defined(committers):
query = query.where(models.Committee.committers == committers)
- if release_managers is not _DEFAULT:
+ if is_defined(release_managers):
query = query.where(models.Committee.release_managers ==
release_managers)
- if vote_policy_id is not _DEFAULT:
+ if is_defined(vote_policy_id):
query = query.where(models.Committee.vote_policy_id ==
vote_policy_id)
- if not isinstance(name_in, _DefaultArgument):
+ if is_defined(name_in):
models_committee_name =
validate_instrumented_attribute(models.Committee.name)
query = query.where(models_committee_name.in_(name_in))
@@ -129,14 +161,14 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
def package(
self,
- artifact_sha3: Any = _DEFAULT,
- artifact_type: Any = _DEFAULT,
- filename: Any = _DEFAULT,
- sha512: Any = _DEFAULT,
- signature_sha3: Any = _DEFAULT,
- uploaded: Any = _DEFAULT,
- bytes_size: Any = _DEFAULT,
- release_key: Any = _DEFAULT,
+ artifact_sha3: Opt[str] = NotSet,
+ artifact_type: Opt[str] = NotSet,
+ filename: Opt[str] = NotSet,
+ sha512: Opt[str] = NotSet,
+ signature_sha3: Opt[str] = NotSet,
+ uploaded: Opt[bool] = NotSet,
+ bytes_size: Opt[int] = NotSet,
+ release_key: Opt[str] = NotSet,
_release: bool = False,
_tasks: bool = False,
_release_project: bool = False,
@@ -144,21 +176,21 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
) -> Query[models.Package]:
query = sqlmodel.select(models.Package)
- if artifact_sha3 is not _DEFAULT:
+ if is_defined(artifact_sha3):
query = query.where(models.Package.artifact_sha3 == artifact_sha3)
- if artifact_type is not _DEFAULT:
+ if is_defined(artifact_type):
query = query.where(models.Package.artifact_type == artifact_type)
- if filename is not _DEFAULT:
+ if is_defined(filename):
query = query.where(models.Package.filename == filename)
- if sha512 is not _DEFAULT:
+ if is_defined(sha512):
query = query.where(models.Package.sha512 == sha512)
- if signature_sha3 is not _DEFAULT:
+ if is_defined(signature_sha3):
query = query.where(models.Package.signature_sha3 ==
signature_sha3)
- if uploaded is not _DEFAULT:
+ if is_defined(uploaded):
query = query.where(models.Package.uploaded == uploaded)
- if bytes_size is not _DEFAULT:
+ if is_defined(bytes_size):
query = query.where(models.Package.bytes_size == bytes_size)
- if release_key is not _DEFAULT:
+ if is_defined(release_key):
query = query.where(models.Package.release_key == release_key)
if _release:
query = query.options(select_in_load(models.Package.release))
@@ -174,12 +206,12 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
def project(
self,
- id: Any = _DEFAULT,
- name: Any = _DEFAULT,
- full_name: Any = _DEFAULT,
- is_podling: Any = _DEFAULT,
- committee_id: Any = _DEFAULT,
- vote_policy_id: Any = _DEFAULT,
+ id: Opt[int] = NotSet,
+ name: Opt[str] = NotSet,
+ full_name: Opt[str] = NotSet,
+ is_podling: Opt[bool] = NotSet,
+ committee_id: Opt[int] = NotSet,
+ vote_policy_id: Opt[int] = NotSet,
_committee: bool = False,
_releases: bool = False,
_distribution_channels: bool = False,
@@ -188,17 +220,17 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
) -> Query[models.Project]:
query = sqlmodel.select(models.Project)
- if id is not _DEFAULT:
+ if is_defined(id):
query = query.where(models.Project.id == id)
- if name is not _DEFAULT:
+ if is_defined(name):
query = query.where(models.Project.name == name)
- if full_name is not _DEFAULT:
+ if is_defined(full_name):
query = query.where(models.Project.full_name == full_name)
- if is_podling is not _DEFAULT:
+ if is_defined(is_podling):
query = query.where(models.Project.is_podling == is_podling)
- if committee_id is not _DEFAULT:
+ if is_defined(committee_id):
query = query.where(models.Project.committee_id == committee_id)
- if vote_policy_id is not _DEFAULT:
+ if is_defined(vote_policy_id):
query = query.where(models.Project.vote_policy_id ==
vote_policy_id)
if _committee:
@@ -216,33 +248,33 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
def public_signing_key(
self,
- fingerprint: Any = _DEFAULT,
- algorithm: Any = _DEFAULT,
- length: Any = _DEFAULT,
- created: Any = _DEFAULT,
- expires: Any = _DEFAULT,
- declared_uid: Any = _DEFAULT,
- apache_uid: Any = _DEFAULT,
- ascii_armored_key: Any = _DEFAULT,
+ fingerprint: Opt[str] = NotSet,
+ algorithm: Opt[str] = NotSet,
+ length: Opt[int] = NotSet,
+ created: Opt[datetime] = NotSet,
+ expires: Opt[datetime | None] = NotSet,
+ declared_uid: Opt[str | None] = NotSet,
+ apache_uid: Opt[str] = NotSet,
+ ascii_armored_key: Opt[str] = NotSet,
_committees: bool = False,
) -> Query[models.PublicSigningKey]:
query = sqlmodel.select(models.PublicSigningKey)
- if fingerprint is not _DEFAULT:
+ if is_defined(fingerprint):
query = query.where(models.PublicSigningKey.fingerprint ==
fingerprint)
- if algorithm is not _DEFAULT:
+ if is_defined(algorithm):
query = query.where(models.PublicSigningKey.algorithm == algorithm)
- if length is not _DEFAULT:
+ if is_defined(length):
query = query.where(models.PublicSigningKey.length == length)
- if created is not _DEFAULT:
+ if is_defined(created):
query = query.where(models.PublicSigningKey.created == created)
- if expires is not _DEFAULT:
+ if is_defined(expires):
query = query.where(models.PublicSigningKey.expires == expires)
- if declared_uid is not _DEFAULT:
+ if is_defined(declared_uid):
query = query.where(models.PublicSigningKey.declared_uid ==
declared_uid)
- if apache_uid is not _DEFAULT:
+ if is_defined(apache_uid):
query = query.where(models.PublicSigningKey.apache_uid ==
apache_uid)
- if ascii_armored_key is not _DEFAULT:
+ if is_defined(ascii_armored_key):
query = query.where(models.PublicSigningKey.ascii_armored_key ==
ascii_armored_key)
if _committees:
@@ -252,16 +284,16 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
def release(
self,
- storage_key: Any = _DEFAULT,
- stage: Any = _DEFAULT,
- phase: Any = _DEFAULT,
- created: Any = _DEFAULT,
- project_id: Any = _DEFAULT,
- package_managers: Any = _DEFAULT,
- version: Any = _DEFAULT,
- sboms: Any = _DEFAULT,
- vote_policy_id: Any = _DEFAULT,
- votes: Any = _DEFAULT,
+ storage_key: Opt[str] = NotSet,
+ stage: Opt[ReleaseStage] = NotSet,
+ phase: Opt[ReleasePhase] = NotSet,
+ created: Opt[datetime] = NotSet,
+ project_id: Opt[int] = NotSet,
+ package_managers: Opt[list[str]] = NotSet,
+ version: Opt[str] = NotSet,
+ sboms: Opt[list[str]] = NotSet,
+ vote_policy_id: Opt[int] = NotSet,
+ votes: Opt[list[VoteEntry]] = NotSet,
_project: bool = False,
_packages: bool = False,
_vote_policy: bool = False,
@@ -270,25 +302,25 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
) -> Query[models.Release]:
query = sqlmodel.select(models.Release)
- if storage_key is not _DEFAULT:
+ if is_defined(storage_key):
query = query.where(models.Release.storage_key == storage_key)
- if stage is not _DEFAULT:
+ if is_defined(stage):
query = query.where(models.Release.stage == stage)
- if phase is not _DEFAULT:
+ if is_defined(phase):
query = query.where(models.Release.phase == phase)
- if created is not _DEFAULT:
+ if is_defined(created):
query = query.where(models.Release.created == created)
- if project_id is not _DEFAULT:
+ if is_defined(project_id):
query = query.where(models.Release.project_id == project_id)
- if package_managers is not _DEFAULT:
+ if is_defined(package_managers):
query = query.where(models.Release.package_managers ==
package_managers)
- if version is not _DEFAULT:
+ if is_defined(version):
query = query.where(models.Release.version == version)
- if sboms is not _DEFAULT:
+ if is_defined(sboms):
query = query.where(models.Release.sboms == sboms)
- if vote_policy_id is not _DEFAULT:
+ if is_defined(vote_policy_id):
query = query.where(models.Release.vote_policy_id ==
vote_policy_id)
- if votes is not _DEFAULT:
+ if is_defined(votes):
query = query.where(models.Release.votes == votes)
if _project:
@@ -306,60 +338,60 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
def ssh_key(
self,
- fingerprint: Any = _DEFAULT,
- key: Any = _DEFAULT,
- asf_uid: Any = _DEFAULT,
+ fingerprint: Opt[str] = NotSet,
+ key: Opt[str] = NotSet,
+ asf_uid: Opt[str] = NotSet,
) -> Query[models.SSHKey]:
query = sqlmodel.select(models.SSHKey)
- if fingerprint is not _DEFAULT:
+ if is_defined(fingerprint):
query = query.where(models.SSHKey.fingerprint == fingerprint)
- if key is not _DEFAULT:
+ if is_defined(key):
query = query.where(models.SSHKey.key == key)
- if asf_uid is not _DEFAULT:
+ if is_defined(asf_uid):
query = query.where(models.SSHKey.asf_uid == asf_uid)
return Query(self, query)
def task(
self,
- id: Any = _DEFAULT,
- status: Any = _DEFAULT,
- task_type: Any = _DEFAULT,
- task_args: Any = _DEFAULT,
- added: Any = _DEFAULT,
- started: Any = _DEFAULT,
- pid: Any = _DEFAULT,
- completed: Any = _DEFAULT,
- result: Any = _DEFAULT,
- error: Any = _DEFAULT,
- package_sha3: Any = _DEFAULT,
+ id: Opt[int] = NotSet,
+ status: Opt[TaskStatus] = NotSet,
+ task_type: Opt[str] = NotSet,
+ task_args: Opt[Any] = NotSet,
+ added: Opt[datetime] = NotSet,
+ started: Opt[datetime | None] = NotSet,
+ pid: Opt[int | None] = NotSet,
+ completed: Opt[datetime | None] = NotSet,
+ result: Opt[Any | None] = NotSet,
+ error: Opt[str | None] = NotSet,
+ package_sha3: Opt[str | None] = NotSet,
_package: bool = False,
_package_release: bool = False,
) -> Query[models.Task]:
query = sqlmodel.select(models.Task)
- if id is not _DEFAULT:
+ if is_defined(id):
query = query.where(models.Task.id == id)
- if status is not _DEFAULT:
+ if is_defined(status):
query = query.where(models.Task.status == status)
- if task_type is not _DEFAULT:
+ if is_defined(task_type):
query = query.where(models.Task.task_type == task_type)
- if task_args is not _DEFAULT:
+ if is_defined(task_args):
query = query.where(models.Task.task_args == task_args)
- if added is not _DEFAULT:
+ if is_defined(added):
query = query.where(models.Task.added == added)
- if started is not _DEFAULT:
+ if is_defined(started):
query = query.where(models.Task.started == started)
- if pid is not _DEFAULT:
+ if is_defined(pid):
query = query.where(models.Task.pid == pid)
- if completed is not _DEFAULT:
+ if is_defined(completed):
query = query.where(models.Task.completed == completed)
- if result is not _DEFAULT:
+ if is_defined(result):
query = query.where(models.Task.result == result)
- if error is not _DEFAULT:
+ if is_defined(error):
query = query.where(models.Task.error == error)
- if package_sha3 is not _DEFAULT:
+ if is_defined(package_sha3):
query = query.where(models.Task.package_sha3 == package_sha3)
if _package:
@@ -371,29 +403,29 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
def vote_policy(
self,
- id: Any = _DEFAULT,
- mailto_addresses: Any = _DEFAULT,
- manual_vote: Any = _DEFAULT,
- min_hours: Any = _DEFAULT,
- release_checklist: Any = _DEFAULT,
- pause_for_rm: Any = _DEFAULT,
+ id: Opt[int] = NotSet,
+ mailto_addresses: Opt[list[str]] = NotSet,
+ manual_vote: Opt[bool] = NotSet,
+ min_hours: Opt[int] = NotSet,
+ release_checklist: Opt[str] = NotSet,
+ pause_for_rm: Opt[bool] = NotSet,
_committees: bool = False,
_projects: bool = False,
_releases: bool = False,
) -> Query[models.VotePolicy]:
query = sqlmodel.select(models.VotePolicy)
- if id is not _DEFAULT:
+ if is_defined(id):
query = query.where(models.VotePolicy.id == id)
- if mailto_addresses is not _DEFAULT:
+ if is_defined(mailto_addresses):
query = query.where(models.VotePolicy.mailto_addresses ==
mailto_addresses)
- if manual_vote is not _DEFAULT:
+ if is_defined(manual_vote):
query = query.where(models.VotePolicy.manual_vote == manual_vote)
- if min_hours is not _DEFAULT:
+ if is_defined(min_hours):
query = query.where(models.VotePolicy.min_hours == min_hours)
- if release_checklist is not _DEFAULT:
+ if is_defined(release_checklist):
query = query.where(models.VotePolicy.release_checklist ==
release_checklist)
- if pause_for_rm is not _DEFAULT:
+ if is_defined(pause_for_rm):
query = query.where(models.VotePolicy.pause_for_rm == pause_for_rm)
if _committees:
diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index 53e776e..49fb027 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -88,7 +88,7 @@ async def release_add_post(session: session.ClientSession,
request: quart.Reques
form=form,
)
- committee_name = form.committee_name.data
+ committee_name = str(form.committee_name.data)
version = not_none(form.version.data)
project_name = not_none(form.project_name.data)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index e6549a0..97b7024 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -29,7 +29,6 @@ import shutil
import tempfile
from collections.abc import AsyncGenerator, Sequence
-import asfquart as asfquart
import asfquart.auth as auth
import asfquart.base as base
import asfquart.session as session
@@ -284,6 +283,7 @@ async def root_keys_delete() -> response.Response:
web_session = await session.read()
if web_session is None:
raise base.ASFQuartException("Not authenticated", errorcode=401)
+ uid = util.unwrap(web_session.uid)
form = await routes.get_form(quart.request)
fingerprint = form.get("fingerprint")
@@ -294,7 +294,7 @@ async def root_keys_delete() -> response.Response:
async with db.session() as data:
async with data.begin():
# Try to get a GPG key first
- key = await data.public_signing_key(fingerprint=fingerprint,
apache_uid=web_session.uid).get()
+ key = await data.public_signing_key(fingerprint=fingerprint,
apache_uid=uid).get()
if key:
# Delete the GPG key
await data.delete(key)
@@ -302,7 +302,7 @@ async def root_keys_delete() -> response.Response:
return quart.redirect(quart.url_for("root_keys_review"))
# If not a GPG key, try to get an SSH key
- ssh_key = await data.ssh_key(fingerprint=fingerprint,
asf_uid=web_session.uid).get()
+ ssh_key = await data.ssh_key(fingerprint=fingerprint,
asf_uid=uid).get()
if ssh_key:
# Delete the SSH key
await data.delete(ssh_key)
@@ -321,11 +321,12 @@ async def root_keys_review() -> str:
web_session = await session.read()
if web_session is None:
raise base.ASFQuartException("Not authenticated", errorcode=401)
+ uid = util.unwrap(web_session.uid)
# Get all existing keys for the user
async with db.session() as data:
- user_keys = await data.public_signing_key(apache_uid=web_session.uid,
_committees=True).all()
- user_ssh_keys = await data.ssh_key(asf_uid=web_session.uid).all()
+ user_keys = await data.public_signing_key(apache_uid=uid,
_committees=True).all()
+ user_ssh_keys = await data.ssh_key(asf_uid=uid).all()
status_message = quart.request.args.get("status_message")
status_type = quart.request.args.get("status_type")
diff --git a/atr/routes/package.py b/atr/routes/package.py
index 7721622..c514b9f 100644
--- a/atr/routes/package.py
+++ b/atr/routes/package.py
@@ -120,7 +120,7 @@ async def package_add_session_process(
"""Helper function for package_add_post."""
# First check for duplicates by filename
- duplicate = await data.package(release_key=release_key,
filename=artifact_file.filename).get()
+ duplicate = await data.package(release_key=release_key,
filename=util.unwrap(artifact_file.filename)).get()
if duplicate:
raise routes.FlashError("This release artifact has already been
uploaded")
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 2600aa0..c1d962b 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -51,7 +51,7 @@ class CreateVotePolicyForm(util.QuartFormTyped):
async def add_voting_policy(session: session.ClientSession, form:
CreateVotePolicyForm) -> response.Response:
- name = form.project_name.data
+ name = str(form.project_name.data)
async with db.session() as data:
async with data.begin():
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]