verdik 0.0.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,10 @@
1
+ .DS_Store
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .ty/
8
+ dist/
9
+ build/
10
+ *.egg-info/
verdik-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: verdik
3
+ Version: 0.0.0
4
+ Summary: Pure, decision-logged policy evaluation for MissionCtrl and agent-fabric authorization seams.
5
+ Project-URL: Homepage, https://github.com/cachetronaut/verdik
6
+ Project-URL: Repository, https://github.com/cachetronaut/verdik
7
+ Project-URL: Issues, https://github.com/cachetronaut/verdik/issues
8
+ License: MIT
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # verdik
13
+
14
+ Python implementation of Verdik.
15
+
16
+ For product-level context, shared contracts, and cross-language repository information, see the public repository: https://github.com/cachetronaut/verdik.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ pip install verdik
22
+ ```
23
+
24
+ ## Import
25
+
26
+ ```python
27
+ import verdik
28
+ ```
29
+
30
+ ## Development
31
+
32
+ Run from `py/`:
33
+
34
+ ```sh
35
+ uv sync --dev
36
+ uv run --with ruff ruff check .
37
+ uv run --with ruff ruff format --check .
38
+ uv run --with ty ty check
39
+ uv run --with pytest --with pytest-asyncio python -m pytest
40
+ ```
41
+
42
+ ## License
43
+
44
+ MIT
verdik-0.0.0/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # verdik
2
+
3
+ Python implementation of Verdik.
4
+
5
+ For product-level context, shared contracts, and cross-language repository information, see the public repository: https://github.com/cachetronaut/verdik.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install verdik
11
+ ```
12
+
13
+ ## Import
14
+
15
+ ```python
16
+ import verdik
17
+ ```
18
+
19
+ ## Development
20
+
21
+ Run from `py/`:
22
+
23
+ ```sh
24
+ uv sync --dev
25
+ uv run --with ruff ruff check .
26
+ uv run --with ruff ruff format --check .
27
+ uv run --with ty ty check
28
+ uv run --with pytest --with pytest-asyncio python -m pytest
29
+ ```
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "verdik"
3
+ version = "0.0.0"
4
+ description = "Pure, decision-logged policy evaluation for MissionCtrl and agent-fabric authorization seams."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ dependencies = []
9
+
10
+ [project.urls]
11
+ Homepage = "https://github.com/cachetronaut/verdik"
12
+ Repository = "https://github.com/cachetronaut/verdik"
13
+ Issues = "https://github.com/cachetronaut/verdik/issues"
14
+
15
+ [build-system]
16
+ requires = ["hatchling"]
17
+ build-backend = "hatchling.build"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/verdik"]
21
+
22
+ [dependency-groups]
23
+ dev = ["pytest>=8", "ruff>=0.6", "ty>=0.0.1a8"]
24
+
25
+ [tool.ruff]
26
+ line-length = 100
27
+ target-version = "py311"
28
+
29
+ [tool.ruff.lint]
30
+ select = ["E", "F", "I", "UP", "B", "SIM"]
31
+
32
+ [tool.ty.environment]
33
+ extra-paths = ["src"]
@@ -0,0 +1,39 @@
1
+ from .core import (
2
+ Condition,
3
+ DecisionLog,
4
+ EvaluationOptions,
5
+ LocalPolicyEngine,
6
+ MatchedRule,
7
+ Obligation,
8
+ PolicyDecision,
9
+ PolicyInput,
10
+ Rule,
11
+ allow_all_rule,
12
+ allow_within_token_policy,
13
+ canonicalize,
14
+ create_local_policy_engine,
15
+ evaluate,
16
+ layer_rules,
17
+ matches_condition,
18
+ read_path,
19
+ )
20
+
21
+ __all__ = [
22
+ "Condition",
23
+ "DecisionLog",
24
+ "EvaluationOptions",
25
+ "LocalPolicyEngine",
26
+ "MatchedRule",
27
+ "Obligation",
28
+ "PolicyDecision",
29
+ "PolicyInput",
30
+ "Rule",
31
+ "allow_all_rule",
32
+ "allow_within_token_policy",
33
+ "canonicalize",
34
+ "create_local_policy_engine",
35
+ "evaluate",
36
+ "layer_rules",
37
+ "matches_condition",
38
+ "read_path",
39
+ ]
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Literal, NotRequired, TypedDict
6
+
7
+ DefaultEffect = Literal["allow", "deny"]
8
+ ConditionOp = Literal[
9
+ "exists",
10
+ "eq",
11
+ "neq",
12
+ "in",
13
+ "contains",
14
+ "starts_with",
15
+ "lte",
16
+ "gte",
17
+ "lt",
18
+ "gt",
19
+ ]
20
+
21
+
22
+ class Condition(TypedDict, total=False):
23
+ all: list[Condition]
24
+ any: list[Condition]
25
+ not_: Condition
26
+ field: str
27
+ op: ConditionOp
28
+ value: Any
29
+
30
+
31
+ class Obligation(TypedDict):
32
+ kind: str
33
+ detail: NotRequired[dict[str, Any]]
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Rule:
38
+ id: str
39
+ effect: DefaultEffect
40
+ when: Condition
41
+ reason: str
42
+ obligations: list[Obligation] = field(default_factory=list)
43
+
44
+
45
+ PolicyInput = dict[str, Any]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class PolicyDecision:
50
+ allow: bool
51
+ reason: str
52
+ obligations: list[Obligation] = field(default_factory=list)
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class MatchedRule:
57
+ id: str
58
+ effect: DefaultEffect
59
+ reason: str
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class DecisionLog:
64
+ matched: list[MatchedRule]
65
+ obligations: list[Obligation]
66
+ binding_rule_id: str | None = None
67
+ defaulted: bool = False
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class EvaluationOptions:
72
+ default_effect: DefaultEffect = "deny"
73
+ default_reason: str | None = None
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class EvaluationResult:
78
+ decision: PolicyDecision
79
+ log: DecisionLog
80
+
81
+
82
+ class LocalPolicyEngine:
83
+ def __init__(self, rules: list[Rule], options: EvaluationOptions | None = None) -> None:
84
+ self._rules = rules
85
+ self._options = options or EvaluationOptions()
86
+
87
+ async def decide(self, value: PolicyInput) -> PolicyDecision:
88
+ return evaluate(self._rules, value, self._options).decision
89
+
90
+
91
+ def evaluate(
92
+ rules: list[Rule],
93
+ value: PolicyInput,
94
+ options: EvaluationOptions | None = None,
95
+ ) -> EvaluationResult:
96
+ options = options or EvaluationOptions()
97
+ matched = [rule for rule in rules if matches_condition(rule.when, value)]
98
+ denied = next((rule for rule in matched if rule.effect == "deny"), None)
99
+ allow_rules = [rule for rule in matched if rule.effect == "allow"]
100
+ obligations = _unique_obligations(
101
+ obligation for rule in allow_rules for obligation in rule.obligations
102
+ )
103
+
104
+ if denied is not None:
105
+ return EvaluationResult(
106
+ PolicyDecision(False, denied.reason),
107
+ _build_log(matched, denied.id, False, obligations),
108
+ )
109
+
110
+ first_allow = allow_rules[0] if allow_rules else None
111
+ if first_allow is not None:
112
+ return EvaluationResult(
113
+ PolicyDecision(True, first_allow.reason, obligations),
114
+ _build_log(matched, first_allow.id, False, obligations),
115
+ )
116
+
117
+ effect = options.default_effect
118
+ return EvaluationResult(
119
+ PolicyDecision(effect == "allow", options.default_reason or f"default_{effect}"),
120
+ _build_log([], None, True, []),
121
+ )
122
+
123
+
124
+ def matches_condition(condition: Condition, value: PolicyInput) -> bool:
125
+ if "all" in condition:
126
+ return all(matches_condition(child, value) for child in condition["all"])
127
+ if "any" in condition:
128
+ return any(matches_condition(child, value) for child in condition["any"])
129
+ if "not_" in condition:
130
+ return not matches_condition(condition["not_"], value)
131
+
132
+ actual = read_path(value, condition["field"])
133
+ expected = condition.get("value")
134
+ op = condition["op"]
135
+ if op == "exists":
136
+ return actual is not None
137
+ if op == "eq":
138
+ return canonicalize(actual) == canonicalize(expected)
139
+ if op == "neq":
140
+ return canonicalize(actual) != canonicalize(expected)
141
+ if op == "in":
142
+ return isinstance(expected, list) and any(
143
+ canonicalize(item) == canonicalize(actual) for item in expected
144
+ )
145
+ if op == "contains":
146
+ if isinstance(actual, list):
147
+ return any(canonicalize(item) == canonicalize(expected) for item in actual)
148
+ return isinstance(actual, str) and isinstance(expected, str) and expected in actual
149
+ if op == "starts_with":
150
+ return isinstance(actual, str) and isinstance(expected, str) and actual.startswith(expected)
151
+ if op == "lte":
152
+ return _compare_number(actual, expected, lambda left, right: left <= right)
153
+ if op == "gte":
154
+ return _compare_number(actual, expected, lambda left, right: left >= right)
155
+ if op == "lt":
156
+ return _compare_number(actual, expected, lambda left, right: left < right)
157
+ if op == "gt":
158
+ return _compare_number(actual, expected, lambda left, right: left > right)
159
+
160
+
161
+ def read_path(value: PolicyInput, path: str) -> Any:
162
+ current: Any = value
163
+ for part in path.split("."):
164
+ if not isinstance(current, dict):
165
+ return None
166
+ current = current.get(part)
167
+ return current
168
+
169
+
170
+ def canonicalize(value: Any) -> str:
171
+ return json.dumps(value, sort_keys=True, separators=(",", ":"))
172
+
173
+
174
+ def create_local_policy_engine(
175
+ rules: list[Rule], options: EvaluationOptions | None = None
176
+ ) -> LocalPolicyEngine:
177
+ return LocalPolicyEngine(rules, options)
178
+
179
+
180
+ def allow_all_rule(reason: str = "within_token") -> Rule:
181
+ return Rule(
182
+ id="allow_within_token",
183
+ effect="allow",
184
+ when={"field": "scope.action", "op": "exists"},
185
+ reason=reason,
186
+ )
187
+
188
+
189
+ def allow_within_token_policy() -> LocalPolicyEngine:
190
+ return create_local_policy_engine([allow_all_rule()], EvaluationOptions(default_effect="deny"))
191
+
192
+
193
+ def layer_rules(*layers: list[Rule]) -> list[Rule]:
194
+ return [rule for layer in layers for rule in layer]
195
+
196
+
197
+ def _build_log(
198
+ rules: list[Rule],
199
+ binding_rule_id: str | None,
200
+ defaulted: bool,
201
+ obligations: list[Obligation],
202
+ ) -> DecisionLog:
203
+ return DecisionLog(
204
+ matched=[MatchedRule(rule.id, rule.effect, rule.reason) for rule in rules],
205
+ binding_rule_id=binding_rule_id,
206
+ defaulted=defaulted,
207
+ obligations=obligations,
208
+ )
209
+
210
+
211
+ def _unique_obligations(obligations: Any) -> list[Obligation]:
212
+ seen: set[str] = set()
213
+ out: list[Obligation] = []
214
+ for obligation in obligations:
215
+ key = canonicalize(obligation)
216
+ if key in seen:
217
+ continue
218
+ seen.add(key)
219
+ out.append(obligation)
220
+ return out
221
+
222
+
223
+ def _compare_number(left: Any, right: Any, compare: Any) -> bool:
224
+ return isinstance(left, int | float) and isinstance(right, int | float) and compare(left, right)
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from verdik import (
6
+ EvaluationOptions,
7
+ Rule,
8
+ allow_within_token_policy,
9
+ canonicalize,
10
+ create_local_policy_engine,
11
+ evaluate,
12
+ layer_rules,
13
+ matches_condition,
14
+ )
15
+
16
+
17
+ def test_evaluate_allows_with_obligations() -> None:
18
+ result = evaluate(
19
+ [
20
+ Rule(
21
+ id="allow_echo",
22
+ effect="allow",
23
+ when={"field": "scope.resource", "op": "eq", "value": "tool.echo"},
24
+ obligations=[{"kind": "emit_audit", "detail": {"level": "decision"}}],
25
+ reason="echo_allowed",
26
+ ),
27
+ Rule(
28
+ id="tag_dev",
29
+ effect="allow",
30
+ when={"field": "principal.claims.role", "op": "eq", "value": "developer"},
31
+ obligations=[{"kind": "emit_audit", "detail": {"level": "decision"}}],
32
+ reason="developer",
33
+ ),
34
+ ],
35
+ _input(),
36
+ )
37
+
38
+ assert result.decision.allow
39
+ assert result.decision.reason == "echo_allowed"
40
+ assert result.decision.obligations == [{"kind": "emit_audit", "detail": {"level": "decision"}}]
41
+ assert result.log.binding_rule_id == "allow_echo"
42
+
43
+
44
+ def test_deny_overrides_matching_allow() -> None:
45
+ result = evaluate(
46
+ [
47
+ Rule(
48
+ id="allow_all",
49
+ effect="allow",
50
+ when={"field": "scope.action", "op": "exists"},
51
+ reason="allowed",
52
+ ),
53
+ Rule(
54
+ id="deny_pii",
55
+ effect="deny",
56
+ when={"field": "claims.constraints.pii_export", "op": "eq", "value": True},
57
+ reason="pii_requires_approval",
58
+ ),
59
+ ],
60
+ _input(),
61
+ )
62
+
63
+ assert not result.decision.allow
64
+ assert result.decision.reason == "pii_requires_approval"
65
+ assert result.log.binding_rule_id == "deny_pii"
66
+
67
+
68
+ def test_default_effect_is_configurable() -> None:
69
+ assert not evaluate([], _input()).decision.allow
70
+ assert evaluate([], _input(), EvaluationOptions(default_effect="allow")).decision.allow
71
+
72
+
73
+ def test_nested_conditions_and_canonicalization() -> None:
74
+ assert matches_condition(
75
+ {
76
+ "all": [
77
+ {"field": "scope.resource", "op": "starts_with", "value": "tool."},
78
+ {"field": "estimate.estimate.model_cost_usd", "op": "lte", "value": 1},
79
+ {"not_": {"field": "principal.kind", "op": "eq", "value": "agent"}},
80
+ ]
81
+ },
82
+ _input(),
83
+ )
84
+ assert canonicalize({"b": 2, "a": 1}) == '{"a":1,"b":2}'
85
+
86
+
87
+ def test_local_policy_engine_and_layering() -> None:
88
+ async def run() -> None:
89
+ engine = create_local_policy_engine(
90
+ layer_rules(
91
+ [
92
+ Rule(
93
+ id="allow",
94
+ effect="allow",
95
+ when={"field": "scope.action", "op": "exists"},
96
+ reason="ok",
97
+ )
98
+ ],
99
+ [
100
+ Rule(
101
+ id="deny",
102
+ effect="deny",
103
+ when={"field": "scope.resource", "op": "eq", "value": "tool.payments"},
104
+ reason="payments_blocked",
105
+ )
106
+ ],
107
+ )
108
+ )
109
+ decision = await engine.decide(
110
+ {
111
+ "principal": {},
112
+ "claims": {},
113
+ "scope": {"action": "charge", "resource": "tool.payments"},
114
+ "estimate": {},
115
+ }
116
+ )
117
+ assert not decision.allow
118
+ assert decision.reason == "payments_blocked"
119
+
120
+ permissive = await allow_within_token_policy().decide(_input())
121
+ assert permissive.allow
122
+ assert permissive.reason == "within_token"
123
+
124
+ asyncio.run(run())
125
+
126
+
127
+ def _input() -> dict[str, object]:
128
+ return {
129
+ "principal": {"id": "dev-user", "kind": "user", "claims": {"role": "developer"}},
130
+ "claims": {"depth": 1, "constraints": {"pii_export": True}},
131
+ "scope": {"action": "echo", "resource": "tool.echo"},
132
+ "context": {"runId": "run_1"},
133
+ "estimate": {"estimate": {"tool_calls": 1, "model_cost_usd": 0.01}},
134
+ }
verdik-0.0.0/uv.lock ADDED
@@ -0,0 +1,135 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "iniconfig"
16
+ version = "2.3.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "packaging"
25
+ version = "26.2"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "pluggy"
34
+ version = "1.6.0"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "verdik"
43
+ version = "0.0.0"
44
+ source = { editable = "." }
45
+
46
+ [package.dev-dependencies]
47
+ dev = [
48
+ { name = "pytest" },
49
+ { name = "ruff" },
50
+ { name = "ty" },
51
+ ]
52
+
53
+ [package.metadata]
54
+
55
+ [package.metadata.requires-dev]
56
+ dev = [
57
+ { name = "pytest", specifier = ">=8" },
58
+ { name = "ruff", specifier = ">=0.6" },
59
+ { name = "ty", specifier = ">=0.0.1a8" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "pygments"
64
+ version = "2.20.0"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
67
+ wheels = [
68
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
69
+ ]
70
+
71
+ [[package]]
72
+ name = "pytest"
73
+ version = "9.0.3"
74
+ source = { registry = "https://pypi.org/simple" }
75
+ dependencies = [
76
+ { name = "colorama", marker = "sys_platform == 'win32'" },
77
+ { name = "iniconfig" },
78
+ { name = "packaging" },
79
+ { name = "pluggy" },
80
+ { name = "pygments" },
81
+ ]
82
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
83
+ wheels = [
84
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "ruff"
89
+ version = "0.15.16"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
92
+ wheels = [
93
+ { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
94
+ { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
95
+ { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
96
+ { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
97
+ { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
98
+ { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
99
+ { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
100
+ { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
101
+ { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
102
+ { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
103
+ { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
104
+ { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
105
+ { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
106
+ { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
107
+ { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
108
+ { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
109
+ { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
110
+ ]
111
+
112
+ [[package]]
113
+ name = "ty"
114
+ version = "0.0.43"
115
+ source = { registry = "https://pypi.org/simple" }
116
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
117
+ wheels = [
118
+ { url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
119
+ { url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
120
+ { url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
121
+ { url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
122
+ { url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
123
+ { url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
124
+ { url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
125
+ { url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
126
+ { url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
127
+ { url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
128
+ { url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
129
+ { url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
130
+ { url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
131
+ { url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
132
+ { url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
133
+ { url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
134
+ { url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
135
+ ]