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