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.
- pytypehintweb/__init__.py +23 -0
- pytypehintweb/decode.py +268 -0
- pytypehintweb/demo/__init__.py +0 -0
- pytypehintweb/demo/__main__.py +56 -0
- pytypehintweb/demo/app.py +701 -0
- pytypehintweb/demo/examples.py +1346 -0
- pytypehintweb/plan.py +1209 -0
- pytypehintweb/py.typed +0 -0
- pytypehintweb/static/contract.js +838 -0
- pytypehintweb/static/defaults.js +167 -0
- pytypehintweb/static/fields.js +841 -0
- pytypehintweb/static/form.js +281 -0
- pytypehintweb/static/inputs.js +1702 -0
- pytypehintweb/static/iso.js +42 -0
- pytypehintweb/static/normalize.js +733 -0
- pytypehintweb/static/slider.js +81 -0
- pytypehintweb/static/widgets.css +1243 -0
- pytypehintweb/types.py +25 -0
- pytypehintweb-0.0.1.dist-info/METADATA +180 -0
- pytypehintweb-0.0.1.dist-info/RECORD +24 -0
- pytypehintweb-0.0.1.dist-info/WHEEL +5 -0
- pytypehintweb-0.0.1.dist-info/entry_points.txt +2 -0
- pytypehintweb-0.0.1.dist-info/licenses/LICENSE +21 -0
- pytypehintweb-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|
pytypehintweb/decode.py
ADDED
|
@@ -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()
|