This is an automated email from the ASF dual-hosted git repository.

sbp 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 982c00c  Add a simple namespaced key to value store to the database
982c00c is described below

commit 982c00cff988118bfdd3bf2bdc2efe417af39ca2
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Apr 7 19:42:02 2025 +0100

    Add a simple namespaced key to value store to the database
---
 atr/blueprints/admin/admin.py | 60 +++++++++++++++++++++++++++++++++++++++++++
 atr/db/__init__.py            | 17 ++++++++++++
 atr/db/models.py              |  7 +++++
 3 files changed, 84 insertions(+)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 430399e..7545dfd 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -16,9 +16,11 @@
 # under the License.
 
 import collections
+import datetime
 import logging
 import pathlib
 import statistics
+import uuid
 from collections.abc import Callable, Mapping
 from typing import Any
 
@@ -202,6 +204,7 @@ async def admin_data(model: str = "Committee") -> str:
             "Release": data.release,
             "SSHKey": data.ssh_key,
             "Task": data.task,
+            "TextValue": data.text_value,
             "VotePolicy": data.vote_policy,
         }
 
@@ -389,6 +392,63 @@ async def _update_committees() -> tuple[int, int]:  # 
noqa: C901
     return added_count, updated_count
 
 
[email protected]("/test-kv")
+async def admin_test_kv() -> str:
+    """Test route for writing and reading from the TextValue KV store."""
+    test_ns = "kv_test"
+    test_key = str(uuid.uuid4())
+    test_value = f"Test value set at {datetime.datetime.now(datetime.UTC)}"
+    message: str
+
+    try:
+        async with db.session() as data:
+            existing = await data.text_value(ns=test_ns, key=test_key).get()
+            if existing:
+                existing.value = test_value
+                data.add(existing)
+            else:
+                new_entry = models.TextValue(ns=test_ns, key=test_key, 
value=test_value)
+                data.add(new_entry)
+            await data.commit()
+            _LOGGER.info(f"Text value test: Wrote {test_ns}/{test_key} = 
{test_value}")
+
+        async with db.session() as data:
+            read_back = await data.text_value(ns=test_ns, key=test_key).get()
+            if read_back and (read_back.value == test_value):
+                message = f"<p class='success'>Test SUCCESS: Wrote/read ok 
(ns='{test_ns}', key='{test_key}')</p>"
+                _LOGGER.info("Text value test SUCCESS")
+            elif read_back:
+                message = (
+                    f"<p class='error'>Test FAILED: Read back wrong value!</p>"
+                    f"<p>Expected: '{test_value}'</p>"
+                    f"<p>Got: '{read_back.value}'</p>"
+                )
+                _LOGGER.error(
+                    f"Text value test FAILED: Read back wrong value! 
Expected='{test_value}', got='{read_back.value}'"
+                )
+            else:
+                message = f"<p class='error'>Test FAILED: Failed read 
(ns='{test_ns}', key='{test_key}')</p>"
+                _LOGGER.error(f"Text value test FAILED: Failed to read back 
key='{test_key}' in ns='{test_ns}'")
+
+    except Exception as e:
+        message = f"<p class='error'>Test FAILED: Exception occurred - 
{e!s}</p>"
+        _LOGGER.exception("Text value test exception")
+
+    return f"""<!DOCTYPE html>
+<html>
+<head><title>Text value test result</title></head>
+<style>
+.error {{ color: red; }}
+.success {{ color: green; }}
+</style>
+<body>
+<h1>Text value test result</h1>
+{message}
+</body>
+</html>
+"""
+
+
 @admin.BLUEPRINT.route("/releases")
 async def admin_releases() -> str:
     """Display a list of all releases across all stages and phases."""
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 4a02c77..fb968af 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -464,6 +464,23 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
 
         return Query(self, query)
 
+    def text_value(
+        self,
+        ns: Opt[str] = NotSet,
+        key: Opt[str] = NotSet,
+        value: Opt[str] = NotSet,
+    ) -> Query[models.TextValue]:
+        query = sqlmodel.select(models.TextValue)
+
+        if is_defined(ns):
+            query = query.where(models.TextValue.ns == ns)
+        if is_defined(key):
+            query = query.where(models.TextValue.key == key)
+        if is_defined(value):
+            query = query.where(models.TextValue.value == value)
+
+        return Query(self, query)
+
 
 def init_database(app: base.QuartApp) -> None:
     """
diff --git a/atr/db/models.py b/atr/db/models.py
index 0dc3da3..110369e 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -439,3 +439,10 @@ class CheckResult(sqlmodel.SQLModel, table=True):
     status: CheckResultStatus
     message: str
     data: Any = sqlmodel.Field(sa_column=sqlalchemy.Column(sqlalchemy.JSON))
+
+
+class TextValue(sqlmodel.SQLModel, table=True):
+    # Composite primary key, automatically handled by SQLModel
+    ns: str = sqlmodel.Field(primary_key=True, index=True)
+    key: str = sqlmodel.Field(primary_key=True, index=True)
+    value: str = sqlmodel.Field()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to