@adhd/apigen-python-env 0.1.5 → 0.2.1

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,628 @@
1
+ """
2
+ apigen_logical.py — Python host binding for the apigen logical-types system.
3
+
4
+ Mirrors the TypeScript well-known scalar codecs (packages/apigen/logical/src/lib/codecs/)
5
+ and provides a schema-walking encode/decode pair so the SAME canonical wire bytes
6
+ round-trip identically between TS and Python.
7
+
8
+ Canonical wire encodings (DESIGN §3):
9
+ date-time → RFC 3339 UTC string (datetime.isoformat with 'Z' suffix)
10
+ int64 → decimal string ("9007199254740993")
11
+ decimal → decimal string ("123.456")
12
+ byte → standard base64 + padding ("SGVsbG8=")
13
+ uuid → lowercase hyphenated ("550e8400-e29b-41d4-a716-446655440000")
14
+ number-special → "NaN" / "Infinity" / "-Infinity"
15
+
16
+ Wire contract note (DESIGN §4.6):
17
+ Encode is value-driven: json.dumps(..., cls=ApigenEncoder) / encode_value().
18
+ Decode is schema-driven and explicit: native json.object_hook is value-only
19
+ and cannot see the schema, so decode() walks the schema and wire in lockstep.
20
+
21
+ All types use Python stdlib only (datetime, decimal, uuid, base64, math).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import base64
27
+ import json
28
+ import math
29
+ from datetime import datetime, timezone
30
+ from decimal import Decimal
31
+ from typing import Any
32
+ from uuid import UUID
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Type aliases
36
+ # ---------------------------------------------------------------------------
37
+
38
+ # Wire is any JSON-serialisable value (str | int | float | bool | None | list | dict).
39
+ Wire = Any
40
+ # SchemaNode is a dict representing a resolved JSON Schema fragment.
41
+ SchemaNode = dict[str, Any]
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Scalar codec helpers — encode (native → wire)
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def encode_datetime(value: datetime) -> str:
49
+ """Encode a datetime to RFC 3339 UTC string with ms precision.
50
+
51
+ Mirrors TS `Date.prototype.toISOString()` which always emits the 'Z' suffix
52
+ and at least millisecond precision.
53
+
54
+ Args:
55
+ value: A datetime object (naive is treated as UTC; aware is converted).
56
+
57
+ Returns:
58
+ RFC 3339 UTC string, e.g. "2024-01-15T12:34:56.789Z".
59
+ """
60
+ # Ensure UTC.
61
+ if value.tzinfo is not None:
62
+ value = value.astimezone(timezone.utc)
63
+ else:
64
+ # Naive → assume UTC (same semantics as JS new Date(...).toISOString()).
65
+ value = value.replace(tzinfo=timezone.utc)
66
+
67
+ # Format matches JS toISOString(): YYYY-MM-DDTHH:MM:SS.mmmZ
68
+ # Python isoformat() with 'microseconds' precision gives 6 fractional digits;
69
+ # we truncate to 3 (ms) and append 'Z' to match the canonical TS wire.
70
+ base = value.strftime("%Y-%m-%dT%H:%M:%S")
71
+ ms = value.microsecond // 1000
72
+ return f"{base}.{ms:03d}Z"
73
+
74
+
75
+ def encode_int64(value: int) -> str:
76
+ """Encode a Python int as a decimal string.
77
+
78
+ Mirrors TS `String(bigint)`. Avoids f64 precision loss for values beyond
79
+ Number.MAX_SAFE_INTEGER.
80
+
81
+ Args:
82
+ value: Any Python int (int64 range or beyond).
83
+
84
+ Returns:
85
+ Decimal string, e.g. "9007199254740993".
86
+ """
87
+ return str(int(value))
88
+
89
+
90
+ def encode_decimal(value: Decimal | str) -> str:
91
+ """Encode a Decimal (or already-valid decimal string) to a decimal string.
92
+
93
+ Mirrors TS `value as string` (branded-string passthrough for the default
94
+ zero-dep mode). Never emits a float representation.
95
+
96
+ Args:
97
+ value: A Python `decimal.Decimal` or a plain decimal string.
98
+
99
+ Returns:
100
+ Decimal string, e.g. "123.456".
101
+ """
102
+ return str(value)
103
+
104
+
105
+ def encode_bytes(value: bytes | bytearray) -> str:
106
+ """Encode bytes to standard base64 (RFC 4648 §4) with padding.
107
+
108
+ Uses the standard alphabet: '+' and '/', NOT the URL-safe '-' and '_'.
109
+ Padding ('=') is always included.
110
+
111
+ Mirrors TS `Buffer.from(value).toString('base64')`.
112
+
113
+ Args:
114
+ value: A bytes or bytearray object.
115
+
116
+ Returns:
117
+ Standard base64 string with padding, e.g. "SGVsbG8=".
118
+ """
119
+ return base64.b64encode(bytes(value)).decode("ascii")
120
+
121
+
122
+ def encode_uuid(value: UUID | str) -> str:
123
+ """Encode a UUID to lowercase hyphenated RFC 4122 form.
124
+
125
+ Mirrors TS `value.toLowerCase()`.
126
+
127
+ Args:
128
+ value: A `uuid.UUID` instance or a UUID string (any case).
129
+
130
+ Returns:
131
+ Lowercase hyphenated UUID string, e.g. "550e8400-e29b-41d4-a716-446655440000".
132
+ """
133
+ return str(value).lower()
134
+
135
+
136
+ def encode_number_special(value: float) -> str | float:
137
+ """Encode a float, mapping non-finite values to string sentinels.
138
+
139
+ Mirrors TS codec: NaN → "NaN", Infinity → "Infinity", -Infinity → "-Infinity",
140
+ finite numbers pass through unchanged.
141
+
142
+ Note: JSON.stringify maps NaN/±Infinity to null by default; this codec
143
+ overrides that with ProtoJSON-compatible string sentinels.
144
+
145
+ Args:
146
+ value: Any Python float.
147
+
148
+ Returns:
149
+ "NaN", "Infinity", or "-Infinity" for non-finite values;
150
+ the original float for finite values.
151
+ """
152
+ if math.isnan(value):
153
+ return "NaN"
154
+ if math.isinf(value):
155
+ return "Infinity" if value > 0 else "-Infinity"
156
+ return value
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Scalar codec helpers — decode (wire → native)
161
+ # ---------------------------------------------------------------------------
162
+
163
+ def decode_datetime(wire: Wire) -> datetime:
164
+ """Decode an RFC 3339 UTC string to a timezone-aware datetime in UTC.
165
+
166
+ The decoded value is always UTC (timezone.utc), matching the TS host's
167
+ `new Date(wire)` which always normalises to UTC.
168
+
169
+ Args:
170
+ wire: A RFC 3339 UTC string, e.g. "2024-01-15T12:34:56.789Z".
171
+
172
+ Returns:
173
+ datetime in UTC.
174
+
175
+ Raises:
176
+ TypeError: if wire is not a string.
177
+ ValueError: if the string is not a valid ISO-8601 datetime.
178
+ """
179
+ if not isinstance(wire, str):
180
+ raise TypeError(f"[date-time] expected a string on the wire, got {type(wire).__name__}")
181
+ # Python's fromisoformat (3.7+) handles 'Z' only from 3.11+.
182
+ # For compatibility with 3.9/3.10 replace 'Z' → '+00:00'.
183
+ normalised = wire.replace("Z", "+00:00")
184
+ dt = datetime.fromisoformat(normalised)
185
+ return dt.astimezone(timezone.utc)
186
+
187
+
188
+ def decode_int64(wire: Wire) -> int:
189
+ """Decode a decimal string to a Python int (exact, no f64 truncation).
190
+
191
+ Args:
192
+ wire: A decimal string, e.g. "9007199254740993".
193
+
194
+ Returns:
195
+ Python int with full precision.
196
+
197
+ Raises:
198
+ TypeError: if wire is not a string.
199
+ ValueError: if the string is not a valid integer representation.
200
+ """
201
+ if not isinstance(wire, str):
202
+ raise TypeError(f"[int64] expected a string on the wire, got {type(wire).__name__}")
203
+ return int(wire)
204
+
205
+
206
+ def decode_decimal(wire: Wire) -> Decimal:
207
+ """Decode a decimal string to a Python `decimal.Decimal`.
208
+
209
+ Args:
210
+ wire: A decimal string, e.g. "123.456".
211
+
212
+ Returns:
213
+ `decimal.Decimal` preserving full precision.
214
+
215
+ Raises:
216
+ TypeError: if wire is not a string.
217
+ """
218
+ if not isinstance(wire, str):
219
+ raise TypeError(f"[decimal] expected a string on the wire, got {type(wire).__name__}")
220
+ return Decimal(wire)
221
+
222
+
223
+ def decode_bytes(wire: Wire) -> bytes:
224
+ """Decode a standard base64 string (RFC 4648 §4) to bytes.
225
+
226
+ Validates that the input uses the standard alphabet (not URL-safe).
227
+
228
+ Args:
229
+ wire: A standard base64 string with optional '=' padding.
230
+
231
+ Returns:
232
+ Decoded bytes.
233
+
234
+ Raises:
235
+ TypeError: if wire is not a string.
236
+ ValueError: if the string is not valid standard base64.
237
+ """
238
+ if not isinstance(wire, str):
239
+ raise TypeError(f"[byte] expected a base64 string on the wire, got {type(wire).__name__}")
240
+ # Validate standard base64 alphabet — disallow URL-safe characters ('-', '_').
241
+ import re
242
+ if not re.fullmatch(r"[A-Za-z0-9+/]*={0,2}", wire):
243
+ raise ValueError(
244
+ f"[byte] wire value is not standard base64 (RFC 4648 §4): {wire!r}. "
245
+ "Standard base64 uses '+' and '/' (not '-' and '_')."
246
+ )
247
+ return base64.b64decode(wire)
248
+
249
+
250
+ def decode_uuid(wire: Wire) -> str:
251
+ """Decode a UUID wire string, validating and normalising to lowercase.
252
+
253
+ Args:
254
+ wire: A UUID string (any case), e.g. "550E8400-...".
255
+
256
+ Returns:
257
+ Lowercase hyphenated UUID string.
258
+
259
+ Raises:
260
+ TypeError: if wire is not a string.
261
+ ValueError: if the string is not a valid RFC 4122 UUID.
262
+ """
263
+ if not isinstance(wire, str):
264
+ raise TypeError(f"[uuid] expected a string on the wire, got {type(wire).__name__}")
265
+ # UUID() constructor validates and normalises.
266
+ return str(UUID(wire)).lower()
267
+
268
+
269
+ def decode_number_special(wire: Wire) -> float:
270
+ """Decode a number-special wire value to a Python float.
271
+
272
+ Maps string sentinels back to their float equivalents:
273
+ "NaN" → float('nan'), "Infinity" → float('inf'), "-Infinity" → float('-inf').
274
+ Numeric wire values pass through directly.
275
+
276
+ Args:
277
+ wire: A string sentinel or a JSON number.
278
+
279
+ Returns:
280
+ Python float.
281
+
282
+ Raises:
283
+ TypeError: if wire is neither a string sentinel nor a number.
284
+ """
285
+ if isinstance(wire, (int, float)):
286
+ return float(wire)
287
+ if isinstance(wire, str):
288
+ if wire == "NaN":
289
+ return float("nan")
290
+ if wire == "Infinity":
291
+ return float("inf")
292
+ if wire == "-Infinity":
293
+ return float("-inf")
294
+ raise TypeError(
295
+ f"[number-special] unrecognised wire value: {wire!r}. "
296
+ "Expected a number or one of 'NaN', 'Infinity', '-Infinity'."
297
+ )
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # JSONEncoder — encode hook for json.dumps
302
+ # ---------------------------------------------------------------------------
303
+
304
+ class ApigenEncoder(json.JSONEncoder):
305
+ """JSON encoder that maps well-known Python types to their canonical wire form.
306
+
307
+ Use as: ``json.dumps(value, cls=ApigenEncoder)``
308
+
309
+ Well-known dispatch:
310
+ datetime → RFC 3339 UTC string
311
+ Decimal → decimal string
312
+ UUID → lowercase hyphenated string
313
+ bytes / bytearray → standard base64 string
314
+ float (non-finite) → "NaN" / "Infinity" / "-Infinity"
315
+ int (large) → int (JSON number — use encode_int64 + schema-walk for int64 format)
316
+
317
+ Note: This encoder is value-driven and cannot see the schema, so it handles
318
+ the "encode" direction only. For schema-driven decode, use ``decode()``.
319
+ """
320
+
321
+ def default(self, obj: Any) -> Any: # noqa: ANN401
322
+ """Override JSONEncoder.default for apigen well-known types."""
323
+ if isinstance(obj, datetime):
324
+ return encode_datetime(obj)
325
+ if isinstance(obj, Decimal):
326
+ return encode_decimal(obj)
327
+ if isinstance(obj, UUID):
328
+ return encode_uuid(obj)
329
+ if isinstance(obj, (bytes, bytearray)):
330
+ return encode_bytes(obj)
331
+ # Let the base encoder raise for everything else.
332
+ return super().default(obj)
333
+
334
+ def encode(self, obj: Any) -> str:
335
+ """Override encode to handle float non-finite values at the top level."""
336
+ return super().encode(self._prepare(obj))
337
+
338
+ def iterencode(self, obj: Any, _one_shot: bool = False) -> Any:
339
+ """Override iterencode to handle float non-finite values recursively."""
340
+ return super().iterencode(self._prepare(obj))
341
+
342
+ def _prepare(self, obj: Any) -> Any:
343
+ """Recursively prepare a value for JSON serialization."""
344
+ if isinstance(obj, float):
345
+ return encode_number_special(obj)
346
+ if isinstance(obj, dict):
347
+ return {k: self._prepare(v) for k, v in obj.items()}
348
+ if isinstance(obj, (list, tuple)):
349
+ return [self._prepare(v) for v in obj]
350
+ return obj
351
+
352
+
353
+ def encode_value(value: Any) -> Wire:
354
+ """Encode a single native value to its canonical wire form (value-driven).
355
+
356
+ This is a convenience wrapper around the individual codec encode functions.
357
+ For plain JSON types (str, int, bool, None, list, dict) this is an identity.
358
+
359
+ Args:
360
+ value: Any Python value.
361
+
362
+ Returns:
363
+ The wire form (a JSON-serialisable value).
364
+ """
365
+ if isinstance(value, datetime):
366
+ return encode_datetime(value)
367
+ if isinstance(value, Decimal):
368
+ return encode_decimal(value)
369
+ if isinstance(value, UUID):
370
+ return encode_uuid(value)
371
+ if isinstance(value, (bytes, bytearray)):
372
+ return encode_bytes(value)
373
+ if isinstance(value, float):
374
+ return encode_number_special(value)
375
+ if isinstance(value, dict):
376
+ return {k: encode_value(v) for k, v in value.items()}
377
+ if isinstance(value, list):
378
+ return [encode_value(v) for v in value]
379
+ return value
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # Schema-driven decode — walks schema and wire in lockstep
384
+ # ---------------------------------------------------------------------------
385
+
386
+ def decode(wire: Wire, schema: SchemaNode, defs: dict[str, SchemaNode] | None = None) -> Any:
387
+ """Schema-driven decode: walk `schema` and `wire` in lockstep, constructing
388
+ native types where the schema owns a logical type.
389
+
390
+ This is the primary decode entry point. It mirrors the TS transcoder walk
391
+ (DESIGN §4.4) applied to the Python host:
392
+
393
+ - Scalars with `format` → dispatch to the typed decoder.
394
+ - $ref → resolve against `defs` and recurse.
395
+ - oneOf → inspect value structure (no discriminator support yet — plain passthrough).
396
+ - array items → recurse per element.
397
+ - object properties → recurse per property.
398
+ - Anything else → plain passthrough (already a JSON-native type).
399
+
400
+ Args:
401
+ wire: The wire value (JSON-parsed Python object).
402
+ schema: Resolved JSON Schema fragment describing the value's type.
403
+ defs: Optional ``$defs`` mapping for resolving ``$ref`` nodes. If not
404
+ provided, uses the top-level ``$defs`` from ``schema`` itself.
405
+
406
+ Returns:
407
+ The decoded native Python value.
408
+ """
409
+ # Collect $defs for $ref resolution (merge top-level with caller-supplied).
410
+ root_defs: dict[str, SchemaNode] = {}
411
+ if isinstance(schema, dict) and "$defs" in schema:
412
+ root_defs.update(schema["$defs"])
413
+ if defs:
414
+ root_defs.update(defs)
415
+
416
+ return _decode_node(wire, schema, root_defs)
417
+
418
+
419
+ def _decode_node(wire: Wire, node: SchemaNode, defs: dict[str, SchemaNode]) -> Any:
420
+ """Recursive decode helper — one schema node at a time."""
421
+ if not isinstance(node, dict):
422
+ return wire
423
+
424
+ # ---- $ref ----
425
+ if "$ref" in node:
426
+ ref: str = node["$ref"]
427
+ resolved = _resolve_ref(ref, defs)
428
+ if resolved is not None:
429
+ return _decode_node(wire, resolved, defs)
430
+ # Unknown ref → passthrough.
431
+ return wire
432
+
433
+ node_type = node.get("type")
434
+ node_format = node.get("format")
435
+
436
+ # ---- Well-known scalar dispatch (format-based) ----
437
+ if node_type == "string" and node_format == "date-time":
438
+ return decode_datetime(wire)
439
+
440
+ if node_type == "string" and node_format == "int64":
441
+ return decode_int64(wire)
442
+
443
+ if node_type == "string" and node_format == "decimal":
444
+ return decode_decimal(wire)
445
+
446
+ if node_type == "string" and node_format == "byte":
447
+ return decode_bytes(wire)
448
+
449
+ if node_type == "string" and node_format == "uuid":
450
+ return decode_uuid(wire)
451
+
452
+ if node_type == "number" and node_format is None:
453
+ # number-special: only fire when explicitly asked (number node, no format).
454
+ # Handle string sentinels if present; otherwise passthrough numeric wire.
455
+ if isinstance(wire, str) and wire in ("NaN", "Infinity", "-Infinity"):
456
+ return decode_number_special(wire)
457
+ return wire
458
+
459
+ # ---- oneOf (union) — passthrough for now; consumer picks the branch ----
460
+ if "oneOf" in node:
461
+ # Without discriminator support, attempt each branch decode and return
462
+ # the first that succeeds without error; fall back to passthrough.
463
+ for branch in node["oneOf"]:
464
+ try:
465
+ return _decode_node(wire, branch, defs)
466
+ except (TypeError, ValueError):
467
+ continue
468
+ return wire
469
+
470
+ # ---- array (recurse over items) ----
471
+ if node_type == "array" and isinstance(wire, list):
472
+ items_schema = node.get("items")
473
+ if items_schema and isinstance(items_schema, dict):
474
+ return [_decode_node(item, items_schema, defs) for item in wire]
475
+ return wire
476
+
477
+ # ---- object (recurse over declared properties) ----
478
+ if node_type == "object" and isinstance(wire, dict):
479
+ props: dict[str, SchemaNode] = node.get("properties", {})
480
+ if not props:
481
+ return wire
482
+ result: dict[str, Any] = {}
483
+ for k, v in wire.items():
484
+ if k in props:
485
+ result[k] = _decode_node(v, props[k], defs)
486
+ else:
487
+ result[k] = v
488
+ return result
489
+
490
+ # ---- plain JSON passthrough ----
491
+ return wire
492
+
493
+
494
+ def _resolve_ref(ref: str, defs: dict[str, SchemaNode]) -> SchemaNode | None:
495
+ """Resolve a JSON Pointer $ref of the form '#/$defs/<Name>' against defs."""
496
+ if not ref.startswith("#/$defs/"):
497
+ return None
498
+ name = ref[len("#/$defs/"):]
499
+ return defs.get(name)
500
+
501
+
502
+ # ---------------------------------------------------------------------------
503
+ # Seed construction (used by the test harness)
504
+ # ---------------------------------------------------------------------------
505
+
506
+ def construct_seed(recipe: Wire) -> Any:
507
+ """Build a Python-native seed value from a LogicalTypeVector seed recipe.
508
+
509
+ A plain Wire value is returned as-is (already the native form for
510
+ scalars like str, int, Decimal).
511
+
512
+ A ``$construct`` recipe has the shape:
513
+ ``{"$construct": <LogicalTypeId>, "args": [<Wire>, ...]}``
514
+
515
+ The following LogicalTypeIds are supported:
516
+ "date-time" → datetime from ISO string arg
517
+ "byte" → bytes from list-of-ints arg
518
+ "number-special" → float NaN / Infinity / -Infinity from string arg
519
+ "int64" → int from decimal-string arg
520
+ "decimal" → Decimal from decimal-string arg
521
+ "uuid" → str (lowercase UUID) from string arg
522
+
523
+ Args:
524
+ recipe: A Wire value or a ``$construct`` dict.
525
+
526
+ Returns:
527
+ The native Python seed value.
528
+
529
+ Raises:
530
+ ValueError: for unrecognised logical type ids.
531
+ TypeError: for wrong arg types.
532
+ """
533
+ if not isinstance(recipe, dict) or "$construct" not in recipe:
534
+ # Plain wire seed — use as-is (e.g. "9007199254740993" for int64, "123.456" for decimal).
535
+ return recipe
536
+
537
+ logical_type: str = recipe["$construct"]
538
+ args: list[Wire] = recipe.get("args", [])
539
+
540
+ if logical_type == "date-time":
541
+ iso_str = args[0]
542
+ return decode_datetime(iso_str)
543
+
544
+ if logical_type == "byte":
545
+ byte_list = args[0]
546
+ return bytes(byte_list)
547
+
548
+ if logical_type == "number-special":
549
+ sentinel: str = args[0]
550
+ return decode_number_special(sentinel)
551
+
552
+ if logical_type == "int64":
553
+ return decode_int64(args[0])
554
+
555
+ if logical_type == "decimal":
556
+ return decode_decimal(args[0])
557
+
558
+ if logical_type == "uuid":
559
+ return decode_uuid(args[0])
560
+
561
+ raise ValueError(f"construct_seed: unrecognised logicalType {logical_type!r}")
562
+
563
+
564
+ # ---------------------------------------------------------------------------
565
+ # Invariant checker (used by the test harness)
566
+ # ---------------------------------------------------------------------------
567
+
568
+ def check_invariant(decoded: Any, pointer: str, expected: Wire) -> bool:
569
+ """Check a single post-decode invariant.
570
+
571
+ The `pointer` is a virtual JSON Pointer that maps to a computed attribute
572
+ of the decoded value — not a literal JSON Pointer into a dict, but a
573
+ semantic label defined per logical type:
574
+
575
+ /epochMs — datetime: int(dt.timestamp() * 1000) since Unix epoch
576
+ /bigintStr — int: str(value) (proves no precision loss)
577
+ /str — Decimal: str(value) (proves no float coercion)
578
+ /utf8 — bytes: decoded as UTF-8 string
579
+ /value — str: the value itself (UUID, etc.)
580
+ /isNaN — float: math.isnan(value)
581
+ /isFinite — float: math.isfinite(value)
582
+
583
+ Args:
584
+ decoded: The decoded native Python value.
585
+ pointer: The semantic pointer string (e.g. "/epochMs").
586
+ expected: The expected Wire value.
587
+
588
+ Returns:
589
+ True if the invariant holds; False otherwise.
590
+ """
591
+ actual: Any
592
+
593
+ if pointer == "/epochMs":
594
+ if not isinstance(decoded, datetime):
595
+ return False
596
+ actual = int(decoded.timestamp() * 1000)
597
+
598
+ elif pointer == "/bigintStr":
599
+ if not isinstance(decoded, int):
600
+ return False
601
+ actual = str(decoded)
602
+
603
+ elif pointer == "/str":
604
+ actual = str(decoded)
605
+
606
+ elif pointer == "/utf8":
607
+ if not isinstance(decoded, (bytes, bytearray)):
608
+ return False
609
+ actual = decoded.decode("utf-8")
610
+
611
+ elif pointer == "/value":
612
+ actual = decoded
613
+
614
+ elif pointer == "/isNaN":
615
+ if not isinstance(decoded, float):
616
+ return False
617
+ actual = math.isnan(decoded)
618
+
619
+ elif pointer == "/isFinite":
620
+ if not isinstance(decoded, float):
621
+ return False
622
+ actual = math.isfinite(decoded)
623
+
624
+ else:
625
+ # Unknown pointer — cannot validate; conservative fail.
626
+ return False
627
+
628
+ return actual == expected
@@ -0,0 +1 @@
1
+ """apigen_python — Python host for @adhd/apigen (SPEC §14)."""