izana-core 0.0.1__tar.gz → 0.2.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.
- {izana_core-0.0.1 → izana_core-0.2.0}/.gitignore +1 -1
- izana_core-0.2.0/PKG-INFO +12 -0
- izana_core-0.2.0/pyproject.toml +30 -0
- izana_core-0.2.0/src/izana_core/__init__.py +0 -0
- izana_core-0.2.0/src/izana_core/cache/__init__.py +0 -0
- izana_core-0.2.0/src/izana_core/cache/backend.py +110 -0
- izana_core-0.2.0/src/izana_core/cache/keys.py +41 -0
- izana_core-0.2.0/src/izana_core/client/__init__.py +0 -0
- izana_core-0.2.0/src/izana_core/client/function.py +565 -0
- izana_core-0.2.0/src/izana_core/executor.py +242 -0
- izana_core-0.2.0/src/izana_core/graph_check.py +246 -0
- izana_core-0.2.0/src/izana_core/ir.py +367 -0
- izana_core-0.2.0/src/izana_core/mwt.py +110 -0
- izana_core-0.2.0/src/izana_core/py.typed +0 -0
- izana_core-0.2.0/src/izana_core/registry.py +121 -0
- izana_core-0.2.0/src/izana_core/templates/ir/document.kdl.j2 +121 -0
- izana_core-0.2.0/src/izana_core/type_utils.py +64 -0
- izana_core-0.2.0/tests/__init__.py +0 -0
- izana_core-0.2.0/tests/test_executor.py +159 -0
- izana_core-0.2.0/tests/test_graph_check.py +160 -0
- izana_core-0.2.0/tests/test_ir.py +95 -0
- izana_core-0.2.0/tests/test_keys.py +67 -0
- izana_core-0.2.0/tests/test_mwt.py +86 -0
- izana_core-0.0.1/PKG-INFO +0 -13
- izana_core-0.0.1/README.md +0 -5
- izana_core-0.0.1/pyproject.toml +0 -14
- izana_core-0.0.1/src/izana_core/__init__.py +0 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: izana-core
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Izana Python core — @client registry, dispatch executor, KDL IR, HMAC cache keys, MWT identity. Framework-agnostic primitives shared by every Python backend adapter.
|
|
5
|
+
License-Expression: Elastic-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: jinja2>=3.1
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: pyjwt>=2.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: ckdl>=1.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "izana-core"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
license = "Elastic-2.0"
|
|
5
|
+
description = "Izana Python core — @client registry, dispatch executor, KDL IR, HMAC cache keys, MWT identity. Framework-agnostic primitives shared by every Python backend adapter."
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"PyJWT>=2.0",
|
|
9
|
+
"jinja2>=3.1",
|
|
10
|
+
"pydantic>=2.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = [
|
|
15
|
+
"pytest>=8.0",
|
|
16
|
+
"ckdl>=1.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/izana_core"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
pythonpath = ["src"]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
python_classes = ["*Tests", "*Test", "Test*"]
|
|
30
|
+
python_functions = ["test_*"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import abc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CacheBackend(abc.ABC):
|
|
8
|
+
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def get(self, key: str) -> bytes | None:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def set(self, key: str, value: bytes) -> None:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def delete(self, key: str) -> bool:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
def delete_by_prefix(self, prefix: str) -> int:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
def clear(self) -> None:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MemoryCache(CacheBackend):
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._store: dict[str, bytes] = {}
|
|
34
|
+
|
|
35
|
+
def get(self, key: str) -> bytes | None:
|
|
36
|
+
return self._store.get(key)
|
|
37
|
+
|
|
38
|
+
def set(self, key: str, value: bytes) -> None:
|
|
39
|
+
self._store[key] = value
|
|
40
|
+
|
|
41
|
+
def delete(self, key: str) -> bool:
|
|
42
|
+
if key in self._store:
|
|
43
|
+
del self._store[key]
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def delete_by_prefix(self, prefix: str) -> int:
|
|
48
|
+
to_delete = [k for k in self._store if k.startswith(prefix)]
|
|
49
|
+
for k in to_delete:
|
|
50
|
+
del self._store[k]
|
|
51
|
+
return len(to_delete)
|
|
52
|
+
|
|
53
|
+
def clear(self) -> None:
|
|
54
|
+
self._store.clear()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RedisCache(CacheBackend):
|
|
58
|
+
|
|
59
|
+
DEFAULT_TTL = 86400 # 24h safety-net
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
redis_url: str,
|
|
64
|
+
prefix: str = "izana:",
|
|
65
|
+
ttl: int | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
try:
|
|
68
|
+
import redis as redis_lib
|
|
69
|
+
except ImportError as exc:
|
|
70
|
+
raise ImportError(
|
|
71
|
+
"Redis is required for Izana's cache backend. "
|
|
72
|
+
"Install it with: pip install izana[cache]"
|
|
73
|
+
) from exc
|
|
74
|
+
self._client = redis_lib.from_url(
|
|
75
|
+
redis_url,
|
|
76
|
+
socket_connect_timeout=5,
|
|
77
|
+
socket_timeout=5,
|
|
78
|
+
health_check_interval=30,
|
|
79
|
+
retry_on_timeout=True,
|
|
80
|
+
max_connections=50,
|
|
81
|
+
)
|
|
82
|
+
self._prefix = prefix
|
|
83
|
+
self._ttl = ttl if ttl is not None else self.DEFAULT_TTL
|
|
84
|
+
|
|
85
|
+
def _key(self, key: str) -> str:
|
|
86
|
+
return f"{self._prefix}{key}"
|
|
87
|
+
|
|
88
|
+
def get(self, key: str) -> bytes | None:
|
|
89
|
+
return self._client.get(self._key(key))
|
|
90
|
+
|
|
91
|
+
def set(self, key: str, value: bytes) -> None:
|
|
92
|
+
self._client.set(self._key(key), value, ex=self._ttl)
|
|
93
|
+
|
|
94
|
+
def delete(self, key: str) -> bool:
|
|
95
|
+
return self._client.unlink(self._key(key)) > 0
|
|
96
|
+
|
|
97
|
+
def delete_by_prefix(self, prefix: str) -> int:
|
|
98
|
+
pattern = f"{self._prefix}{prefix}*"
|
|
99
|
+
count = 0
|
|
100
|
+
cursor = 0
|
|
101
|
+
while True:
|
|
102
|
+
cursor, keys = self._client.scan(cursor, match=pattern, count=1000)
|
|
103
|
+
if keys:
|
|
104
|
+
count += self._client.unlink(*keys)
|
|
105
|
+
if cursor == 0:
|
|
106
|
+
break
|
|
107
|
+
return count
|
|
108
|
+
|
|
109
|
+
def clear(self) -> None:
|
|
110
|
+
self.delete_by_prefix("")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
CONTEXT_KEY_PREFIX = "ctx:"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def derive_cache_key(
|
|
13
|
+
secret: str,
|
|
14
|
+
context: str,
|
|
15
|
+
params: dict[str, Any],
|
|
16
|
+
user_id: str | None = None,
|
|
17
|
+
rev: int = 0,
|
|
18
|
+
) -> str:
|
|
19
|
+
def _normalize(v: Any) -> str:
|
|
20
|
+
if v is True:
|
|
21
|
+
return "true"
|
|
22
|
+
if v is False:
|
|
23
|
+
return "false"
|
|
24
|
+
if v is None:
|
|
25
|
+
return "null"
|
|
26
|
+
return str(v)
|
|
27
|
+
|
|
28
|
+
sorted_params = {k: _normalize(v) for k, v in sorted(params.items())}
|
|
29
|
+
|
|
30
|
+
key_data: dict[str, Any] = {"c": context, "p": sorted_params, "r": rev}
|
|
31
|
+
if user_id is not None:
|
|
32
|
+
key_data["u"] = str(user_id)
|
|
33
|
+
|
|
34
|
+
message = json.dumps(key_data, sort_keys=True, separators=(",", ":"))
|
|
35
|
+
hmac_hex = hmac.new(
|
|
36
|
+
secret.encode("utf-8"),
|
|
37
|
+
message.encode("utf-8"),
|
|
38
|
+
hashlib.sha256,
|
|
39
|
+
).hexdigest()
|
|
40
|
+
|
|
41
|
+
return f"{CONTEXT_KEY_PREFIX}{context}:{hmac_hex}"
|
|
File without changes
|