phzyx-cache 0.1.0__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.
- phzyx_cache-0.1.0/.gitignore +87 -0
- phzyx_cache-0.1.0/PKG-INFO +33 -0
- phzyx_cache-0.1.0/README.md +22 -0
- phzyx_cache-0.1.0/pyproject.toml +19 -0
- phzyx_cache-0.1.0/src/phzyx/cache/__init__.py +13 -0
- phzyx_cache-0.1.0/src/phzyx/cache/backends.py +76 -0
- phzyx_cache-0.1.0/src/phzyx/cache/resource.py +53 -0
- phzyx_cache-0.1.0/tests/test_resource_cache.py +28 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.eggs/
|
|
9
|
+
dist/
|
|
10
|
+
!packages/admin/web/dist/
|
|
11
|
+
!packages/admin/web/dist/**
|
|
12
|
+
build/
|
|
13
|
+
*.egg
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
ENV/
|
|
19
|
+
examples/**/.venv/
|
|
20
|
+
|
|
21
|
+
# uv / packaging
|
|
22
|
+
# uv.lock is committed for reproducible workspace installs
|
|
23
|
+
|
|
24
|
+
# Testing / coverage
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.hypothesis/
|
|
27
|
+
.coverage
|
|
28
|
+
.coverage.*
|
|
29
|
+
coverage.xml
|
|
30
|
+
coverage.json
|
|
31
|
+
htmlcov/
|
|
32
|
+
.mypy_cache/
|
|
33
|
+
.ruff_cache/
|
|
34
|
+
.dmypy.json
|
|
35
|
+
dmypy.json
|
|
36
|
+
.pytype/
|
|
37
|
+
.pyre/
|
|
38
|
+
|
|
39
|
+
# Type checkers / IDE
|
|
40
|
+
.idea/
|
|
41
|
+
.vscode/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
*~
|
|
45
|
+
|
|
46
|
+
# OS
|
|
47
|
+
.DS_Store
|
|
48
|
+
Thumbs.db
|
|
49
|
+
|
|
50
|
+
# Env / secrets
|
|
51
|
+
.env
|
|
52
|
+
.env.*
|
|
53
|
+
!.env.example
|
|
54
|
+
|
|
55
|
+
# Node (AdminBoard SPA + sites)
|
|
56
|
+
node_modules/
|
|
57
|
+
.pnpm-store/
|
|
58
|
+
packages/admin/web/node_modules/
|
|
59
|
+
# Built SPA is committed for offline wheel/demo convenience (M4);
|
|
60
|
+
# rebuild via packages/admin/web `npm run build` when sources change.
|
|
61
|
+
# packages/admin/web/dist/
|
|
62
|
+
|
|
63
|
+
# Local tooling
|
|
64
|
+
.justfile.local
|
|
65
|
+
packages/admin/web/node_modules/
|
|
66
|
+
dist-release/
|
|
67
|
+
|
|
68
|
+
# Local demo SQLite
|
|
69
|
+
examples/**/*.db
|
|
70
|
+
examples/**/media/
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Admin web tooling
|
|
74
|
+
packages/admin/web/test-results/
|
|
75
|
+
packages/admin/web/playwright-report/
|
|
76
|
+
packages/admin/web/blob-report/
|
|
77
|
+
|
|
78
|
+
# Private: research, design plans, AI agent helpers (local only — do not push)
|
|
79
|
+
# Files remain on disk; they are untracked for remotes.
|
|
80
|
+
research/
|
|
81
|
+
AGENTS.md
|
|
82
|
+
AGENT_TO_AGENT.md
|
|
83
|
+
docs/agent-rules.md
|
|
84
|
+
.cursor/
|
|
85
|
+
.claude/
|
|
86
|
+
.grok/
|
|
87
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phzyx-cache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Phzyx versioned resource cache (memory / Redis)
|
|
5
|
+
Author: Athul Nandaswaroop
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: redis
|
|
9
|
+
Requires-Dist: redis>=5; extra == 'redis'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# phzyx-cache
|
|
13
|
+
|
|
14
|
+
**Public battery.** `uv add 'phzyx-cache'` or `uv add 'phzyx-forge[cache]'`.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Per-resource **version keys** for list caching. Writes bump the version so stale lists miss.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from phzyx.cache import MemoryCache, ResourceCache
|
|
21
|
+
|
|
22
|
+
cache = ResourceCache(MemoryCache())
|
|
23
|
+
cache.set_list("users", "v1", [{"id": 1}]) # version auto-managed
|
|
24
|
+
cache.invalidate("users") # on write
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Redis:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv add 'phzyx-cache[redis]'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Limits:** only Phzyx write path invalidates; external DB writers can serve stale lists until TTL/version bump. Not a query cache for arbitrary SQL.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# phzyx-cache
|
|
2
|
+
|
|
3
|
+
**Public battery.** `uv add 'phzyx-cache'` or `uv add 'phzyx-forge[cache]'`.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Per-resource **version keys** for list caching. Writes bump the version so stale lists miss.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from phzyx.cache import MemoryCache, ResourceCache
|
|
10
|
+
|
|
11
|
+
cache = ResourceCache(MemoryCache())
|
|
12
|
+
cache.set_list("users", "v1", [{"id": 1}]) # version auto-managed
|
|
13
|
+
cache.invalidate("users") # on write
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Redis:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add 'phzyx-cache[redis]'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Limits:** only Phzyx write path invalidates; external DB writers can serve stale lists until TTL/version bump. Not a query cache for arbitrary SQL.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "phzyx-cache"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Phzyx versioned resource cache (memory / Redis)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Athul Nandaswaroop" }]
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
redis = ["redis>=5"]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/phzyx"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Phzyx versioned resource cache."""
|
|
2
|
+
|
|
3
|
+
from phzyx.cache.backends import MemoryCache, RedisCache
|
|
4
|
+
from phzyx.cache.resource import ResourceCache
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"MemoryCache",
|
|
10
|
+
"RedisCache",
|
|
11
|
+
"ResourceCache",
|
|
12
|
+
"__version__",
|
|
13
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Cache backends — memory and optional Redis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class CacheBackend(Protocol):
|
|
13
|
+
def get(self, key: str) -> str | None: ...
|
|
14
|
+
|
|
15
|
+
def set(self, key: str, value: str, *, ttl: int | None = None) -> None: ...
|
|
16
|
+
|
|
17
|
+
def delete(self, key: str) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class MemoryCache:
|
|
22
|
+
"""Process-local cache with optional TTL."""
|
|
23
|
+
|
|
24
|
+
_data: dict[str, tuple[str, float | None]] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def get(self, key: str) -> str | None:
|
|
27
|
+
item = self._data.get(key)
|
|
28
|
+
if item is None:
|
|
29
|
+
return None
|
|
30
|
+
value, expires = item
|
|
31
|
+
if expires is not None and time.time() > expires:
|
|
32
|
+
self._data.pop(key, None)
|
|
33
|
+
return None
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
def set(self, key: str, value: str, *, ttl: int | None = None) -> None:
|
|
37
|
+
exp = time.time() + ttl if ttl else None
|
|
38
|
+
self._data[key] = (value, exp)
|
|
39
|
+
|
|
40
|
+
def delete(self, key: str) -> None:
|
|
41
|
+
self._data.pop(key, None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RedisCache:
|
|
45
|
+
"""Redis string cache (optional redis package)."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, client: Any | None = None, *, url: str = "redis://localhost:6379/0") -> None:
|
|
48
|
+
if client is not None:
|
|
49
|
+
self._client = client
|
|
50
|
+
else:
|
|
51
|
+
try:
|
|
52
|
+
import redis # type: ignore[import-not-found]
|
|
53
|
+
except ImportError as exc: # pragma: no cover
|
|
54
|
+
raise ImportError("uv add 'phzyx-cache[redis]'") from exc
|
|
55
|
+
self._client = redis.Redis.from_url(url, decode_responses=True)
|
|
56
|
+
|
|
57
|
+
def get(self, key: str) -> str | None:
|
|
58
|
+
val = self._client.get(key)
|
|
59
|
+
return None if val is None else str(val)
|
|
60
|
+
|
|
61
|
+
def set(self, key: str, value: str, *, ttl: int | None = None) -> None:
|
|
62
|
+
if ttl:
|
|
63
|
+
self._client.setex(key, ttl, value)
|
|
64
|
+
else:
|
|
65
|
+
self._client.set(key, value)
|
|
66
|
+
|
|
67
|
+
def delete(self, key: str) -> None:
|
|
68
|
+
self._client.delete(key)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def dumps(obj: Any) -> str:
|
|
72
|
+
return json.dumps(obj, default=str, separators=(",", ":"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def loads(raw: str) -> Any:
|
|
76
|
+
return json.loads(raw)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Per-resource versioned list cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from phzyx.cache.backends import CacheBackend, dumps, loads
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResourceCache:
|
|
11
|
+
"""Cache lists under ``phzyx:res:{name}:v{version}:list:{suffix}``.
|
|
12
|
+
|
|
13
|
+
``invalidate(resource)`` bumps the version so all prior list keys miss.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, backend: CacheBackend, *, prefix: str = "phzyx", ttl: int | None = 60
|
|
18
|
+
) -> None:
|
|
19
|
+
self.backend = backend
|
|
20
|
+
self.prefix = prefix
|
|
21
|
+
self.ttl = ttl
|
|
22
|
+
|
|
23
|
+
def _ver_key(self, resource: str) -> str:
|
|
24
|
+
return f"{self.prefix}:res:{resource}:version"
|
|
25
|
+
|
|
26
|
+
def version(self, resource: str) -> int:
|
|
27
|
+
raw = self.backend.get(self._ver_key(resource))
|
|
28
|
+
return int(raw or "1")
|
|
29
|
+
|
|
30
|
+
def invalidate(self, resource: str) -> int:
|
|
31
|
+
"""Bump resource version; returns new version."""
|
|
32
|
+
v = self.version(resource) + 1
|
|
33
|
+
self.backend.set(self._ver_key(resource), str(v))
|
|
34
|
+
return v
|
|
35
|
+
|
|
36
|
+
def _list_key(self, resource: str, suffix: str) -> str:
|
|
37
|
+
return f"{self.prefix}:res:{resource}:v{self.version(resource)}:list:{suffix}"
|
|
38
|
+
|
|
39
|
+
def get_list(self, resource: str, suffix: str = "default") -> list[dict[str, Any]] | None:
|
|
40
|
+
raw = self.backend.get(self._list_key(resource, suffix))
|
|
41
|
+
if raw is None:
|
|
42
|
+
return None
|
|
43
|
+
data = loads(raw)
|
|
44
|
+
return data if isinstance(data, list) else None
|
|
45
|
+
|
|
46
|
+
def set_list(
|
|
47
|
+
self,
|
|
48
|
+
resource: str,
|
|
49
|
+
rows: list[dict[str, Any]],
|
|
50
|
+
*,
|
|
51
|
+
suffix: str = "default",
|
|
52
|
+
) -> None:
|
|
53
|
+
self.backend.set(self._list_key(resource, suffix), dumps(rows), ttl=self.ttl)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Resource version cache tests (S6.2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from phzyx.cache import MemoryCache, ResourceCache
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.unit
|
|
10
|
+
def test_list_cache_invalidate_on_version_bump() -> None:
|
|
11
|
+
cache = ResourceCache(MemoryCache(), ttl=None)
|
|
12
|
+
cache.set_list("users", [{"id": 1}])
|
|
13
|
+
assert cache.get_list("users") == [{"id": 1}]
|
|
14
|
+
cache.invalidate("users")
|
|
15
|
+
assert cache.get_list("users") is None
|
|
16
|
+
cache.set_list("users", [{"id": 2}])
|
|
17
|
+
assert cache.get_list("users") == [{"id": 2}]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.unit
|
|
21
|
+
def test_memory_ttl_expiry() -> None:
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
mem = MemoryCache()
|
|
25
|
+
mem.set("k", "v", ttl=1)
|
|
26
|
+
assert mem.get("k") == "v"
|
|
27
|
+
time.sleep(1.05)
|
|
28
|
+
assert mem.get("k") is None
|