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/05/19 18:42:27 UTC

(tooling-trusted-releases) branch sbp updated: Add notification methods to the storage interface

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 ed1cf4b7 Add notification methods to the storage interface
ed1cf4b7 is described below

commit ed1cf4b7fefe736bfcde4e0ab264f9ce5309ebd1
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue May 19 19:41:24 2026 +0100

    Add notification methods to the storage interface
---
 atr/storage/__init__.py              |  3 ++
 atr/storage/readers/__init__.py      |  3 +-
 atr/storage/readers/notifications.py | 51 +++++++++++++++++++++
 atr/storage/writers/__init__.py      |  2 +
 atr/storage/writers/notifications.py | 88 ++++++++++++++++++++++++++++++++++++
 5 files changed, 146 insertions(+), 1 deletion(-)

diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 64daa8b8..0f5ae18e 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -80,6 +80,7 @@ class ReadAsFoundationCommitter(ReadAsGeneralPublic):
     def __init__(self, read: Read, data: db.Session) -> None:
         # self.checks = readers.checks.FoundationCommitter(read, self, data)
         # self.releases = readers.releases.FoundationCommitter(read, self, data)
+        self.notifications = readers.notifications.FoundationCommitter(read, self, data)
         self.tokens = readers.tokens.FoundationCommitter(read, self, data)
         self.user = readers.user.FoundationCommitter(read, self, data)
 
@@ -149,6 +150,7 @@ class WriteAsFoundationCommitter(WriteAsGeneralPublic):
         self.checks = writers.checks.FoundationCommitter(write, self, data)
         self.keys = writers.keys.FoundationCommitter(write, self, data)
         self.mail = writers.mail.FoundationCommitter(write, self, data)
+        self.notifications = writers.notifications.FoundationCommitter(write, self, data)
         self.policy = writers.policy.FoundationCommitter(write, self, data)
         self.project = writers.project.FoundationCommitter(write, self, data)
         self.release = writers.release.FoundationCommitter(write, self, data)
@@ -254,6 +256,7 @@ class WriteAsUserService(WriteAs):
             raise AccessError("User service writes require an ASF UID", status=500)
         self.__data = data
         self.__asf_uid = asf_uid
+        self.notifications = writers.notifications.UserService(self, data)
 
     @property
     def asf_uid(self) -> str:
diff --git a/atr/storage/readers/__init__.py b/atr/storage/readers/__init__.py
index d011ab90..9f5a59e8 100644
--- a/atr/storage/readers/__init__.py
+++ b/atr/storage/readers/__init__.py
@@ -16,8 +16,9 @@
 # under the License.
 
 import atr.storage.readers.checks as checks
+import atr.storage.readers.notifications as notifications
 import atr.storage.readers.releases as releases
 import atr.storage.readers.tokens as tokens
 import atr.storage.readers.user as user
 
-__all__ = ["checks", "releases", "tokens", "user"]
+__all__ = ["checks", "notifications", "releases", "tokens", "user"]
diff --git a/atr/storage/readers/notifications.py b/atr/storage/readers/notifications.py
new file mode 100644
index 00000000..01332286
--- /dev/null
+++ b/atr/storage/readers/notifications.py
@@ -0,0 +1,51 @@
+# 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.
+
+from __future__ import annotations
+
+import sqlmodel
+
+import atr.db as db
+import atr.models.sql as sql
+import atr.storage as storage
+
+
+class FoundationCommitter:
+    # We never usually need to authenticate to read, but we do for notifications
+    # They're still public, but we need to show them to the correct user
+    # TODO: We should probably move all interaction.py stuff to storage anyway
+
+    def __init__(self, read: storage.Read, read_as: storage.ReadAsFoundationCommitter, data: db.Session):
+        self.__read = read
+        self.__read_as = read_as
+        self.__data = data
+        self.__asf_uid = read.authorisation.asf_uid
+
+    async def pending(self, limit: int = 50) -> list[sql.Notification]:
+        if self.__asf_uid is None:
+            raise storage.AccessError("Not authorized", status=403)
+        return await self.__list_for_uid(limit)
+
+    async def __list_for_uid(self, limit: int) -> list[sql.Notification]:
+        via = sql.validate_instrumented_attribute
+        stmt = (
+            sqlmodel.select(sql.Notification)
+            .where(sql.Notification.asf_uid == self.__asf_uid)
+            .order_by(via(sql.Notification.created), via(sql.Notification.id))
+            .limit(limit)
+        )
+        return await self.__data.query_all(stmt)
diff --git a/atr/storage/writers/__init__.py b/atr/storage/writers/__init__.py
index 67353037..5a8b34e6 100644
--- a/atr/storage/writers/__init__.py
+++ b/atr/storage/writers/__init__.py
@@ -23,6 +23,7 @@ import atr.storage.writers.checks as checks
 import atr.storage.writers.distributions as distributions
 import atr.storage.writers.keys as keys
 import atr.storage.writers.mail as mail
+import atr.storage.writers.notifications as notifications
 import atr.storage.writers.policy as policy
 import atr.storage.writers.project as project
 import atr.storage.writers.release as release
@@ -41,6 +42,7 @@ __all__ = [
     "distributions",
     "keys",
     "mail",
+    "notifications",
     "policy",
     "project",
     "release",
diff --git a/atr/storage/writers/notifications.py b/atr/storage/writers/notifications.py
new file mode 100644
index 00000000..8905a673
--- /dev/null
+++ b/atr/storage/writers/notifications.py
@@ -0,0 +1,88 @@
+# 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.
+
+from __future__ import annotations
+
+from typing import Final
+
+import sqlmodel
+
+import atr.db as db
+import atr.models.sql as sql
+import atr.storage as storage
+
+_MAX_MESSAGE_LENGTH: Final[int] = 1024
+
+
+class FoundationCommitter:
+    def __init__(self, write: storage.Write, write_as: storage.WriteAsFoundationCommitter, data: db.Session):
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        self.__asf_uid = write_as.asf_uid
+
+    async def dismiss(self, notification_id: int) -> bool:
+        via = sql.validate_instrumented_attribute
+        result = await self.__data.execute(
+            sqlmodel.delete(sql.Notification).where(
+                via(sql.Notification.id) == notification_id,
+                via(sql.Notification.asf_uid) == self.__asf_uid,
+            )
+        )
+        await self.__data.commit()
+        dismissed = (getattr(result, "rowcount", 0) or 0) > 0
+        if dismissed:
+            self.__write_as.append_to_audit_log(
+                asf_uid=self.__asf_uid,
+                notification_id=notification_id,
+            )
+        return dismissed
+
+
+class UserService:
+    def __init__(self, waus: storage.WriteAsUserService, data: db.Session):
+        self.__waus = waus
+        self.__data = data
+        self.__asf_uid = waus.asf_uid
+
+    async def create(
+        self,
+        message: str,
+        level: sql.NotificationLevel = sql.NotificationLevel.ERROR,
+    ) -> sql.Notification:
+        notification = sql.Notification(
+            asf_uid=self.__asf_uid,
+            message=_normalised_message(message),
+            level=level,
+        )
+        self.__data.add(notification)
+        await self.__data.commit()
+        self.__waus.append_to_audit_log(
+            asf_uid=self.__asf_uid,
+            notification_id=notification.id,
+            level=level.value,
+        )
+        return notification
+
+
+def _normalised_message(message: str) -> str:
+    message = " ".join(message.strip().split())
+    if not message:
+        raise ValueError("Notification message cannot be empty")
+    if len(message) > _MAX_MESSAGE_LENGTH:
+        return message[: _MAX_MESSAGE_LENGTH - 3] + "..."
+    return message


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org