You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/07/20 15:21:49 UTC
(tooling-trusted-releases) branch sbp updated: Consolidate release queries for the navigation menu
This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new 7019111f Consolidate release queries for the navigation menu
7019111f is described below
commit 7019111f25f0f69bc846a578069427fa1e693fa9
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon Jul 20 16:19:27 2026 +0100
Consolidate release queries for the navigation menu
---
atr/db/interaction.py | 71 +++++++++----------
atr/server.py | 5 +-
tests/unit/test_topnav.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 210 insertions(+), 37 deletions(-)
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 6fd8d1b6..ef760a65 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -946,38 +946,6 @@ def trusted_vote_round(release: sql.Release) -> int | None:
return None
-async def unfinished_releases(asfuid: str, is_member: bool) -> list[tuple[str, str, list[sql.Release]]]:
- releases: list[tuple[str, str, list[sql.Release]]] = []
- async with db.session() as data:
- user_projects = await user.projects(asfuid)
- user_projects.sort(key=lambda p: p.display_name)
-
- active_phases = [
- sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
- sql.ReleasePhase.RELEASE_CANDIDATE,
- sql.ReleasePhase.RELEASE_PREVIEW,
- ]
- for project in user_projects:
- stmt = (
- sqlmodel.select(sql.Release)
- .where(
- sql.Release.project_key == project.key,
- sql.validate_instrumented_attribute(sql.Release.phase).in_(active_phases),
- )
- .options(db.select_in_load(sql.Release.project))
- .order_by(sql.validate_instrumented_attribute(sql.Release.created).desc())
- )
- result = await data.execute(stmt)
- active_releases = list(result.scalars().all())
- if not user.can_view_embargoed_release(project.committee, asfuid, is_member=is_member):
- active_releases = [r for r in active_releases if (not r.is_embargoed)]
- if active_releases:
- active_releases.sort(key=lambda r: r.created, reverse=True)
- releases.append((project.short_display_name, project.key, active_releases))
-
- return releases
-
-
async def user_committees(asf_uid: str) -> list[tuple[str, str]]:
results = []
for committee in await user_committees_participant(asf_uid):
@@ -1003,9 +971,42 @@ async def user_committees_participant(asf_uid: str, caller_data: db.Session | No
return await data.committee(has_participant=asf_uid).all()
-async def user_projects(asf_uid: str, caller_data: db.Session | None = None) -> list[tuple[str, str]]:
- projects = await user.projects(asf_uid)
- return [(p.key, p.display_name) for p in projects]
+async def user_topnav(
+ asf_uid: str, is_member: bool
+) -> tuple[list[tuple[str, str, list[sql.Release]]], list[tuple[str, str]]]:
+ via = sql.validate_instrumented_attribute
+ user_projects = await user.projects(asf_uid)
+ user_projects.sort(key=lambda p: p.display_name)
+
+ active_phases = [
+ sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+ sql.ReleasePhase.RELEASE_CANDIDATE,
+ sql.ReleasePhase.RELEASE_PREVIEW,
+ ]
+ async with db.session() as data:
+ stmt = (
+ sqlmodel.select(sql.Release)
+ .where(
+ via(sql.Release.project_key).in_([p.key for p in user_projects]),
+ via(sql.Release.phase).in_(active_phases),
+ )
+ .order_by(via(sql.Release.created).desc())
+ )
+ result = await data.execute(stmt)
+ releases_by_project: dict[str, list[sql.Release]] = {}
+ for release in result.scalars().all():
+ releases_by_project.setdefault(release.project_key, []).append(release)
+
+ unfinished: list[tuple[str, str, list[sql.Release]]] = []
+ for project in user_projects:
+ active_releases = releases_by_project.get(project.key, [])
+ for release in active_releases:
+ release.project = project
+ if not user.can_view_embargoed_release(project.committee, asf_uid, is_member=is_member):
+ active_releases = [r for r in active_releases if (not r.is_embargoed)]
+ if active_releases:
+ unfinished.append((project.short_display_name, project.key, active_releases))
+ return unfinished, [(p.key, p.display_name) for p in user_projects]
async def validate_trusted_jwt(publisher: str, jwt: str) -> tuple[github.TrustedPublisherPayload, str | None]:
diff --git a/atr/server.py b/atr/server.py
index af86e43e..54738fe3 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -285,8 +285,9 @@ def _app_setup_context(app: base.QuartApp) -> None:
user_notifications: list[sql.Notification] = []
if current_user is not None:
current_uid = current_user.uid
- topnav_unfinished_releases = await interaction.unfinished_releases(current_uid, current_user.is_member)
- topnav_user_projects = await interaction.user_projects(current_uid)
+ topnav_unfinished_releases, topnav_user_projects = await interaction.user_topnav(
+ current_uid, current_user.is_member
+ )
async with db.session() as data:
db_user = await data.user(asf_uid=current_uid).get()
if db_user:
diff --git a/tests/unit/test_topnav.py b/tests/unit/test_topnav.py
new file mode 100644
index 00000000..c7b43bd9
--- /dev/null
+++ b/tests/unit/test_topnav.py
@@ -0,0 +1,171 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime
+from collections.abc import AsyncIterator
+
+import pytest
+import sqlalchemy
+import sqlalchemy.event
+import sqlalchemy.ext.asyncio
+import sqlmodel
+
+import atr.db as db
+import atr.db.interaction as interaction
+import atr.models.sql as sql
+import atr.user as user
+
+
+class MockApp:
+ def __init__(self):
+ self.extensions: dict[str, object] = {}
+
+
+class MockConfig:
+ ADMIN_USERS_ADDITIONAL = ""
+
+
+@pytest.fixture
+def mock_app(monkeypatch: pytest.MonkeyPatch) -> MockApp:
+ app = MockApp()
+ monkeypatch.setattr("asfquart.APP", app)
+ return app
+
+
+@pytest.fixture
+async def sqlite_global_db() -> AsyncIterator[sqlalchemy.ext.asyncio.AsyncEngine]:
+ engine = sqlalchemy.ext.asyncio.create_async_engine(
+ "sqlite+aiosqlite:///:memory:",
+ connect_args={"check_same_thread": False},
+ poolclass=sqlalchemy.pool.StaticPool,
+ )
+ async with engine.begin() as conn:
+ await conn.run_sync(sqlmodel.SQLModel.metadata.create_all)
+ db._global_atr_sessionmaker = sqlalchemy.ext.asyncio.async_sessionmaker(
+ bind=engine, class_=db.Session, expire_on_commit=False
+ )
+ yield engine
+ await engine.dispose()
+ db._global_atr_sessionmaker = None
+
+
+@pytest.mark.asyncio
+async def test_user_topnav_embargo_and_grouping(
+ sqlite_global_db: sqlalchemy.ext.asyncio.AsyncEngine, mock_app: MockApp, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ user._get_additional_admin_users.cache_clear()
+ monkeypatch.setattr("atr.config.get", lambda: MockConfig())
+ mock_app.extensions["admins"] = frozenset()
+ async with db.session() as data:
+ data.add(sql.Committee(key="example", name="Example", committee_members=["alice"], committers=["bob"]))
+ data.add(sql.Project(key="example", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+ data.add(sql.Project(key="example-two", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+ data.add(
+ sql.Release(
+ key="example-1.0.0",
+ phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+ project_key="example",
+ version="1.0.0",
+ created=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC),
+ )
+ )
+ data.add(
+ sql.Release(
+ key="example-2.0.0",
+ phase=sql.ReleasePhase.RELEASE_CANDIDATE,
+ project_key="example",
+ version="2.0.0",
+ created=datetime.datetime(2026, 2, 1, tzinfo=datetime.UTC),
+ expedited=True,
+ )
+ )
+ data.add(
+ sql.Release(
+ key="example-0.9.0",
+ phase=sql.ReleasePhase.RELEASE,
+ project_key="example",
+ version="0.9.0",
+ created=datetime.datetime(2025, 12, 1, tzinfo=datetime.UTC),
+ )
+ )
+ await data.commit()
+
+ unfinished, projects = await interaction.user_topnav("alice", False)
+ assert projects == [("example", "example"), ("example-two", "example-two")]
+ assert [(name, key) for name, key, _releases in unfinished] == [("example", "example")]
+ releases = unfinished[0][2]
+ assert [r.version for r in releases] == ["2.0.0", "1.0.0"]
+ assert releases[0].project.key == "example"
+
+ unfinished, _projects = await interaction.user_topnav("bob", False)
+ assert [r.version for r in unfinished[0][2]] == ["1.0.0"]
+
+ unfinished, _projects = await interaction.user_topnav("bob", True)
+ assert [r.version for r in unfinished[0][2]] == ["2.0.0", "1.0.0"]
+
+
+@pytest.mark.asyncio
+async def test_user_topnav_statement_count_constant(
+ sqlite_global_db: sqlalchemy.ext.asyncio.AsyncEngine, mock_app: MockApp, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ user._get_additional_admin_users.cache_clear()
+ monkeypatch.setattr("atr.config.get", lambda: MockConfig())
+ mock_app.extensions["admins"] = frozenset()
+ statements: list[str] = []
+ sqlalchemy.event.listen(
+ sqlite_global_db.sync_engine,
+ "before_cursor_execute",
+ lambda conn, cursor, statement, parameters, context, executemany: statements.append(statement),
+ )
+
+ async with db.session() as data:
+ data.add(sql.Committee(key="example", name="Example", committee_members=["alice"]))
+ data.add(sql.Project(key="example", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+ data.add(
+ sql.Release(
+ key="example-1.0.0",
+ phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+ project_key="example",
+ version="1.0.0",
+ created=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC),
+ )
+ )
+ await data.commit()
+
+ statements.clear()
+ await interaction.user_topnav("alice", False)
+ baseline = len(statements)
+
+ async with db.session() as data:
+ for number in (2, 3):
+ data.add(sql.Project(key=f"example-{number}", committee_key="example", status=sql.ProjectStatus.ACTIVE))
+ data.add(
+ sql.Release(
+ key=f"example-{number}-1.0.0",
+ phase=sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT,
+ project_key=f"example-{number}",
+ version="1.0.0",
+ created=datetime.datetime(2026, 1, number, tzinfo=datetime.UTC),
+ )
+ )
+ await data.commit()
+
+ statements.clear()
+ unfinished, projects = await interaction.user_topnav("alice", False)
+ assert len(projects) == 3
+ assert len(unfinished) == 3
+ assert len(statements) == baseline
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org