graphite-code 0.3.0__py3-none-any.whl
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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Single-use automatic execution coordinator for approved route pools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, ClassVar, Protocol
|
|
7
|
+
|
|
8
|
+
from .approval import ApprovalAuthority, ApprovalError, SignedApproval
|
|
9
|
+
from .contracts import PublicRecord
|
|
10
|
+
from .route_pool import (
|
|
11
|
+
ApprovedRoutePool,
|
|
12
|
+
RouteAttemptEvidence,
|
|
13
|
+
RouteAuthority,
|
|
14
|
+
RoutePoolError,
|
|
15
|
+
RouteSelection,
|
|
16
|
+
select_route,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class RouteExecutionEvidence(PublicRecord):
|
|
22
|
+
candidate_id: str
|
|
23
|
+
candidate_digest: str
|
|
24
|
+
attempt_ordinal: int
|
|
25
|
+
outcome_category: str
|
|
26
|
+
input_tokens: int
|
|
27
|
+
output_tokens: int
|
|
28
|
+
duration_ms: int
|
|
29
|
+
cost_microunits: int | None
|
|
30
|
+
|
|
31
|
+
_public_fields: ClassVar[tuple[str, ...]] = (
|
|
32
|
+
"candidate_id",
|
|
33
|
+
"candidate_digest",
|
|
34
|
+
"attempt_ordinal",
|
|
35
|
+
"outcome_category",
|
|
36
|
+
"input_tokens",
|
|
37
|
+
"output_tokens",
|
|
38
|
+
"duration_ms",
|
|
39
|
+
"cost_microunits",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
if (
|
|
44
|
+
not isinstance(self.candidate_id, str)
|
|
45
|
+
or not self.candidate_id
|
|
46
|
+
or "\x00" in self.candidate_id
|
|
47
|
+
or len(self.candidate_id) > 256
|
|
48
|
+
or any(character.isspace() for character in self.candidate_id)
|
|
49
|
+
or not isinstance(self.candidate_digest, str)
|
|
50
|
+
or len(self.candidate_digest) != 64
|
|
51
|
+
or any(
|
|
52
|
+
character not in "0123456789abcdef"
|
|
53
|
+
for character in self.candidate_digest
|
|
54
|
+
)
|
|
55
|
+
or self.outcome_category
|
|
56
|
+
not in {
|
|
57
|
+
"succeeded",
|
|
58
|
+
"capacity_unavailable",
|
|
59
|
+
"provider_process_failure",
|
|
60
|
+
"provider_unavailable",
|
|
61
|
+
"provider_protocol",
|
|
62
|
+
"timeout",
|
|
63
|
+
"cancelled",
|
|
64
|
+
}
|
|
65
|
+
or isinstance(self.attempt_ordinal, bool)
|
|
66
|
+
or not isinstance(self.attempt_ordinal, int)
|
|
67
|
+
or not 1 <= self.attempt_ordinal <= 2
|
|
68
|
+
):
|
|
69
|
+
raise RoutePoolError("route_evidence_invalid")
|
|
70
|
+
for value, maximum in (
|
|
71
|
+
(self.input_tokens, 262_144),
|
|
72
|
+
(self.output_tokens, 32_768),
|
|
73
|
+
(self.duration_ms, 86_400_000),
|
|
74
|
+
):
|
|
75
|
+
if (
|
|
76
|
+
isinstance(value, bool)
|
|
77
|
+
or not isinstance(value, int)
|
|
78
|
+
or not 0 <= value <= maximum
|
|
79
|
+
):
|
|
80
|
+
raise RoutePoolError("route_evidence_invalid")
|
|
81
|
+
if self.cost_microunits is not None and (
|
|
82
|
+
isinstance(self.cost_microunits, bool)
|
|
83
|
+
or not isinstance(self.cost_microunits, int)
|
|
84
|
+
or self.cost_microunits < 0
|
|
85
|
+
or self.cost_microunits > 10**12
|
|
86
|
+
):
|
|
87
|
+
raise RoutePoolError("route_evidence_invalid")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class RouteExecutionResult:
|
|
92
|
+
candidate_id: str
|
|
93
|
+
candidate_digest: str
|
|
94
|
+
attempt_ordinal: int
|
|
95
|
+
output: str = field(repr=False, compare=False)
|
|
96
|
+
input_tokens: int
|
|
97
|
+
output_tokens: int
|
|
98
|
+
duration_ms: int
|
|
99
|
+
cost_microunits: int | None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RouteAttemptFailure(RuntimeError):
|
|
103
|
+
"""Trusted adapter failure carrying sanitized evidence only."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, evidence: RouteAttemptEvidence) -> None:
|
|
106
|
+
if not isinstance(evidence, RouteAttemptEvidence):
|
|
107
|
+
raise RoutePoolError("route_attempt_invalid")
|
|
108
|
+
self.evidence = evidence
|
|
109
|
+
super().__init__(evidence.failure_category)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AuthorityLoader(Protocol):
|
|
113
|
+
def __call__(self) -> tuple[RouteAuthority, ...]: ...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RouteRunner(Protocol):
|
|
117
|
+
def __call__(self, selection: RouteSelection) -> RouteExecutionResult: ...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
EvidenceSink = Callable[[RouteExecutionEvidence], Any]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _load_authority(loader: AuthorityLoader) -> tuple[RouteAuthority, ...]:
|
|
124
|
+
try:
|
|
125
|
+
authorities = loader()
|
|
126
|
+
except Exception:
|
|
127
|
+
raise RoutePoolError("route_authority_unavailable") from None
|
|
128
|
+
if not isinstance(authorities, tuple):
|
|
129
|
+
raise RoutePoolError("route_authority_invalid")
|
|
130
|
+
return authorities
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _persist_evidence(
|
|
134
|
+
sink: EvidenceSink,
|
|
135
|
+
evidence: RouteExecutionEvidence,
|
|
136
|
+
) -> None:
|
|
137
|
+
try:
|
|
138
|
+
sink(evidence)
|
|
139
|
+
except Exception:
|
|
140
|
+
raise RoutePoolError("route_evidence_unavailable") from None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _failure_evidence(
|
|
144
|
+
selection: RouteSelection,
|
|
145
|
+
failure: RouteAttemptEvidence,
|
|
146
|
+
) -> RouteExecutionEvidence:
|
|
147
|
+
if (
|
|
148
|
+
failure.candidate_id != selection.candidate.candidate_id
|
|
149
|
+
or failure.candidate_digest != selection.candidate.digest
|
|
150
|
+
or failure.attempt_ordinal != selection.attempt_ordinal
|
|
151
|
+
):
|
|
152
|
+
raise RoutePoolError("route_attempt_invalid")
|
|
153
|
+
return RouteExecutionEvidence(
|
|
154
|
+
candidate_id=failure.candidate_id,
|
|
155
|
+
candidate_digest=failure.candidate_digest,
|
|
156
|
+
attempt_ordinal=failure.attempt_ordinal,
|
|
157
|
+
outcome_category=failure.failure_category,
|
|
158
|
+
input_tokens=failure.input_tokens,
|
|
159
|
+
output_tokens=failure.output_tokens,
|
|
160
|
+
duration_ms=failure.duration_ms,
|
|
161
|
+
cost_microunits=failure.cost_microunits,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _validate_result(
|
|
166
|
+
pool: ApprovedRoutePool,
|
|
167
|
+
selection: RouteSelection,
|
|
168
|
+
result: RouteExecutionResult,
|
|
169
|
+
) -> RouteExecutionEvidence:
|
|
170
|
+
if (
|
|
171
|
+
not isinstance(result, RouteExecutionResult)
|
|
172
|
+
or result.candidate_id != selection.candidate.candidate_id
|
|
173
|
+
or result.candidate_digest != selection.candidate.digest
|
|
174
|
+
or result.attempt_ordinal != selection.attempt_ordinal
|
|
175
|
+
or not isinstance(result.output, str)
|
|
176
|
+
or not result.output
|
|
177
|
+
or "\x00" in result.output
|
|
178
|
+
or len(result.output) > 1_048_576
|
|
179
|
+
):
|
|
180
|
+
raise RoutePoolError("route_result_invalid")
|
|
181
|
+
values = (
|
|
182
|
+
(result.input_tokens, selection.remaining_input_tokens),
|
|
183
|
+
(result.output_tokens, selection.remaining_output_tokens),
|
|
184
|
+
(result.duration_ms, selection.remaining_duration_ms),
|
|
185
|
+
)
|
|
186
|
+
if any(
|
|
187
|
+
isinstance(value, bool)
|
|
188
|
+
or not isinstance(value, int)
|
|
189
|
+
or value < 0
|
|
190
|
+
or value > maximum
|
|
191
|
+
for value, maximum in values
|
|
192
|
+
):
|
|
193
|
+
raise RoutePoolError("route_pool_budget_exhausted")
|
|
194
|
+
if pool.max_cost_microunits is None:
|
|
195
|
+
if result.cost_microunits is not None:
|
|
196
|
+
raise RoutePoolError("route_pool_cost_invalid")
|
|
197
|
+
elif (
|
|
198
|
+
isinstance(result.cost_microunits, bool)
|
|
199
|
+
or not isinstance(result.cost_microunits, int)
|
|
200
|
+
or result.cost_microunits < 0
|
|
201
|
+
or selection.remaining_cost_microunits is None
|
|
202
|
+
or result.cost_microunits > selection.remaining_cost_microunits
|
|
203
|
+
):
|
|
204
|
+
raise RoutePoolError("route_pool_budget_exhausted")
|
|
205
|
+
return RouteExecutionEvidence(
|
|
206
|
+
result.candidate_id,
|
|
207
|
+
result.candidate_digest,
|
|
208
|
+
result.attempt_ordinal,
|
|
209
|
+
"succeeded",
|
|
210
|
+
result.input_tokens,
|
|
211
|
+
result.output_tokens,
|
|
212
|
+
result.duration_ms,
|
|
213
|
+
result.cost_microunits,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def execute_approved_route_pool(
|
|
218
|
+
*,
|
|
219
|
+
pool: ApprovedRoutePool,
|
|
220
|
+
signed_approval: SignedApproval,
|
|
221
|
+
approval_authority: ApprovalAuthority,
|
|
222
|
+
authority_loader: AuthorityLoader,
|
|
223
|
+
runner: RouteRunner,
|
|
224
|
+
evidence_sink: EvidenceSink,
|
|
225
|
+
repository_quota_tokens: int,
|
|
226
|
+
machine_quota_tokens: int,
|
|
227
|
+
now: Callable[[], int],
|
|
228
|
+
) -> RouteExecutionResult:
|
|
229
|
+
"""Consume one pool and automatically advance once on safe capacity failure."""
|
|
230
|
+
if (
|
|
231
|
+
not isinstance(pool, ApprovedRoutePool)
|
|
232
|
+
or not isinstance(approval_authority, ApprovalAuthority)
|
|
233
|
+
or not callable(authority_loader)
|
|
234
|
+
or not callable(runner)
|
|
235
|
+
or not callable(evidence_sink)
|
|
236
|
+
or not callable(now)
|
|
237
|
+
):
|
|
238
|
+
raise RoutePoolError("route_execution_invalid")
|
|
239
|
+
try:
|
|
240
|
+
approval_authority.consume(
|
|
241
|
+
signed_approval,
|
|
242
|
+
pool,
|
|
243
|
+
repository_quota_tokens=repository_quota_tokens,
|
|
244
|
+
machine_quota_tokens=machine_quota_tokens,
|
|
245
|
+
)
|
|
246
|
+
except ApprovalError:
|
|
247
|
+
raise
|
|
248
|
+
attempts: list[RouteAttemptEvidence] = []
|
|
249
|
+
while len(attempts) < pool.max_attempts:
|
|
250
|
+
try:
|
|
251
|
+
current_time = now()
|
|
252
|
+
except Exception:
|
|
253
|
+
raise RoutePoolError("route_pool_time_invalid") from None
|
|
254
|
+
selection = select_route(
|
|
255
|
+
pool,
|
|
256
|
+
_load_authority(authority_loader),
|
|
257
|
+
tuple(attempts),
|
|
258
|
+
now=current_time,
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
result = runner(selection)
|
|
262
|
+
except RouteAttemptFailure as exc:
|
|
263
|
+
failure = exc.evidence
|
|
264
|
+
evidence = _failure_evidence(selection, failure)
|
|
265
|
+
_persist_evidence(evidence_sink, evidence)
|
|
266
|
+
attempts.append(failure)
|
|
267
|
+
if len(attempts) >= pool.max_attempts:
|
|
268
|
+
raise RoutePoolError("route_pool_attempts_exhausted") from None
|
|
269
|
+
continue
|
|
270
|
+
except Exception:
|
|
271
|
+
raise RoutePoolError("route_attempt_failed") from None
|
|
272
|
+
evidence = _validate_result(pool, selection, result)
|
|
273
|
+
_persist_evidence(evidence_sink, evidence)
|
|
274
|
+
return result
|
|
275
|
+
raise RoutePoolError("route_pool_attempts_exhausted")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Minimal in-house JSON-Schema validator for governed structured output.
|
|
2
|
+
|
|
3
|
+
The governed output schemas are sha256-pinned constants (not attacker input), so this
|
|
4
|
+
validator's job is to enforce the constraints those schemas express, not to police keyword
|
|
5
|
+
novelty. It therefore fails closed ONLY on combinator keywords (anyOf/oneOf/allOf/not/$ref/
|
|
6
|
+
patternProperties/...) -- keywords that change what "valid" means and that this validator
|
|
7
|
+
cannot safely approximate. All other (refinement/annotation) keywords are tolerated:
|
|
8
|
+
matches_schema enforces the ones it understands and ignores the rest, which can only
|
|
9
|
+
under-enforce a single constraint, never accept a structurally wrong shape. Zero third-party
|
|
10
|
+
dependencies by design.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
# Keywords that combine/redirect subschemas and thus change what "valid" means. This
|
|
17
|
+
# validator cannot approximate them, so a schema using any of them fails closed at
|
|
18
|
+
# is_supported_schema -> the executor rejects it with request_invalid before any call,
|
|
19
|
+
# rather than being silently under-validated.
|
|
20
|
+
_COMBINATORS = frozenset({
|
|
21
|
+
"anyOf", "oneOf", "allOf", "not", "$ref", "$dynamicRef",
|
|
22
|
+
"if", "then", "else", "patternProperties", "propertyNames",
|
|
23
|
+
"dependencies", "dependentSchemas", "dependentRequired",
|
|
24
|
+
"unevaluatedProperties", "unevaluatedItems", "contains",
|
|
25
|
+
"prefixItems", "additionalItems",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_supported_schema(schema: object) -> bool:
|
|
30
|
+
"""True unless the schema uses a combinator keyword this validator cannot safely
|
|
31
|
+
approximate (or a non-bool additionalProperties, which is a schema-valued shape
|
|
32
|
+
change). Refinement/annotation keywords are tolerated. Governed schemas are pinned
|
|
33
|
+
constants, so this gate catches a shape we would mis-validate, not unknown-but-harmless
|
|
34
|
+
keys."""
|
|
35
|
+
if not isinstance(schema, dict) or not schema:
|
|
36
|
+
return False
|
|
37
|
+
if not _COMBINATORS.isdisjoint(schema):
|
|
38
|
+
return False
|
|
39
|
+
if not isinstance(schema.get("additionalProperties", False), bool):
|
|
40
|
+
return False
|
|
41
|
+
pattern = schema.get("pattern")
|
|
42
|
+
if isinstance(pattern, str):
|
|
43
|
+
try:
|
|
44
|
+
re.compile(pattern)
|
|
45
|
+
except re.error:
|
|
46
|
+
return False
|
|
47
|
+
properties = schema.get("properties")
|
|
48
|
+
if properties is not None:
|
|
49
|
+
if not isinstance(properties, dict):
|
|
50
|
+
return False
|
|
51
|
+
if not all(isinstance(k, str) and is_supported_schema(v) for k, v in properties.items()):
|
|
52
|
+
return False
|
|
53
|
+
items = schema.get("items")
|
|
54
|
+
if items is not None and not is_supported_schema(items):
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def matches_schema(value: object, schema: object) -> bool:
|
|
60
|
+
"""True iff value conforms to every constraint in schema that this validator
|
|
61
|
+
understands; unknown refinement keywords are ignored (never over-reject).
|
|
62
|
+
|
|
63
|
+
Precondition: schema must already have passed is_supported_schema -- callers gate the
|
|
64
|
+
schema once, then match many values against it. Given such a schema this never raises.
|
|
65
|
+
Untrusted VALUES are always handled safely: a non-conforming (or wrong-typed) value
|
|
66
|
+
returns False, never raises.
|
|
67
|
+
"""
|
|
68
|
+
if not isinstance(schema, dict):
|
|
69
|
+
return False
|
|
70
|
+
type_value = schema.get("type")
|
|
71
|
+
if type_value is not None and not _matches_type(value, type_value):
|
|
72
|
+
return False
|
|
73
|
+
if "const" in schema and not _json_equal(value, schema["const"]):
|
|
74
|
+
return False
|
|
75
|
+
if "enum" in schema and not any(_json_equal(value, member) for member in schema["enum"]):
|
|
76
|
+
return False
|
|
77
|
+
if isinstance(value, str) and not _matches_string(value, schema):
|
|
78
|
+
return False
|
|
79
|
+
if isinstance(value, dict) and _is_object_schema(schema) and not _matches_object(value, schema):
|
|
80
|
+
return False
|
|
81
|
+
if isinstance(value, list) and _is_array_schema(schema) and not _matches_array(value, schema):
|
|
82
|
+
return False
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_object_schema(schema: dict) -> bool:
|
|
87
|
+
return schema.get("type") == "object" or any(
|
|
88
|
+
key in schema for key in ("properties", "required", "additionalProperties")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_array_schema(schema: dict) -> bool:
|
|
93
|
+
return schema.get("type") == "array" or any(
|
|
94
|
+
key in schema for key in ("items", "minItems", "maxItems")
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _matches_string(value: str, schema: dict) -> bool:
|
|
99
|
+
pattern = schema.get("pattern")
|
|
100
|
+
if isinstance(pattern, str) and re.fullmatch(pattern, value) is None:
|
|
101
|
+
return False
|
|
102
|
+
maximum = schema.get("maxLength")
|
|
103
|
+
if _is_bound(maximum) and len(value) > maximum:
|
|
104
|
+
return False
|
|
105
|
+
minimum = schema.get("minLength")
|
|
106
|
+
if _is_bound(minimum) and len(value) < minimum:
|
|
107
|
+
return False
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _matches_object(value: dict, schema: dict) -> bool:
|
|
112
|
+
properties = schema.get("properties", {})
|
|
113
|
+
if not isinstance(properties, dict):
|
|
114
|
+
return False
|
|
115
|
+
if not all(key in value for key in schema.get("required", [])):
|
|
116
|
+
return False
|
|
117
|
+
if schema.get("additionalProperties", False) is False and not set(value) <= set(properties):
|
|
118
|
+
return False
|
|
119
|
+
return all(
|
|
120
|
+
matches_schema(value[key], subschema)
|
|
121
|
+
for key, subschema in properties.items()
|
|
122
|
+
if key in value
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _matches_array(value: list, schema: dict) -> bool:
|
|
127
|
+
minimum = schema.get("minItems")
|
|
128
|
+
if _is_bound(minimum) and len(value) < minimum:
|
|
129
|
+
return False
|
|
130
|
+
maximum = schema.get("maxItems")
|
|
131
|
+
if _is_bound(maximum) and len(value) > maximum:
|
|
132
|
+
return False
|
|
133
|
+
items = schema.get("items")
|
|
134
|
+
if isinstance(items, dict):
|
|
135
|
+
return all(matches_schema(element, items) for element in value)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_bound(value: object) -> bool:
|
|
140
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _matches_type(value: object, type_value: object) -> bool:
|
|
144
|
+
if isinstance(type_value, list):
|
|
145
|
+
return any(_matches_type(value, member) for member in type_value)
|
|
146
|
+
if type_value == "string":
|
|
147
|
+
return isinstance(value, str)
|
|
148
|
+
if type_value == "integer":
|
|
149
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
150
|
+
if type_value == "number":
|
|
151
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
152
|
+
if type_value == "boolean":
|
|
153
|
+
return isinstance(value, bool)
|
|
154
|
+
if type_value == "null":
|
|
155
|
+
return value is None
|
|
156
|
+
if type_value == "object":
|
|
157
|
+
return isinstance(value, dict)
|
|
158
|
+
if type_value == "array":
|
|
159
|
+
return isinstance(value, list)
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _json_equal(value: object, target: object) -> bool:
|
|
164
|
+
"""Equality that does not conflate True==1, 1==1.0, or 1.0==1 (JSON-kind aware)."""
|
|
165
|
+
if isinstance(value, bool) or isinstance(target, bool):
|
|
166
|
+
return value is target
|
|
167
|
+
if isinstance(value, (int, float)) and isinstance(target, (int, float)):
|
|
168
|
+
return type(value) is type(target) and value == target
|
|
169
|
+
return value == target
|