proactive-gate 0.2.5__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/PKG-INFO +11 -3
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/README.md +3 -1
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/pyproject.toml +12 -2
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/stores.py +11 -1
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/tests/test_stores.py +18 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/.gitignore +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/__init__.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/checks.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/clock.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/conformance.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/gate.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/policy.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/presets.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/py.typed +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/src/proactive_gate/types.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/tests/conftest.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/tests/test_clock.py +0 -0
- {proactive_gate-0.2.5 → proactive_gate-0.3.1}/tests/test_conformance.py +0 -0
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: proactive-gate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Decide whether a proactive assistant may speak now: ordered checks, budgets, quiet hours, JSON policies, shared conformance fixtures with the TypeScript package.
|
|
5
5
|
Project-URL: Homepage, https://bubblegunn.github.io/proactive-gate/
|
|
6
|
+
Project-URL: Documentation, https://bubblegunn.github.io/proactive-gate/start/
|
|
7
|
+
Project-URL: Playground, https://bubblegunn.github.io/proactive-gate/playground/
|
|
6
8
|
Project-URL: Repository, https://github.com/Bubblegunn/proactive-gate
|
|
7
9
|
Project-URL: Issues, https://github.com/Bubblegunn/proactive-gate/issues
|
|
8
10
|
Project-URL: Specification, https://github.com/Bubblegunn/proactive-gate/tree/main/spec
|
|
9
11
|
Project-URL: Changelog, https://github.com/Bubblegunn/proactive-gate/blob/main/CHANGELOG.md
|
|
10
12
|
Author: Efe Genc
|
|
11
13
|
License-Expression: MIT
|
|
12
|
-
Keywords: agents,notifications,policy,proactive,quiet-hours,rate-limit
|
|
14
|
+
Keywords: agents,ai-agents,consent,guardrails,interruption,llm,notification-budget,notification-policy,notifications,policy,policy-engine,proactive,proactive-agents,proactive-ai,quiet-hours,rate-limit
|
|
13
15
|
Classifier: Development Status :: 4 - Beta
|
|
14
16
|
Classifier: Framework :: AsyncIO
|
|
15
17
|
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
16
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
25
|
Classifier: Topic :: Communications
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
27
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
28
|
Classifier: Typing :: Typed
|
|
23
29
|
Requires-Python: >=3.11
|
|
@@ -110,7 +116,9 @@ runs that loop with values you supply, for tests with no store at all.
|
|
|
110
116
|
|
|
111
117
|
`MemoryStore` (in-process), `SqliteStore` (standard library, one file per host),
|
|
112
118
|
`AsyncMemoryStore`, and `RedisStore` over `redis.asyncio` (INCR, then EXPIRE on the first
|
|
113
|
-
increment). Any object with `get`, `set`, `incr` and `delete` works.
|
|
119
|
+
increment). Any object with `get`, `set`, `incr` and `delete` works. `SqliteStore` removes
|
|
120
|
+
an expired row when a read touches it and clears every already-expired row on each `set` or
|
|
121
|
+
`incr`, so a key nobody reads again still disappears.
|
|
114
122
|
|
|
115
123
|
## Presets
|
|
116
124
|
|
|
@@ -79,7 +79,9 @@ runs that loop with values you supply, for tests with no store at all.
|
|
|
79
79
|
|
|
80
80
|
`MemoryStore` (in-process), `SqliteStore` (standard library, one file per host),
|
|
81
81
|
`AsyncMemoryStore`, and `RedisStore` over `redis.asyncio` (INCR, then EXPIRE on the first
|
|
82
|
-
increment). Any object with `get`, `set`, `incr` and `delete` works.
|
|
82
|
+
increment). Any object with `get`, `set`, `incr` and `delete` works. `SqliteStore` removes
|
|
83
|
+
an expired row when a read touches it and clears every already-expired row on each `set` or
|
|
84
|
+
`incr`, so a key nobody reads again still disappears.
|
|
83
85
|
|
|
84
86
|
## Presets
|
|
85
87
|
|
|
@@ -4,13 +4,17 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proactive-gate"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Decide whether a proactive assistant may speak now: ordered checks, budgets, quiet hours, JSON policies, shared conformance fixtures with the TypeScript package."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
authors = [{ name = "Efe Genc" }]
|
|
13
|
-
keywords = [
|
|
13
|
+
keywords = [
|
|
14
|
+
"proactive", "proactive-ai", "proactive-agents", "ai-agents", "llm", "agents",
|
|
15
|
+
"notifications", "notification-policy", "notification-budget", "quiet-hours",
|
|
16
|
+
"consent", "rate-limit", "policy", "policy-engine", "guardrails", "interruption",
|
|
17
|
+
]
|
|
14
18
|
classifiers = [
|
|
15
19
|
"Development Status :: 4 - Beta",
|
|
16
20
|
"Intended Audience :: Developers",
|
|
@@ -18,8 +22,12 @@ classifiers = [
|
|
|
18
22
|
"Programming Language :: Python :: 3.11",
|
|
19
23
|
"Programming Language :: Python :: 3.12",
|
|
20
24
|
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
26
|
"Framework :: AsyncIO",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
22
29
|
"Topic :: Software Development :: Libraries",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
31
|
"Topic :: Communications",
|
|
24
32
|
"Typing :: Typed",
|
|
25
33
|
]
|
|
@@ -31,6 +39,8 @@ dev = ["pytest>=8", "mypy>=1.11", "redis>=5"]
|
|
|
31
39
|
|
|
32
40
|
[project.urls]
|
|
33
41
|
Homepage = "https://bubblegunn.github.io/proactive-gate/"
|
|
42
|
+
Documentation = "https://bubblegunn.github.io/proactive-gate/start/"
|
|
43
|
+
Playground = "https://bubblegunn.github.io/proactive-gate/playground/"
|
|
34
44
|
Repository = "https://github.com/Bubblegunn/proactive-gate"
|
|
35
45
|
Issues = "https://github.com/Bubblegunn/proactive-gate/issues"
|
|
36
46
|
Specification = "https://github.com/Bubblegunn/proactive-gate/tree/main/spec"
|
|
@@ -72,8 +72,15 @@ class SqliteStore:
|
|
|
72
72
|
def __init__(self, path: str = ":memory:") -> None:
|
|
73
73
|
self._conn = sqlite3.connect(path, isolation_level=None, check_same_thread=False)
|
|
74
74
|
self._conn.execute("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at REAL)")
|
|
75
|
+
# The partial index keeps a sweep proportional to the dead rows instead of a table scan.
|
|
76
|
+
self._conn.execute("CREATE INDEX IF NOT EXISTS kv_expires_at ON kv (expires_at) WHERE expires_at IS NOT NULL")
|
|
75
77
|
self._lock = threading.Lock()
|
|
76
78
|
|
|
79
|
+
def _sweep(self, now: float) -> None:
|
|
80
|
+
"""A read prunes only the key it touches; a write first clears every row
|
|
81
|
+
that has already expired, so a key nobody reads again does not live forever."""
|
|
82
|
+
self._conn.execute("DELETE FROM kv WHERE expires_at IS NOT NULL AND expires_at <= ?", (now,))
|
|
83
|
+
|
|
77
84
|
def _row(self, key: str) -> str | None:
|
|
78
85
|
row = self._conn.execute("SELECT value, expires_at FROM kv WHERE key = ?", (key,)).fetchone()
|
|
79
86
|
if row is None:
|
|
@@ -89,14 +96,17 @@ class SqliteStore:
|
|
|
89
96
|
return self._row(key)
|
|
90
97
|
|
|
91
98
|
def set(self, key: str, value: str, ttl_seconds: int | None = None) -> None:
|
|
92
|
-
|
|
99
|
+
now = time.time()
|
|
100
|
+
expires = now + ttl_seconds if ttl_seconds else None
|
|
93
101
|
with self._lock:
|
|
102
|
+
self._sweep(now)
|
|
94
103
|
self._conn.execute("INSERT OR REPLACE INTO kv (key, value, expires_at) VALUES (?, ?, ?)", (key, value, expires))
|
|
95
104
|
|
|
96
105
|
def incr(self, key: str, ttl_seconds: int | None = None) -> int:
|
|
97
106
|
with self._lock:
|
|
98
107
|
self._conn.execute("BEGIN IMMEDIATE")
|
|
99
108
|
try:
|
|
109
|
+
self._sweep(time.time())
|
|
100
110
|
current = self._row(key)
|
|
101
111
|
next_value = int(current or 0) + 1
|
|
102
112
|
if current is None:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import sqlite3
|
|
3
|
+
import time
|
|
2
4
|
from datetime import datetime, timezone
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
@@ -31,6 +33,22 @@ def test_sqlite_store_persists_across_connections(tmp_path: Path) -> None:
|
|
|
31
33
|
second.close()
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
def test_sqlite_store_removes_expired_rows_on_write(tmp_path: Path) -> None:
|
|
37
|
+
path = str(tmp_path / "gate.sqlite")
|
|
38
|
+
store = SqliteStore(path)
|
|
39
|
+
raw = sqlite3.connect(path, isolation_level=None)
|
|
40
|
+
expired = time.time() - 10
|
|
41
|
+
for i in range(50):
|
|
42
|
+
raw.execute("INSERT INTO kv (key, value, expires_at) VALUES (?, 'v', ?)", (f"stale-{i}", expired))
|
|
43
|
+
raw.execute("INSERT INTO kv (key, value, expires_at) VALUES ('keeper', 'v', ?)", (time.time() + 600,))
|
|
44
|
+
# None of the stale keys were read, so all fifty rows are still physically there.
|
|
45
|
+
assert raw.execute("SELECT COUNT(*) FROM kv").fetchone()[0] == 51
|
|
46
|
+
store.incr("fresh", 60)
|
|
47
|
+
assert {key for (key,) in raw.execute("SELECT key FROM kv")} == {"keeper", "fresh"}
|
|
48
|
+
raw.close()
|
|
49
|
+
store.close()
|
|
50
|
+
|
|
51
|
+
|
|
34
52
|
def _user() -> UserState:
|
|
35
53
|
return UserState.from_dict({"id": "u1", "consent": True, "timezone": "Europe/Istanbul", "createdAt": "2026-01-01T00:00:00Z"})
|
|
36
54
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|