fora-protocol 1.0.3__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.
- fora_protocol-1.0.3.dist-info/METADATA +57 -0
- fora_protocol-1.0.3.dist-info/RECORD +16 -0
- fora_protocol-1.0.3.dist-info/WHEEL +5 -0
- fora_protocol-1.0.3.dist-info/licenses/LICENSE +201 -0
- fora_protocol-1.0.3.dist-info/top_level.txt +2 -0
- vocab/functiontokens.py +85 -0
- vocab/geographytokens.py +38 -0
- vocab/pricingunits.py +64 -0
- vocab/py.typed +0 -0
- vocab/quotametrics.py +48 -0
- vocab/usertypes.py +47 -0
- wire/base.py +106 -0
- wire/models.py +2145 -0
- wire/names.py +50 -0
- wire/py.typed +0 -0
- wire/unique.py +12 -0
wire/base.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Base class for every generated model.
|
|
2
|
+
|
|
3
|
+
All generated models inherit this, so it is the single place to configure model-wide
|
|
4
|
+
behavior and the one point an application extends to add or relax behavior across every
|
|
5
|
+
model at once. Hand-written; not regenerated.
|
|
6
|
+
|
|
7
|
+
Because every generated model — nested ones included — inherits this class, a validator
|
|
8
|
+
here runs at every depth of a message for free. That is why the wire-naming refusal below
|
|
9
|
+
needs no walk, while its TypeScript twin in ``gen/ts/wire/base.ts`` has to recurse itself.
|
|
10
|
+
"""
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
14
|
+
from pydantic_core import PydanticCustomError
|
|
15
|
+
|
|
16
|
+
from wire.names import snake_from_json_name
|
|
17
|
+
from wire.unique import UNIQUE_ITEM_FIELDS
|
|
18
|
+
|
|
19
|
+
#: Error ``type`` on the refusal below, so a caller can recognise it in
|
|
20
|
+
#: ``ValidationError.errors()`` without matching on a message string.
|
|
21
|
+
JSON_NAME_ALIAS_ERROR = "fora_json_name_alias"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WireModel(BaseModel):
|
|
25
|
+
# Forward-compatible: fields from a newer protocol version are ignored, not
|
|
26
|
+
# rejected. A consumer that wants strictness sets extra="forbid" in its own
|
|
27
|
+
# subclass.
|
|
28
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
|
29
|
+
|
|
30
|
+
@model_validator(mode="before")
|
|
31
|
+
@classmethod
|
|
32
|
+
def _refuse_json_name_alias(cls, data: Any) -> Any:
|
|
33
|
+
"""Refuse a message whose field names are protojson's lowerCamelCase alias.
|
|
34
|
+
|
|
35
|
+
The FORA wire is snake_case proto-JSON and the camelCase ``json_name`` alias is out
|
|
36
|
+
of contract, so a conformant peer serves snake_case — connect-go does that only
|
|
37
|
+
when a codec with UseProtoNames is registered, which a FORA deployment does and a
|
|
38
|
+
stock connect-go server does not. ``extra="ignore"`` above means such an answer
|
|
39
|
+
would otherwise validate SUCCESSFULLY into a message with every multiword field
|
|
40
|
+
missing, and nothing anywhere would say so.
|
|
41
|
+
|
|
42
|
+
Depth is the point, and the money verb is why: a stock connect-go server omits
|
|
43
|
+
unset fields, so a TransactionResponse arrives as ``{ver, items}`` — every root key
|
|
44
|
+
a single word, identical in both spellings — while ``transaction_id`` sits one
|
|
45
|
+
level down in TransactionResultItem and is dropped. That reads as a purchase that
|
|
46
|
+
succeeded with no transaction id and no delivery URL, severing the dispute chain at
|
|
47
|
+
its first link. Inheritance covers the depth: TransactionResultItem is a WireModel
|
|
48
|
+
too, so it checks itself.
|
|
49
|
+
|
|
50
|
+
Open maps are out of reach by construction rather than by a hold-back list. A
|
|
51
|
+
``dict[str, Any]`` field is validated as data, not as a model, so its keys — which
|
|
52
|
+
are caller-chosen — never reach this method.
|
|
53
|
+
|
|
54
|
+
A value that is not a mapping is left alone: this also runs when a model is built
|
|
55
|
+
from Python objects rather than parsed from the wire, and there the field names are
|
|
56
|
+
already the field names.
|
|
57
|
+
"""
|
|
58
|
+
if not isinstance(data, dict):
|
|
59
|
+
return data
|
|
60
|
+
declared = cls.model_fields
|
|
61
|
+
for key in data:
|
|
62
|
+
if not isinstance(key, str) or key in declared:
|
|
63
|
+
continue
|
|
64
|
+
name = snake_from_json_name(key)
|
|
65
|
+
if name != key and name in declared:
|
|
66
|
+
raise PydanticCustomError(
|
|
67
|
+
JSON_NAME_ALIAS_ERROR,
|
|
68
|
+
"peer answered with the lowerCamelCase json_name alias ({key}); the FORA "
|
|
69
|
+
"wire is snake_case proto-JSON, so its answer cannot be read without "
|
|
70
|
+
"silently dropping every multiword field",
|
|
71
|
+
{"key": key},
|
|
72
|
+
)
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
@model_validator(mode="after")
|
|
76
|
+
def _reject_duplicate_items(self) -> "WireModel":
|
|
77
|
+
# (buf.validate.field).repeated.unique. datamodel-code-generator drops JSON
|
|
78
|
+
# Schema's `uniqueItems` for pydantic v2, so this rule cannot ride into models.py
|
|
79
|
+
# the way every other field rule does; it arrives via the generated wire/unique.py
|
|
80
|
+
# instead. Zod gets the same rule inline, from `uniqueItems` in the JSON Schema.
|
|
81
|
+
# No rule is restated here: the field list is derived from Go protovalidate.
|
|
82
|
+
#
|
|
83
|
+
# Walk the MRO, NOT just type(self).__name__: the manifest is keyed by the
|
|
84
|
+
# generated message class name, and a consumer SUBCLASS (the documented extension
|
|
85
|
+
# seam above) has a different __name__. Field-annotation rules (constr/Field
|
|
86
|
+
# bounds, patterns) inherit structurally across the MRO; this out-of-band rule
|
|
87
|
+
# must walk the base classes itself or it silently no-ops on subclasses.
|
|
88
|
+
# WireModel/BaseModel/object are not manifest keys, so they resolve to () — inert.
|
|
89
|
+
fields: set[str] = set()
|
|
90
|
+
for klass in type(self).__mro__:
|
|
91
|
+
fields.update(UNIQUE_ITEM_FIELDS.get(klass.__name__, ()))
|
|
92
|
+
for name in fields:
|
|
93
|
+
items = getattr(self, name, None)
|
|
94
|
+
if items is not None and len(items) != len(set(items)):
|
|
95
|
+
raise ValueError(f"{name}: repeated value must contain unique items")
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
99
|
+
# Proto-JSON omits unset optional fields; default to the same so parse → dump
|
|
100
|
+
# round-trips match the wire. Pass exclude_none=False to include nulls.
|
|
101
|
+
kwargs.setdefault("exclude_none", True)
|
|
102
|
+
return super().model_dump(**kwargs)
|
|
103
|
+
|
|
104
|
+
def model_dump_json(self, **kwargs: Any) -> str:
|
|
105
|
+
kwargs.setdefault("exclude_none", True)
|
|
106
|
+
return super().model_dump_json(**kwargs)
|