pytypehintweb 0.0.1__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.
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from pytypehintweb.decode import decode
4
+ from pytypehintweb.plan import INLINE, PLAIN, WRAPPED, WebConfig, plan_of
5
+ from pytypehintweb.types import COLOR_PATTERN, EMAIL_PATTERN, Color, Email
6
+
7
+ STATIC = Path(__file__).parent / "static"
8
+
9
+ __version__ = "0.0.1"
10
+
11
+ __all__ = [
12
+ "plan_of",
13
+ "decode",
14
+ "WebConfig",
15
+ "STATIC",
16
+ "PLAIN",
17
+ "INLINE",
18
+ "WRAPPED",
19
+ "Color",
20
+ "Email",
21
+ "COLOR_PATTERN",
22
+ "EMAIL_PATTERN",
23
+ ]
@@ -0,0 +1,268 @@
1
+ from datetime import date, time
2
+
3
+ from pytypehint import (
4
+ Date, EnumShape, Float, Int, List, NoneShape, Signature, Str, Struct, Time,
5
+ )
6
+
7
+ # The reserved keys of the discriminated transport, mirrored from the core. A
8
+ # field can never carry them: field names must be identifiers, and neither is.
9
+ _TYPE = "$type"
10
+ _VALUE = "$value"
11
+
12
+
13
+ def decode(schema, data):
14
+ # Prepare a JSON-parsed transport object for schema.build(). Some values the
15
+ # transport cannot express by the exact type the core demands: JSON and the
16
+ # browser collapse 3.0 to 3, so an int arrives where a float is wanted; and a
17
+ # date or a time travels as an ISO string where a date/time object is wanted.
18
+ # decode walks the schema shapes and, guided by the shape at each path,
19
+ # coerces int -> float and str -> date/time wherever that shape is the only
20
+ # possible reading. Everything else passes through untouched.
21
+ #
22
+ # It never guesses from a value's content: a string is turned into a date only
23
+ # because the shape (or an explicit $type) says so, never because it "looks
24
+ # like" one. decode prepares, it does not validate — a value the core will
25
+ # reject passes through unchanged so build() reports it with its own error.
26
+ # The returned dict is always new; the input is never mutated.
27
+ return _decode_fields(_fields_of(schema), data)
28
+
29
+
30
+ def _fields_of(schema):
31
+ if type(schema) is Struct:
32
+ return schema.fields
33
+
34
+ if type(schema) is Signature:
35
+ return schema.params
36
+
37
+ raise TypeError(
38
+ f"decode expects a compiled Signature or Struct, got "
39
+ f"{type(schema).__name__}")
40
+
41
+
42
+ def _transport_type(shape):
43
+ # The runtime type a value arrives as: every dataclass arrives as a dict,
44
+ # every other shape as its own pytype. This is the grouping the core uses to
45
+ # decide whether a wrapper is required.
46
+ return dict if type(shape) is Struct else shape.pytype
47
+
48
+
49
+ def _field_by_name(fields, name):
50
+ for field in fields:
51
+ if field.name == name:
52
+ return field
53
+
54
+ return None
55
+
56
+
57
+ def _decode_fields(fields, data):
58
+ if type(data) is not dict:
59
+ # Not the shape decode walks; build() reports the mismatch.
60
+ return data
61
+
62
+ result = {}
63
+
64
+ for key, value in data.items():
65
+ field = _field_by_name(fields, key)
66
+
67
+ # An unknown key is not decode's to interpret; it travels intact and
68
+ # build() rejects it.
69
+ result[key] = (value if field is None
70
+ else _decode_options(field.shape, value))
71
+
72
+ return result
73
+
74
+
75
+ def _decode_options(shapes, value):
76
+ if value is None:
77
+ return None
78
+
79
+ if type(value) is dict:
80
+ return _decode_dict(shapes, value)
81
+
82
+ if type(value) is list:
83
+ return _decode_list(shapes, value)
84
+
85
+ if type(value) is str:
86
+ return _decode_string(shapes, value)
87
+
88
+ return _decode_scalar(shapes, value)
89
+
90
+
91
+ def _to_date(value):
92
+ try:
93
+ return date.fromisoformat(value)
94
+ except ValueError:
95
+ # Not an ISO date: pass intact, build() rejects it as the wrong type.
96
+ return value
97
+
98
+
99
+ def _to_time(value):
100
+ try:
101
+ return time.fromisoformat(value)
102
+ except ValueError:
103
+ return value
104
+
105
+
106
+ def _to_enum_member(shape, value):
107
+ try:
108
+ # cls[name] resolves through __members__, so an alias name returns its
109
+ # canonical member. A name that does not exist raises KeyError: pass it
110
+ # intact, build() rejects it as the wrong type. decode never validates.
111
+ return shape.cls[value]
112
+ except KeyError:
113
+ return value
114
+
115
+
116
+ def _decode_string(shapes, value):
117
+ # A Str reading keeps a string a string: only convert where a single Date,
118
+ # Time or enum shape is the unambiguous reading and no Str competes. The
119
+ # content of the string is never inspected — a str that "looks like" a date
120
+ # stays a str, and a name is turned into a member only because the shape says
121
+ # so. When more than one string-transport shape reads the path (date | time,
122
+ # date | Estado), the reading is ambiguous, so decode leaves it for the core.
123
+ has_str = any(type(s) is Str for s in shapes)
124
+
125
+ if has_str:
126
+ return value
127
+
128
+ dates = [s for s in shapes if type(s) is Date]
129
+ times = [s for s in shapes if type(s) is Time]
130
+ enums = [s for s in shapes if type(s) is EnumShape]
131
+
132
+ if (bool(dates) + bool(times) + bool(enums)) != 1:
133
+ return value
134
+
135
+ if len(dates) == 1:
136
+ return _to_date(value)
137
+
138
+ if len(times) == 1:
139
+ return _to_time(value)
140
+
141
+ if len(enums) == 1:
142
+ return _to_enum_member(enums[0], value)
143
+
144
+ return value
145
+
146
+
147
+ def _decode_scalar(shapes, value):
148
+ # bool is a subtype of int in Python, but `type(value) is int` already
149
+ # excludes it: a JSON true/false is never a number here.
150
+ if type(value) is int:
151
+ has_float = any(type(s) is Float for s in shapes)
152
+ has_int = any(type(s) is Int for s in shapes)
153
+
154
+ # Coerce only where Float is the single numeric reading. An Int in the
155
+ # same position (int | float) makes a bare number ambiguous, so decode
156
+ # leaves it for the core to route by exact type.
157
+ if has_float and not has_int:
158
+ return float(value)
159
+
160
+ return value
161
+
162
+
163
+ def _decode_list(shapes, value):
164
+ lists = [s for s in shapes if type(s) is List]
165
+
166
+ # A bare list only reaches here when a single List branch reads the path; a
167
+ # union of lists collides on the transport type and travels wrapped instead.
168
+ if len(lists) == 1:
169
+ return [_decode_options(lists[0].item, item) for item in value]
170
+
171
+ return value
172
+
173
+
174
+ def _decode_dict(shapes, value):
175
+ # $value is the reserved payload key of the discriminated wrapper; a
176
+ # dataclass can never carry it, so it tells a wrapper from an inline struct.
177
+ if _VALUE in value:
178
+ return _decode_wrapped(shapes, value)
179
+
180
+ if _TYPE in value:
181
+ return _decode_inline_struct(shapes, value)
182
+
183
+ return _decode_plain_struct(shapes, value)
184
+
185
+
186
+ def _decode_wrapped(shapes, value):
187
+ # A wrapped payload is exactly {$type, $value} — no missing key, no extra
188
+ # key. A dict that only resembles one (a missing $value, an unexpected key,
189
+ # an unknown discriminator, or a wrapper where the schema never asks for one)
190
+ # is malformed transport, not decode's to repair: it travels intact so
191
+ # build() reports it. The exact-set check keeps an explicit null payload
192
+ # ({"$type": t, "$value": null}) distinct from an absent $value — the routing
193
+ # in _decode_dict never reaches here without $value, and value[_VALUE] then
194
+ # reads the real payload rather than conflating null with absence.
195
+ if set(value) != {_TYPE, _VALUE}:
196
+ return value
197
+
198
+ # The wrapper is the wire format of a union. A single-branch path never
199
+ # travels wrapped, so a wrapper there is malformed, not a value to unwrap.
200
+ branches = [s for s in shapes if type(s) is not NoneShape]
201
+
202
+ if len(branches) < 2:
203
+ return value
204
+
205
+ discriminator = value[_TYPE]
206
+
207
+ selected = next(
208
+ (s for s in branches if s.option_id() == discriminator),
209
+ None)
210
+
211
+ if selected is None:
212
+ # Not a branch decode can name; build() reports it.
213
+ return value
214
+
215
+ inner = _decode_options((selected,), value[_VALUE])
216
+
217
+ # The wrapper is the adapter's wire format. The core keeps it only where it
218
+ # sees a genuine collision — several options sharing one runtime type. int
219
+ # and float share a JSON number but not a Python type, so the core routes
220
+ # them bare: decode consumes the wrapper, turning $type into the real type
221
+ # distinction the coerced payload already answers. Where the core does keep
222
+ # the wrapper (list | list, ...), decode keeps it and only prepares $value.
223
+ group = [s for s in branches
224
+ if _transport_type(s) == _transport_type(selected)]
225
+
226
+ if len(group) == 1:
227
+ return inner
228
+
229
+ return {_TYPE: discriminator, _VALUE: inner}
230
+
231
+
232
+ def _decode_inline_struct(shapes, value):
233
+ discriminator = value.get(_TYPE)
234
+
235
+ struct = next(
236
+ (s for s in shapes
237
+ if type(s) is Struct and s.option_id() == discriminator),
238
+ None)
239
+
240
+ if struct is None:
241
+ return value
242
+
243
+ return _decode_struct(struct, value)
244
+
245
+
246
+ def _decode_plain_struct(shapes, value):
247
+ structs = [s for s in shapes if type(s) is Struct]
248
+
249
+ if len(structs) == 1:
250
+ return _decode_struct(structs[0], value)
251
+
252
+ return value
253
+
254
+
255
+ def _decode_struct(struct, value):
256
+ result = {}
257
+
258
+ for key, item in value.items():
259
+ # $type is the discriminator of an inline struct, not a field; it stays.
260
+ if key == _TYPE:
261
+ result[key] = item
262
+ continue
263
+
264
+ field = _field_by_name(struct.fields, key)
265
+ result[key] = (item if field is None
266
+ else _decode_options(field.shape, item))
267
+
268
+ return result
File without changes
@@ -0,0 +1,56 @@
1
+ import argparse
2
+ import webbrowser
3
+
4
+ MISSING = ("The demo requires the 'demo' extra:\n"
5
+ "pip install pytypehintweb[demo]")
6
+
7
+
8
+ BROWSER_HOSTS = {
9
+ "0.0.0.0": "127.0.0.1",
10
+ "::": "[::1]",
11
+ "::0": "[::1]",
12
+ }
13
+
14
+
15
+ def _load():
16
+ try:
17
+ import fastapi # noqa: F401
18
+ import uvicorn
19
+ except ImportError:
20
+ raise SystemExit(MISSING) from None
21
+
22
+ from pytypehintweb.demo import app as demo
23
+
24
+ return uvicorn, demo
25
+
26
+
27
+ def _url(host: str, port: int) -> str:
28
+ resolved = BROWSER_HOSTS.get(host, host)
29
+
30
+ # An IPv6 literal must be bracketed in a URL authority, or the colons
31
+ # collide with the port separator. Wildcard binds resolve above to an
32
+ # already-bracketed loopback, so bracket only a bare colon-bearing host.
33
+ if ":" in resolved and not resolved.startswith("["):
34
+ resolved = f"[{resolved}]"
35
+
36
+ return f"http://{resolved}:{port}"
37
+
38
+
39
+ def main():
40
+ parser = argparse.ArgumentParser(prog="pytypehintweb-demo")
41
+ parser.add_argument("--host", default="127.0.0.1")
42
+ parser.add_argument("--port", type=int, default=8000)
43
+ parser.add_argument("--no-browser", action="store_true")
44
+ args = parser.parse_args()
45
+
46
+ uvicorn, demo = _load()
47
+
48
+ if not args.no_browser:
49
+ url = _url(args.host, args.port)
50
+ demo.ON_STARTUP.append(lambda: webbrowser.open(url))
51
+
52
+ uvicorn.run(demo.app, host=args.host, port=args.port)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()