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.
@@ -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