Skip to content

Commit ac37e90

Browse files
Merge pull request #52 from ZeroIntensity/view-body-validation
View body validation
2 parents 7a37e54 + 018fbb4 commit ac37e90

File tree

11 files changed

+898
-261
lines changed

11 files changed

+898
-261
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ dist/
1818
*.egg-info
1919
build/
2020
*.so
21-
html/dist.css
21+
html/dist.css
22+
38venv
23+
39venv
24+
311venv

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## Unreleased
8+
## [1.0.0-alpha5] - 2023-09-24
99

1010
- Added `app.query` and `app.body`
1111
- Patched warning with starting app from incorrect filename
1212
- Updated `__all__` for `routing.py`
1313
- Added `view.Response` and `view.HTML`
1414
- Fixed `__view_result__`
15+
- Added support for `__view_body__` and `__view_construct__`
16+
- Added support for Pydantic, `NamedTuple`, and dataclasses for type validation
17+
- Support for direct union types (i.e. `str | int`, `Union[str, int]`) on type validation
1518
- Added support for non async routes
1619

1720
## [1.0.0-alpha4] - 2023-09-10

docs/parameters.md

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ view.py will ensure that the type sent to the server is compatible with what you
7878

7979
```py
8080
@app.get("/")
81-
@query("number", int)
81+
@app.query("number", int)
8282
async def index(number: int):
8383
# number will always be an int.
8484
# if it isn't, an error 400 is sent back to the user automatically
@@ -94,12 +94,16 @@ The following types are supported:
9494
- `float`
9595
- `dict` (or `typing.Dict`)
9696
- `None`
97+
- Pydantic Models
98+
- Dataclasses
99+
- `typing.TypedDict`
100+
- `NamedTuple`
97101

98102
You can allow unions by just passing more parameters:
99103

100104
```py
101105
@app.get('/hello')
102-
@query("name", str, None)
106+
@app.query("name", str, None)
103107
async def hello(name: str | None):
104108
if not name:
105109
return "hello world"
@@ -111,11 +115,143 @@ You can pass type arguments to a `dict`, which are also validated by the server:
111115

112116
```py
113117
@app.get("/something")
114-
@body("data", dict[str, int]) # typing.Dict on 3.8 and 3.9
118+
@app.body("data", dict[str, int]) # typing.Dict on 3.8 and 3.9
115119
async def something(data: dict[str, int]):
116120
# data will always be a dictionary of strings and integers
117121
return "..."
118122
```
119123

120124
The key in a dictionary must always be `str` (i.e. `dict[int, str]` is not allowed), but the value can be any supported type (including other dictionaries!)
121125

126+
### Objects
127+
128+
Here's an example of using an object type with `dataclasses`:
129+
130+
```py
131+
from view import new_app, query
132+
from dataclasses import dataclass, field
133+
134+
app = new_app()
135+
136+
def now() -> str:
137+
... # Calculate current time
138+
139+
@dataclass
140+
class Post:
141+
content: str
142+
created_at = field(default_factory=now)
143+
144+
145+
@app.post("/create")
146+
@app.query("data", Post)
147+
async def create(data: Post):
148+
print(f"Created post {data.created_at}")
149+
return "Success", 201
150+
```
151+
152+
You may also have recursive types, like so:
153+
154+
```py
155+
class MyOtherObject(NamedTuple):
156+
something: int
157+
158+
class MyObject(NamedTuple):
159+
something: str
160+
another_thing: MyOtherObject
161+
```
162+
163+
### Typed Dictionaries
164+
165+
You may use `typing.TypedDict` to type your dictionary inputs if you don't want to use a basic `dict[..., ...]` (or `typing.Dict`), like so:
166+
167+
```py
168+
from view import new_app
169+
from typing import TypedDict
170+
171+
app = new_app()
172+
173+
class MyDict(TypedDict):
174+
a: str
175+
b: int
176+
177+
@app.get("/")
178+
@app.query("data", MyDict)
179+
async def index(data: MyDict):
180+
return data["a"]
181+
182+
app.run()
183+
```
184+
185+
You may also use `NotRequired` to allow certain keys to get omitted:
186+
187+
```py
188+
class MyDict(TypedDict):
189+
a: str
190+
b: NotRequired[int]
191+
```
192+
193+
## View Body Protocol
194+
195+
If you would like to create your own object that gets validated by view.py, you may use the `__view_body__` protocol.
196+
197+
A `__view_body__` should contain a dictionary containing the keys and their corresponding types, like so:
198+
199+
```py
200+
from view import new_app
201+
202+
app = new_app()
203+
204+
class MyObject:
205+
__view_body__ = {"a": str, "b": int}
206+
207+
@app.get("/")
208+
@app.query("data", MyObject)
209+
async def index(data: MyObject):
210+
...
211+
212+
app.run()
213+
```
214+
215+
The above would ensure the body contains something like the following in JSON:
216+
217+
```json
218+
{
219+
"data": {
220+
"a": "...",
221+
"b": 0
222+
}
223+
}
224+
```
225+
226+
A default type can be annotated via `view.BodyParam`:
227+
228+
```py
229+
class MyObject:
230+
__view_body__ = {
231+
"hello": BodyParam(types=(str, int), default="world"),
232+
"world": BodyParam(types=str, default="hello"),
233+
}
234+
```
235+
236+
Note that `__view_body__` can also be a static function, like so:
237+
238+
```py
239+
class MyObject:
240+
@staticmethod
241+
def __view_body__():
242+
return {"a": str, "b": int}
243+
```
244+
245+
### Initialization
246+
247+
By default, an object supporting `__view_body__` will have the proper keyword arguments passed to it's `__init__`.
248+
249+
If you would like to have special behavior in your `__init__`, you may instead add a static `__view_construct__` function that returns an instance:
250+
251+
```py
252+
class MyObject:
253+
__view_body__ = {"a": str, "b": int}
254+
255+
def __view_construct__(**kwargs):
256+
return MyObject()
257+
```

docs/running.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
# Running
22

3-
Is your view app running, you better go catch it!
4-
5-
!!! danger "Apologies"
6-
7-
I'm sorry, you may make a PR to delete this.
8-
9-
103
## Serving
114

125
You can simply run a view app via the `view serve` command, but if you're like me and would rather to just use the `python3` command, that's just as easy.

0 commit comments

Comments
 (0)