Skip to content

Commit 321ed15

Browse files
Merge pull request #128 from ZeroIntensity/caching
Stabilize Caching
2 parents 8d194f2 + 493766c commit 321ed15

6 files changed

Lines changed: 142 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added `templates` and `TemplatesConfig` to config
1313
- Added the `templates` function
1414
- Added support for `attrs` in type validation
15+
- Added documentation for caching
16+
- Added the `cache_rate` parameter to routers
1517
- Removed `psutil` and `plotext` as a global dependency
1618
- Added `fancy` optional dependencies
1719
- Fixed route inputs with synchronous routes

docs/building-projects/responses.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,46 @@ app = new_app()
1313
async def index():
1414
return "Hello, view.py", 201, {"x-my-header": "my_header"}
1515
```
16+
17+
## Caching
18+
19+
Sometimes, computing the response for a route can be expensive or unnecessary. For this, view.py, along with many other web frameworks, provide the ability to cache responses.
20+
21+
View lets you do this by using the `cache_rate` parameter on a router.
22+
23+
For example:
24+
25+
```py
26+
from view import new_app
27+
28+
app = new_app()
29+
30+
@app.get("/", cache_rate=10) # reload this route every 10 requests
31+
async def index():
32+
return "..."
33+
34+
app.run()
35+
```
36+
37+
You can see this in more detail by using a route that changes it's responses:
38+
39+
```py
40+
from view import new_app
41+
42+
app = new_app()
43+
count = 1
44+
45+
@app.get("/", cache_rate=10)
46+
async def index():
47+
global count
48+
count += 1
49+
return str(count)
50+
51+
app.run()
52+
```
53+
54+
In the above example, `index` is only called every 10 requests, so after 20 calls, `count` would be `2`.
55+
1656
## Response Protocol
1757

1858
If you have some sort of object that you want to wrap a response around, view.py gives you the `__view_response__` protocol. The only requirements are:

mkdocs.yml

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@ repo_name: ZeroIntensity/view.py
55

66
nav:
77
- Home: index.md
8-
- Getting Started:
9-
- Installation: getting-started/installation.md
10-
- Configuration: getting-started/configuration.md
11-
- Project Creation: getting-started/creating_a_project.md
12-
- Building Projects:
13-
- App Basics: building-projects/app_basics.md
14-
- Routing: building-projects/routing.md
15-
- Returning Responses: building-projects/responses.md
16-
- Taking Parameters: building-projects/parameters.md
17-
- HTML Templating: building-projects/templating.md
18-
- Writing Documentation: building-projects/documenting.md
8+
- Installation: getting-started/installation.md
9+
- Configuration: getting-started/configuration.md
10+
- Project Creation: getting-started/creating_a_project.md
11+
- App Basics: building-projects/app_basics.md
12+
- Routing: building-projects/routing.md
13+
- Returning Responses: building-projects/responses.md
14+
- Taking Parameters: building-projects/parameters.md
15+
- HTML Templating: building-projects/templating.md
16+
- Writing Documentation: building-projects/documenting.md
1917

2018
theme:
2119
name: material
@@ -47,7 +45,19 @@ theme:
4745
name: Switch to system preference
4846

4947
markdown_extensions:
50-
- admonition
48+
- pymdownx.highlight:
49+
anchor_linenums: true
50+
- pymdownx.inlinehilite
51+
- pymdownx.snippets
52+
- admonition
53+
- pymdownx.details
54+
- pymdownx.tabbed:
55+
alternate_style: true
56+
- pymdownx.superfences:
57+
custom_fences:
58+
- name: mermaid
59+
class: mermaid
60+
format: !!python/name:pymdownx.superfences.fence_code_format
5161

5262
plugins:
5363
- mkdocstrings:

src/view/app.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -319,39 +319,40 @@ def _method_wrapper(
319319
self,
320320
path: str,
321321
doc: str | None,
322+
cache_rate: int,
322323
target: Callable[..., Any],
323324
# i dont really feel like typing this properly
324325
) -> Callable[[RouteOrCallable], Route]:
325326
def inner(route: RouteOrCallable) -> Route:
326-
new_route = target(path, doc)(route)
327+
new_route = target(path, doc, cache_rate=cache_rate)(route)
327328
self._push_route(new_route)
328329
return new_route
329330

330331
return inner
331332

332-
def get(self, path: str, *, doc: str | None = None):
333+
def get(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
333334
"""Set a GET route."""
334-
return self._method_wrapper(path, doc, get)
335+
return self._method_wrapper(path, doc, cache_rate, get)
335336

336-
def post(self, path: str, *, doc: str | None = None):
337+
def post(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
337338
"""Set a POST route."""
338-
return self._method_wrapper(path, doc, post)
339+
return self._method_wrapper(path, doc, cache_rate, post)
339340

340-
def delete(self, path: str, *, doc: str | None = None):
341+
def delete(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
341342
"""Set a DELETE route."""
342-
return self._method_wrapper(path, doc, delete)
343+
return self._method_wrapper(path, doc, cache_rate, delete)
343344

344-
def patch(self, path: str, *, doc: str | None = None):
345+
def patch(self, path: str, doc: str | None = None, *, cache_rate: int = -1,):
345346
"""Set a PATCH route."""
346-
return self._method_wrapper(path, doc, patch)
347+
return self._method_wrapper(path, doc, cache_rate, patch)
347348

348-
def put(self, path: str, *, doc: str | None = None):
349+
def put(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
349350
"""Set a PUT route."""
350-
return self._method_wrapper(path, doc, put)
351+
return self._method_wrapper(path, doc, cache_rate, put)
351352

352-
def options(self, path: str, *, doc: str | None = None):
353+
def options(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
353354
"""Set a OPTIONS route."""
354-
return self._method_wrapper(path, doc, options)
355+
return self._method_wrapper(path, doc, cache_rate, options)
355356

356357
def _set_log_arg(self, kwargs: _LogArgs, key: str) -> None:
357358
if key not in kwargs:

src/view/routing.py

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from enum import Enum
99
from typing import Any, Callable, Generic, Type, TypeVar, Union
1010

11-
from typing_extensions import ParamSpec
12-
1311
from ._util import LoadChecker, make_hint
1412
from .exceptions import InvalidRouteError, MistakeError
1513
from .typing import Validator, ValueType, ViewResponse, ViewRoute
@@ -24,7 +22,6 @@
2422
"query",
2523
"body",
2624
"route_types",
27-
"cache",
2825
"BodyParam",
2926
)
3027

@@ -59,21 +56,17 @@ class RouteInput(Generic[V]):
5956
validators: list[Validator[V]]
6057

6158

62-
P = ParamSpec("P")
63-
T = TypeVar("T", bound="ViewResponse")
64-
65-
6659
@dataclass
6760
class Part(Generic[V]):
6861
name: str
6962
type: type[V] | None
7063

7164

7265
@dataclass
73-
class Route(Generic[P, T], LoadChecker):
66+
class Route(LoadChecker):
7467
"""Standard Route Wrapper"""
7568

76-
func: Callable[P, T]
69+
func: Callable[..., ViewResponse]
7770
path: str | None
7871
method: Method
7972
inputs: list[RouteInput]
@@ -98,24 +91,24 @@ def __repr__(self):
9891

9992
__str__ = __repr__
10093

101-
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
94+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
10295
return self.func(*args, **kwargs)
10396

10497

105-
RouteOrCallable = Union[Route[P, T], ViewRoute[P, T]]
98+
RouteOrCallable = Union[Route, ViewRoute]
10699

107100

108-
def _ensure_route(r: RouteOrCallable[..., Any]) -> Route[..., Any]:
101+
def _ensure_route(r: RouteOrCallable) -> Route:
109102
if isinstance(r, Route):
110103
return r
111104

112105
return Route(r, None, Method.GET, [])
113106

114107

115108
def route_types(
116-
r: RouteOrCallable[P, T],
109+
r: RouteOrCallable,
117110
data: type[Any] | tuple[type[Any]] | dict[str, Any],
118-
) -> Route[P, T]:
111+
) -> Route:
119112
route = _ensure_route(r)
120113
if isinstance(data, tuple):
121114
for i in data:
@@ -134,13 +127,15 @@ def route_types(
134127

135128

136129
def _method(
137-
r: RouteOrCallable[P, T],
130+
r: RouteOrCallable,
138131
raw_path: str | None,
139132
doc: str | None,
140133
method: Method,
141-
) -> Route[P, T]:
134+
cache_rate: int
135+
) -> Route:
142136
route = _ensure_route(r)
143137
route.method = method
138+
route.cache_rate = cache_rate
144139
util_path = raw_path or "/"
145140

146141
if not util_path.startswith("/"):
@@ -203,19 +198,20 @@ def _method(
203198
return route
204199

205200

206-
Path = Callable[[RouteOrCallable[P, T]], Route[P, T]]
201+
Path = Callable[[RouteOrCallable], Route]
207202

208203

209204
def _method_wrapper(
210-
path_or_route: str | None | RouteOrCallable[P, T],
205+
path_or_route: str | None | RouteOrCallable,
211206
doc: str | None,
212207
method: Method,
213-
) -> Path[P, T]:
214-
def inner(r: RouteOrCallable[P, T]) -> Route[P, T]:
208+
cache_rate: int
209+
) -> Path:
210+
def inner(r: RouteOrCallable) -> Route:
215211
if (not isinstance(path_or_route, str)) and path_or_route:
216212
raise TypeError(f"{path_or_route!r} is not a string")
217213

218-
return _method(r, path_or_route, doc, method)
214+
return _method(r, path_or_route, doc, method, cache_rate)
219215

220216
if not path_or_route:
221217
return inner
@@ -227,45 +223,57 @@ def inner(r: RouteOrCallable[P, T]) -> Route[P, T]:
227223

228224

229225
def get(
230-
path_or_route: str | None | RouteOrCallable[P, T] = None,
226+
path_or_route: str | None | RouteOrCallable = None,
231227
doc: str | None = None,
232-
) -> Path[P, T]:
233-
return _method_wrapper(path_or_route, doc, Method.GET)
228+
*,
229+
cache_rate: int = -1,
230+
) -> Path:
231+
return _method_wrapper(path_or_route, doc, Method.GET, cache_rate)
234232

235233

236234
def post(
237235
path_or_route: str | None | RouteOrCallable = None,
238236
doc: str | None = None,
237+
*,
238+
cache_rate: int = -1,
239239
):
240-
return _method_wrapper(path_or_route, doc, Method.POST)
240+
return _method_wrapper(path_or_route, doc, Method.POST, cache_rate)
241241

242242

243243
def patch(
244244
path_or_route: str | None | RouteOrCallable = None,
245245
doc: str | None = None,
246+
*,
247+
cache_rate: int = -1,
246248
):
247-
return _method_wrapper(path_or_route, doc, Method.PATCH)
249+
return _method_wrapper(path_or_route, doc, Method.PATCH, cache_rate)
248250

249251

250252
def put(
251253
path_or_route: str | None | RouteOrCallable = None,
252254
doc: str | None = None,
255+
*,
256+
cache_rate: int = -1,
253257
):
254-
return _method_wrapper(path_or_route, doc, Method.PUT)
258+
return _method_wrapper(path_or_route, doc, Method.PUT, cache_rate)
255259

256260

257261
def delete(
258262
path_or_route: str | None | RouteOrCallable = None,
259263
doc: str | None = None,
264+
*,
265+
cache_rate: int = -1,
260266
):
261-
return _method_wrapper(path_or_route, doc, Method.DELETE)
267+
return _method_wrapper(path_or_route, doc, Method.DELETE, cache_rate)
262268

263269

264270
def options(
265271
path_or_route: str | None | RouteOrCallable = None,
266272
doc: str | None = None,
273+
*,
274+
cache_rate: int = -1,
267275
):
268-
return _method_wrapper(path_or_route, doc, Method.OPTIONS)
276+
return _method_wrapper(path_or_route, doc, Method.OPTIONS, cache_rate)
269277

270278

271279
class _NoDefault:
@@ -314,11 +322,3 @@ def inner(r: RouteOrCallable) -> Route:
314322

315323
return inner
316324

317-
318-
def cache(amount: int):
319-
def inner(r: RouteOrCallable) -> Route:
320-
route = _ensure_route(r)
321-
route.cache_rate = amount
322-
return route
323-
324-
return inner

tests/test_app.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing_extensions import NotRequired
66
from ward import test
77

8-
from view import BodyParam, Response, body, new_app, query
8+
from view import BodyParam, Response, body, new_app, query, get
99

1010

1111
@test("responses")
@@ -541,6 +541,31 @@ async def index(test: Test):
541541
assert (await test.get("/", query={"test": {"a": "b", "b": 0, "c": [], "d": {"a": "b"}}})).status == 400
542542
assert (await test.get("/", query={"test": {"a": "b", "b": 0, "c": [], "d": {"a": 0}}})).message == "b"
543543

544+
@test("caching")
545+
async def _():
546+
app = new_app()
547+
count = 0
548+
549+
@app.get("/param", cache_rate=10)
550+
async def param():
551+
nonlocal count
552+
count += 1
553+
return str(count)
554+
555+
@get("/param_std", cache_rate=10)
556+
async def param_std():
557+
nonlocal count
558+
count += 1
559+
return str(count)
560+
561+
562+
async with app.test() as test:
563+
results = [(await test.get("/param")).message for _ in range(10)]
564+
assert all(i == results[0] for i in results)
565+
566+
results = [(await test.get("/param_std")).message for _ in range(10)]
567+
assert all(i == results[0] for i in results)
568+
544569
@test("synchronous route inputs")
545570
async def _():
546571
app = new_app()
@@ -565,3 +590,4 @@ def both(a: str, b: str):
565590
assert (await test.get("/", query={"test": "a"})).message == "a"
566591
assert (await test.get("/body", body={"test": "b"})).message == "b"
567592
assert (await test.get("/both", body={"a": "a"}, query={"b": "b"})).message == "ab"
593+

0 commit comments

Comments
 (0)