, hx-trigger | Target | None | | See Trigger keywords section below |
164 |
165 | > Redmage doesn't have any support for a specific template engine, but it should be pretty easy to build a **Component** subclass to support one, such as Jinja2. See the todo_jinja2 example.
166 |
167 |
168 | ## Nesting Components
169 |
170 |
171 | Components can be easily nested by using them just like you would use any other html **Element** object.
172 |
173 | ```
174 | from redmage import Redmage
175 | from redmage.elements import Body, Doc, H1, Head, Html, Script, Title
176 |
177 |
178 | app = Redmage()
179 |
180 |
181 | class Index(Component, routes=("/",)):
182 |
183 | async def render(self):
184 | return Doc(
185 | Html(
186 | Head(
187 | Title("Todo App"),
188 | ),
189 | Body(
190 | ChildComponent(),
191 | Script(src="https://unpkg.com/htmx.org@2.0.0-beta4"),
192 | ),
193 | )
194 | )
195 |
196 |
197 | class ChildComponent(Component):
198 |
199 | async def render(self):
200 | return H1("Child Component")
201 |
202 | ```
203 |
204 | > In the following examples I'm going to assume that the components we write are rendered in an **Index** component like above so it will be ommited.
205 |
206 |
207 | ## Targets
208 |
209 | [htmx targets](https://htmx.org/docs/#targets)
210 |
211 | ### Simple Example
212 |
213 | Thus far, our components have been static. With Redmage we have the ability to react to events on the frontend and update the state of our component using htmx. To do, this we use **redmage.Target** class. It has decorator attributes associated with the following HTTP methods.
214 |
215 | * get
216 | * post
217 | * put
218 | * patch
219 | * delete
220 |
221 | These decorators wrap methods of our component. We can pass the output of these methods to an Element's **target** keyword argument. Check out the example below.
222 |
223 | ```
224 | from redmage import Target
225 |
226 |
227 | class ClickComponent(Component):
228 |
229 | def __init__(self):
230 | self.count = 0
231 |
232 | async def render(self):
233 | return Button(
234 | self.count,
235 | target=self.set_count(self.count + 1)
236 | )
237 |
238 | @Target.post
239 | def set_count(self, count: int):
240 | self.count = count
241 | ```
242 |
243 | When the button is clicked an HTTP POST request is issued to our application. The **set_count** method is ran, updating the component state, and the component is re-rendered and swapped into the DOM.
244 |
245 | By default, if the target method returns **None** then **self** is rendered. We could also explicitly return self, another component, or a tuple of components (this can be useful in conjunction with out of bounds swaps).
246 |
247 | ### Target method arguments
248 |
249 | All of the arguments of a render method, except **self**, require type hints so that Redmage can build a route. Positional or keyword (and keyword only) arguments are added to the route as path arguments or query parameters respectively.
250 |
251 | If the request has a body, the first argument must be a [positional only argument](https://peps.python.org/pep-0570/). It's type must be a class that de-serializes the body by passing the fields as keyword arguments to it's constructor, like a dataclass.
252 |
253 | ```
254 | @dataclass
255 | class UpdateMessageForm:
256 | content: str
257 |
258 |
259 | class Message(Component):
260 |
261 | def __init__(self, content):
262 | self.content = content
263 |
264 | async def render(self):
265 | return Div(
266 | P(f"{self.content=}"),
267 | Form(
268 | Input(
269 | type="text",
270 | id="content",
271 | name="content",
272 | ),
273 | Button("Update message", type="submit"),
274 | target=self.update_message(),
275 | ),
276 | )
277 |
278 | @Target.post
279 | def update_message(self, form: UpdateMessageForm, /):
280 | self.content = form.content
281 | ```
282 |
283 | Redmage (and the underlying starlette app) must know how to convert your types to strings so that they can be encoded in URLs and converted back to your object types.
284 |
285 | Use the starlette app to register your custom converters according to it's [documentation](https://www.starlette.io/routing/). Redmage will use these converters.
286 |
287 | > Redmage adds a couple of custom convertors that starlette does not provide. One is a boolean converter to convert **bool** type. The other is a custom string converter that is used for **str**. Since Redmage building URLs we need to convert the empty string to _\_\_empty\_\__.
288 |
289 | ### Component State
290 |
291 | The component's state will also be encoded in the url so it can be recreated when the request is issued. Only attributes that have a class annotation will be included. The same converters described above will be used to serialize/de-serialize the component's attributes.
292 |
293 | ```
294 | @dataclass
295 | class UpdateMessageForm:
296 | content: str
297 |
298 |
299 | class MessageAndCounter(Component):
300 | content: str
301 | count: int
302 |
303 | def __init__(self, content, count):
304 | self.content = content
305 | self.count = count
306 |
307 | async def render(self):
308 | return Div(
309 | P(f"{self.content=}"),
310 | Form(
311 | Input(
312 | type="text",
313 | id="content",
314 | name="content",
315 | ),
316 | Button("Update message", type="submit"),
317 | target=self.update_message(),
318 | ),
319 | P(f"{self.count=}"),
320 | Button("Add 1", click=self.update_count(self.count + 1)),
321 | )
322 |
323 | @Target.post
324 | def update_message(self, form: UpdateMessageForm, /):
325 | self.content = form.content
326 |
327 | @Target.post
328 | def update_count(self, count: int):
329 | self.count = count
330 | ```
331 |
332 | In this example, if we didn't add the class annotations, when the message was updated the count would not be set and vice versa, breaking our component.
333 |
334 | ## Triggers
335 |
336 | [htmx triggers](https://htmx.org/docs/#triggers)
337 |
338 | Redmage provides a very thin abstraction over _hx-trigger_ attributes. An element's **trigger** keyword argument can be used to tell Redamge which event type should trigger a component update. You can just pass a string value of the [event](https://developer.mozilla.org/en-US/docs/Web/Events) name.
339 |
340 | ```
341 | class HoverCount(Component):
342 | def __init__(self):
343 | self.count = 0
344 |
345 | async def render(self):
346 | return Div(
347 | self.count,
348 | target=self.set_count(self.count + 1),
349 | trigger="mouseover",
350 | )
351 |
352 | @Target.post
353 | def set_count(self, count: int):
354 | self.count = count
355 | ```
356 |
357 | Now when you mouse over the number the count increases by one. Check the htmx documentation because it provides a number of modifiers that can be used to customize the trigger behavior.
358 |
359 | Redmage has built-in classes that can be used to build triggers as well.
360 |
361 | * **redmage.triggers.Trigger**
362 | * **redmage.triggers.TriggerModifier**
363 | * **redmage.triggers.DelayTriggerModifier**
364 | * **redmage.triggers.ThrottleTriggerModifier**
365 |
366 | > TODO document redmage.triggers.* classes.
367 |
368 | Below is an example of using **Trigger** classes to add a delay to a trigger.
369 |
370 | ```
371 | from redmage.triggers import DelayTriggerModifier, Trigger
372 | from redmage.types import HTMXTrigger
373 |
374 |
375 | class HoverCount(Component):
376 | def __init__(self):
377 | self.count = 0
378 |
379 | async def render(self):
380 | trigger = Trigger(HTMXTrigger.MOUSEOVER, DelayTriggerModifier(1000))
381 |
382 | return Div(
383 | self.count, target=self.set_count(self.count + 1), trigger=trigger
384 | )
385 |
386 | @Target.post
387 | def set_count(self, count: int):
388 | self.count = count
389 | ```
390 |
391 | Now the count will update after one second instead of immediately.
392 |
393 | ## Trigger keywords
394 |
395 | The **Element** class has a number of keyword arguments associated with events that we can pass **Target** objects too. This can simplify our code by not having to add both target and trigger keyword arguments. Below is an example.
396 |
397 | ```
398 | class HoverCount(Component):
399 | def __init__(self):
400 | self.count = 0
401 |
402 | async def render(self):
403 | return Div(
404 | self.count,
405 | mouse_over=self.set_count(self.count + 1),
406 | )
407 |
408 | @Target.post
409 | def set_count(self, count: int):
410 | self.count = count
411 | ```
412 |
413 | The **Element** class currently provides keyword arguments for the following events:
414 |
415 | * click
416 | * submit
417 | * change
418 | * mouse_over
419 | * mouse_enter
420 | * load
421 | * intersect
422 | * revealed
423 |
424 |
425 | ## Render Extensions
426 |
427 | We can use render extensions to inject objects as positional arguments to each **render** method in our application.
428 |
429 | > TODO give an example of how to register a render extension and when you might use one.
430 |
431 |
432 | ## Examples
433 |
434 | > TODO add cool examples.
435 |
--------------------------------------------------------------------------------
/tests/test_core.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import Any, Optional
3 |
4 | import pytest
5 | from starlette.convertors import Convertor, register_url_convertor
6 | from starlette.responses import HTMLResponse
7 | from starlette.testclient import TestClient
8 |
9 | from redmage import Component, Redmage, Target
10 | from redmage.elements import Div, Form, Input
11 | from redmage.exceptions import RedmageError
12 | from redmage.types import HTMXClass, HTMXHeaders, HTMXSwap
13 |
14 |
15 | @pytest.fixture(autouse=True)
16 | def redmage_app():
17 | yield
18 | # Reset app after each test
19 | Component.app = None
20 | Component.components = []
21 |
22 |
23 | def test_sanity():
24 | assert True
25 |
26 |
27 | def test_redmage_create_app():
28 | app = Redmage()
29 | assert app.debug is False
30 |
31 |
32 | def test_redmage_create_app_with_debug():
33 | app = Redmage(debug=True)
34 | assert app.debug is True
35 |
36 |
37 | def test_redmage_create_app_with_middlware():
38 | class MockMiddleware:
39 | def __init__(self, app):
40 | self.app = app
41 |
42 | async def __call__(self, scope, receive, send):
43 | await self.app(scope, receive, send)
44 |
45 | middleware = [MockMiddleware]
46 | app = Redmage(middleware=middleware)
47 | starlette = app.starlette
48 | assert starlette
49 |
50 |
51 | def test_component_constructor():
52 | app = Redmage()
53 |
54 | class TestComponent(Component):
55 | async def render(self): ...
56 |
57 | component = TestComponent()
58 | assert component.id
59 |
60 |
61 | def test_component_registered_before_app():
62 | class TestComponent(Component):
63 | async def render(self): ...
64 |
65 | component = TestComponent()
66 | assert component.id
67 |
68 |
69 | def test_redmage_register_component_with_no_targets():
70 | app = Redmage()
71 |
72 | class TestComponent(Component):
73 | async def render(self):
74 | return "Hello World"
75 |
76 | app.create_routes()
77 | assert len(app.routes) == 0
78 |
79 |
80 | def test_redmage_register_component_index():
81 | app = Redmage()
82 |
83 | class TestComponent(Component, routes=("/",)):
84 | async def render(self):
85 | return Div("Hello World")
86 |
87 | app.create_routes()
88 | assert len(app.routes) == 1
89 | assert app.routes[0].path == "/"
90 |
91 | client = TestClient(app.starlette)
92 | response = client.get("/")
93 | assert response.status_code == 200
94 |
95 |
96 | def test_redmage_register_component_with_get_target_with_args():
97 | app = Redmage()
98 |
99 | class TestComponent(Component):
100 | async def render(self):
101 | return Div(f"Hello World {self.param1} {self.param2}")
102 |
103 | @Target.get
104 | def test_target(self, param1: int, param2: str):
105 | self.param1 = param1
106 | self.param2 = param2
107 |
108 | app.create_routes()
109 | assert len(app.routes) == 1
110 | assert app.routes[0].name == "test_target"
111 | assert (
112 | app.routes[0].path
113 | == "/TestComponent/{id:str}/test_target/{test_target__param1:int}/{test_target__param2:str}"
114 | )
115 |
116 | client = TestClient(app.starlette)
117 | response = client.get("/TestComponent/1/test_target/1/test")
118 | assert response.status_code == 200
119 | assert response.text.strip() == 'Hello World 1 test
'
120 |
121 |
122 | def test_redmage_register_component_with_get_async_target():
123 | app = Redmage()
124 |
125 | class TestComponent(Component):
126 | async def render(self):
127 | return Div(f"Hello World {self.param1} {self.param2}")
128 |
129 | @Target.get
130 | async def test_target(self, param1: int, param2: str):
131 | self.param1 = param1
132 | self.param2 = param2
133 |
134 | app.create_routes()
135 | assert len(app.routes) == 1
136 | assert app.routes[0].name == "test_target"
137 | assert (
138 | app.routes[0].path
139 | == "/TestComponent/{id:str}/test_target/{test_target__param1:int}/{test_target__param2:str}"
140 | )
141 |
142 | client = TestClient(app.starlette)
143 | response = client.get("/TestComponent/1/test_target/1/test")
144 | # assert response.status_code == 200
145 | # assert response.text.strip() == 'Hello World 1 test
'
146 |
147 |
148 | def test_redmage_register_component_with_post_target_with_args():
149 | app = Redmage()
150 |
151 | @dataclass
152 | class TestSerializer:
153 | param1: int
154 | param2: str
155 |
156 | class TestComponent(Component):
157 | async def render(self):
158 | return Div(f"Hello World {self.param1} {self.param2}")
159 |
160 | @Target.post
161 | def test_target(self, test_serializer: TestSerializer, /):
162 | self.param1 = test_serializer.param1
163 | self.param2 = test_serializer.param2
164 |
165 | app.create_routes()
166 | assert len(app.routes) == 1
167 | assert app.routes[0].name == "test_target"
168 | assert app.routes[0].path == "/TestComponent/{id:str}/test_target"
169 |
170 | client = TestClient(app.starlette)
171 | response = client.post(
172 | "/TestComponent/1/test_target", data={"param1": 1, "param2": "test"}
173 | )
174 | assert response.status_code == 200
175 | assert response.text.strip() == 'Hello World 1 test
'
176 |
177 |
178 | def test_redmage_register_component_with_put_target_with_args():
179 | app = Redmage()
180 |
181 | @dataclass
182 | class TestSerializer:
183 | param1: int
184 | param2: str
185 |
186 | class TestComponent(Component):
187 | async def render(self):
188 | return Div(f"Hello World {self.param1} {self.param2}")
189 |
190 | @Target.put
191 | def test_target(self, test_serializer: TestSerializer, /):
192 | self.param1 = test_serializer.param1
193 | self.param2 = test_serializer.param2
194 |
195 | app.create_routes()
196 | assert len(app.routes) == 1
197 | assert app.routes[0].name == "test_target"
198 | assert app.routes[0].path == "/TestComponent/{id:str}/test_target"
199 |
200 | client = TestClient(app.starlette)
201 | response = client.put(
202 | "/TestComponent/1/test_target", data={"param1": 1, "param2": "test"}
203 | )
204 | assert response.status_code == 200
205 | assert response.text.strip() == 'Hello World 1 test
'
206 |
207 |
208 | def test_redmage_register_component_with_patch_target_with_args():
209 | app = Redmage()
210 |
211 | @dataclass
212 | class TestSerializer:
213 | param1: int
214 | param2: str
215 |
216 | class TestComponent(Component):
217 | async def render(self):
218 | return Div(f"Hello World {self.param1} {self.param2}")
219 |
220 | @Target.patch
221 | def test_target(self, test_serializer: TestSerializer, /):
222 | self.param1 = test_serializer.param1
223 | self.param2 = test_serializer.param2
224 |
225 | app.create_routes()
226 | assert len(app.routes) == 1
227 | assert app.routes[0].name == "test_target"
228 | assert app.routes[0].path == "/TestComponent/{id:str}/test_target"
229 |
230 | client = TestClient(app.starlette)
231 | response = client.patch(
232 | "/TestComponent/1/test_target", data={"param1": 1, "param2": "test"}
233 | )
234 | assert response.status_code == 200
235 | assert response.text.strip() == 'Hello World 1 test
'
236 |
237 |
238 | def test_redmage_register_component_with_delete_target_with_args():
239 | app = Redmage()
240 |
241 | class TestComponent(Component):
242 | async def render(self):
243 | return Div(f"Hello World {self.param1} {self.param2}")
244 |
245 | @Target.delete
246 | def test_target(self, param1: int, param2: str):
247 | self.param1 = param1
248 | self.param2 = param2
249 |
250 | app.create_routes()
251 | assert len(app.routes) == 1
252 | assert app.routes[0].name == "test_target"
253 | assert (
254 | app.routes[0].path
255 | == "/TestComponent/{id:str}/test_target/{test_target__param1:int}/{test_target__param2:str}"
256 | )
257 |
258 | client = TestClient(app.starlette)
259 | response = client.delete("/TestComponent/1/test_target/1/test")
260 | assert response.status_code == 200
261 | assert response.text.strip() == 'Hello World 1 test
'
262 |
263 |
264 | def test_redmage_register_component_nested():
265 | app = Redmage()
266 |
267 | class NestedComponent(Component):
268 | async def render(self):
269 | return Div(f"Hello World")
270 |
271 | @property
272 | def id(self) -> str:
273 | return "NestedComponent-1"
274 |
275 | class TestComponent(Component, routes=("/",)):
276 | async def render(self):
277 | return Div(NestedComponent())
278 |
279 | @property
280 | def id(self) -> str:
281 | return "TestComponent-1"
282 |
283 | app.create_routes()
284 |
285 | client = TestClient(app.starlette)
286 | response = client.get("/")
287 | assert response.status_code == 200
288 | assert (
289 | response.text.strip()
290 | == ''
291 | )
292 |
293 |
294 | def test_redmage_escape_element_content():
295 | app = Redmage()
296 |
297 | class TestComponent(Component, routes=("/",)):
298 | async def render(self):
299 | return Div("test
")
300 |
301 | @property
302 | def id(self) -> str:
303 | return "TestComponent-1"
304 |
305 | app.create_routes()
306 |
307 | client = TestClient(app.starlette)
308 | response = client.get("/")
309 | assert response.status_code == 200
310 | assert (
311 | response.text.strip()
312 | == '<h1>test</h1>
'
313 | )
314 |
315 |
316 | def test_redmage_do_not_escape_element_content():
317 | app = Redmage()
318 |
319 | class TestComponent(Component, routes=("/",)):
320 | async def render(self):
321 | return Div("test
", safe=True)
322 |
323 | @property
324 | def id(self) -> str:
325 | return "TestComponent-1"
326 |
327 | app.create_routes()
328 |
329 | client = TestClient(app.starlette)
330 | response = client.get("/")
331 | assert response.status_code == 200
332 | assert response.text.strip() == 'test
'
333 |
334 |
335 | def test_redmage_register_component_with_get_target_with_query_params():
336 | app = Redmage()
337 |
338 | class TestComponent(Component):
339 | async def render(self):
340 | return Div(f"Hello World {self.param1} {self.param2}")
341 |
342 | @Target.get
343 | def test_target(self, param1: int = 0, param2: str = "init"):
344 | self.param1 = param1
345 | self.param2 = param2
346 |
347 | app.create_routes()
348 | assert len(app.routes) == 1
349 | assert app.routes[0].name == "test_target"
350 | assert app.routes[0].path == "/TestComponent/{id:str}/test_target"
351 |
352 | client = TestClient(app.starlette)
353 | response = client.get("/TestComponent/1/test_target?test_target__param1=1")
354 | assert response.status_code == 200
355 | assert response.text.strip() == 'Hello World 1 init
'
356 |
357 | client = TestClient(app.starlette)
358 | response = client.get(
359 | "/TestComponent/1/test_target?test_target__param1=2&test_target__param2=test"
360 | )
361 | assert response.status_code == 200
362 | assert response.text.strip() == 'Hello World 2 test
'
363 |
364 |
365 | def test_redmage_register_component_with_annotations():
366 | app = Redmage()
367 |
368 | class TestComponent(Component):
369 | test_annotation: str
370 |
371 | async def render(self):
372 | return Div(
373 | f"Hello World {self.test_annotation} {self.param1} {self.param2}"
374 | )
375 |
376 | @Target.get
377 | def test_target(self, param1: int = 0, param2: str = "init"):
378 | self.param1 = param1
379 | self.param2 = param2
380 |
381 | app.create_routes()
382 | assert len(app.routes) == 1
383 | assert app.routes[0].name == "test_target"
384 | assert (
385 | app.routes[0].path
386 | == "/TestComponent/{id:str}/test_annotation/{test_annotation:str}/test_target"
387 | )
388 |
389 | client = TestClient(app.starlette)
390 | response = client.get(
391 | "/TestComponent/1/test_annotation/new_annotation/test_target?test_target__param1=1"
392 | )
393 | assert response.status_code == 200
394 | assert (
395 | response.text.strip()
396 | == 'Hello World new_annotation 1 init
'
397 | )
398 |
399 |
400 | def test_redmage_create_get_target():
401 | app = Redmage()
402 |
403 | class TestComponent(Component):
404 | async def render(self):
405 | return Div(
406 | f"Hello World",
407 | target=self.test_target(1, param2="test"),
408 | )
409 |
410 | @Target.get
411 | def test_target(self, param1: int, param2: str = "init"): ...
412 |
413 | client = TestClient(app.starlette)
414 | response = client.get("/TestComponent/1/test_target/0/?test_target__param2=init")
415 | assert response.status_code == 200
416 | assert (
417 | response.text.strip()
418 | == f'Hello World
'
419 | )
420 |
421 |
422 | def test_redmage_create_get_target_with_optional_parameter():
423 | app = Redmage()
424 |
425 | class OptionalInt(Convertor):
426 | regex = "[0-9]+|None"
427 |
428 | def convert(self, value: str) -> Optional[int]:
429 | return int(value) if value != "None" else None
430 |
431 | def to_string(self, value: Optional[int]) -> str:
432 | return str(value) if value else "None"
433 |
434 | register_url_convertor("Optional[int]", OptionalInt())
435 |
436 | class TestComponent(Component):
437 | async def render(self):
438 | return Div(
439 | f"Hello World",
440 | target=self.test_target(1, param2=2),
441 | )
442 |
443 | @Target.get
444 | def test_target(self, param1: int, param2: "Optional[int]" = None): ...
445 |
446 | client = TestClient(app.starlette)
447 | response = client.get("/TestComponent/1/test_target/1/?test_target__param2=None")
448 | assert response.status_code == 200
449 | assert (
450 | response.text.strip()
451 | == f'Hello World
'
452 | )
453 |
454 |
455 | def test_redmage_create_get_target_with_annotation():
456 | app = Redmage()
457 |
458 | class TestComponent(Component):
459 | test_annotation: str
460 |
461 | async def render(self):
462 | return Div(
463 | f"Hello World",
464 | target=self.test_target(1, param2="test"),
465 | )
466 |
467 | @Target.get
468 | def test_target(self, param1: int, param2: str = "init"): ...
469 |
470 | client = TestClient(app.starlette)
471 | response = client.get(
472 | "/TestComponent/1/test_annotation/test/test_target/0/?test_target__param2=init"
473 | )
474 | assert response.status_code == 200
475 | assert (
476 | response.text.strip()
477 | == f'Hello World
'
478 | )
479 |
480 |
481 | def test_redmage_create_post_target():
482 | app = Redmage()
483 |
484 | class TestComponent(Component):
485 | async def render(self):
486 | return Div(
487 | f"Hello World",
488 | target=self.test_target(self.param1),
489 | )
490 |
491 | @Target.post
492 | def test_target(self, test_id: int):
493 | self.param1 = test_id
494 |
495 | client = TestClient(app.starlette)
496 | response = client.post("/TestComponent/1/test_target/4")
497 | assert response.status_code == 200
498 | assert (
499 | response.text.strip()
500 | == f'Hello World
'
501 | )
502 |
503 |
504 | def test_redmage_create_post_target_swap():
505 | app = Redmage()
506 |
507 | class TestComponent(Component):
508 | async def render(self):
509 | return Div(
510 | f"Hello World",
511 | target=self.test_target(1, "bar"),
512 | swap=HTMXSwap.INNER_HTML,
513 | )
514 |
515 | @Target.post
516 | def test_target(self, test_id: int, test: str = "foo"): ...
517 |
518 | client = TestClient(app.starlette)
519 | response = client.post("/TestComponent/1/test_target/1")
520 | assert response.status_code == 200
521 | assert (
522 | response.text.strip()
523 | == f'Hello World
'
524 | )
525 |
526 |
527 | def test_redmage_create_post_target_indicator():
528 | app = Redmage()
529 |
530 | class TestComponent(Component):
531 | async def render(self):
532 | return Div(
533 | f"Hello World",
534 | target=self.test_target(),
535 | indicator=True,
536 | )
537 |
538 | @Target.post
539 | def test_target(self): ...
540 |
541 | client = TestClient(app.starlette)
542 | response = client.post("/TestComponent/1/test_target")
543 | assert response.status_code == 200
544 | assert (
545 | response.text.strip()
546 | == f'Hello World
'
547 | )
548 |
549 |
550 | def test_redmage_form():
551 | app = Redmage()
552 |
553 | @dataclass
554 | class TestSerializer:
555 | param1: int
556 |
557 | class TestComponent(Component):
558 | async def render(self):
559 | return Form(
560 | Input(name="param1", value=1),
561 | target=self.test_target(),
562 | )
563 |
564 | @Target.post
565 | def test_target(self, test_serializer: TestSerializer, /): ...
566 |
567 | client = TestClient(app.starlette)
568 | response = client.post("/TestComponent/1/test_target", data={"param1": 1})
569 | assert response.status_code == 200
570 | assert (
571 | response.text.strip()
572 | == f''
573 | )
574 |
575 |
576 | def test_redmage_form_without_serializer():
577 | app = Redmage()
578 |
579 | class TestComponent(Component):
580 | async def render(self):
581 | return Form(
582 | Input(name="param1", value=1),
583 | target=self.test_target(),
584 | )
585 |
586 | @Target.post
587 | def test_target(self): ...
588 |
589 | client = TestClient(app.starlette)
590 | with pytest.raises(RedmageError):
591 | client.post("/TestComponent/1/test_target", data={"param1": 1})
592 |
593 |
594 | def test_redmage_component_that_returns_another_component():
595 | app = Redmage()
596 |
597 | class ChildComponent(Component):
598 | async def render(self):
599 | return Div(f"Hello Child")
600 |
601 | class TestComponent(Component):
602 | async def render(self):
603 | return Div(f"Hello World")
604 |
605 | @Target.get
606 | def test_target(self):
607 | child = ChildComponent()
608 | child._id = "ChildComponent-1"
609 | return child
610 |
611 | client = TestClient(app.starlette)
612 | response = client.get("/TestComponent/1/test_target")
613 | assert response.status_code == 200
614 | assert response.text.strip() == 'Hello Child
'
615 |
616 |
617 | def test_redmage_component_that_returns_multiple_components():
618 | app = Redmage()
619 |
620 | class ChildComponent(Component):
621 | async def render(self):
622 | return Div(f"Hello Child")
623 |
624 | class TestComponent(Component):
625 | async def render(self):
626 | return Div(f"Hello World")
627 |
628 | @Target.get
629 | def test_target(self):
630 | child = ChildComponent()
631 | child._id = "ChildComponent-1"
632 | return child, child
633 |
634 | client = TestClient(app.starlette)
635 | response = client.get("/TestComponent/1/test_target")
636 | assert response.status_code == 200
637 | assert (
638 | response.text.strip()
639 | == 'Hello Child
\n\nHello Child
'
640 | )
641 |
642 |
643 | def test_redmage_component_that_returns_multiple_components_async():
644 | app = Redmage()
645 |
646 | class ChildComponent(Component):
647 | async def render(self):
648 | return Div(f"Hello Child")
649 |
650 | class TestComponent(Component):
651 | async def render(self):
652 | return Div(f"Hello World")
653 |
654 | @Target.get
655 | async def test_target(self):
656 | child = ChildComponent()
657 | child._id = "ChildComponent-1"
658 | return child, child
659 |
660 | client = TestClient(app.starlette)
661 | response = client.get("/TestComponent/1/test_target")
662 | assert response.status_code == 200
663 | assert (
664 | response.text.strip()
665 | == 'Hello Child
\n\nHello Child
'
666 | )
667 |
668 |
669 | def test_redmage_component_that_returns_headers_in_the_options_dict():
670 | app = Redmage()
671 |
672 | class TestComponent(Component):
673 | async def render(self):
674 | return Div(f"Hello World")
675 |
676 | @Target.get
677 | def test_target(self):
678 | return Div(f"Hello World")
679 |
680 | @staticmethod
681 | def build_response(content: Any) -> HTMLResponse:
682 | return HTMLResponse(
683 | content=content, headers={HTMXHeaders.HX_LOCATION: "test"}
684 | )
685 |
686 | client = TestClient(app.starlette)
687 | response = client.get("/TestComponent/1/test_target")
688 | assert response.status_code == 200
689 | assert response.headers[HTMXHeaders.HX_LOCATION] == "test"
690 |
691 |
692 | def test_redamge_component_with_render_extenstion():
693 | app = Redmage()
694 |
695 | def extension(el):
696 | el.attrs(test="test")
697 | return el
698 |
699 | Component.add_render_extension(extension=extension)
700 |
701 | class TestComponent(Component):
702 | async def render(self, extension=extension):
703 | return extension(Div(f"Hello World"))
704 |
705 | @Target.get
706 | def test_target(self): ...
707 |
708 | client = TestClient(app.starlette)
709 | response = client.get("/TestComponent/1/test_target")
710 | assert response.status_code == 200
711 | assert (
712 | response.text.strip()
713 | == 'Hello World
'
714 | )
715 |
716 |
717 | def test_redamge_component_with_render_extenstion_var_keyword():
718 | app = Redmage()
719 |
720 | def extension(el):
721 | el.attrs(test="test")
722 | return el
723 |
724 | Component.add_render_extension(extension=extension)
725 |
726 | class TestComponent(Component):
727 | async def render(self, **exts):
728 | return exts["extension"](Div(f"Hello World"))
729 |
730 | @Target.get
731 | def test_target(self): ...
732 |
733 | client = TestClient(app.starlette)
734 | response = client.get("/TestComponent/1/test_target")
735 | assert response.status_code == 200
736 | assert (
737 | response.text.strip()
738 | == 'Hello World
'
739 | )
740 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "anyio"
5 | version = "4.1.0"
6 | description = "High level compatibility layer for multiple asynchronous event loop implementations"
7 | optional = false
8 | python-versions = ">=3.8"
9 | files = [
10 | {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"},
11 | {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"},
12 | ]
13 |
14 | [package.dependencies]
15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
16 | idna = ">=2.8"
17 | sniffio = ">=1.1"
18 |
19 | [package.extras]
20 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
21 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
22 | trio = ["trio (>=0.23)"]
23 |
24 | [[package]]
25 | name = "black"
26 | version = "24.1.1"
27 | description = "The uncompromising code formatter."
28 | optional = false
29 | python-versions = ">=3.8"
30 | files = [
31 | {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"},
32 | {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"},
33 | {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"},
34 | {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"},
35 | {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"},
36 | {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"},
37 | {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"},
38 | {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"},
39 | {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"},
40 | {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"},
41 | {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"},
42 | {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"},
43 | {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"},
44 | {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"},
45 | {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"},
46 | {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"},
47 | {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"},
48 | {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"},
49 | {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"},
50 | {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"},
51 | {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"},
52 | {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"},
53 | ]
54 |
55 | [package.dependencies]
56 | click = ">=8.0.0"
57 | mypy-extensions = ">=0.4.3"
58 | packaging = ">=22.0"
59 | pathspec = ">=0.9.0"
60 | platformdirs = ">=2"
61 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
62 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
63 |
64 | [package.extras]
65 | colorama = ["colorama (>=0.4.3)"]
66 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
67 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
68 | uvloop = ["uvloop (>=0.15.2)"]
69 |
70 | [[package]]
71 | name = "certifi"
72 | version = "2023.11.17"
73 | description = "Python package for providing Mozilla's CA Bundle."
74 | optional = false
75 | python-versions = ">=3.6"
76 | files = [
77 | {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
78 | {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
79 | ]
80 |
81 | [[package]]
82 | name = "click"
83 | version = "8.1.7"
84 | description = "Composable command line interface toolkit"
85 | optional = false
86 | python-versions = ">=3.7"
87 | files = [
88 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
89 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
90 | ]
91 |
92 | [package.dependencies]
93 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
94 |
95 | [[package]]
96 | name = "colorama"
97 | version = "0.4.6"
98 | description = "Cross-platform colored terminal text."
99 | optional = false
100 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
101 | files = [
102 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
103 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
104 | ]
105 |
106 | [[package]]
107 | name = "coverage"
108 | version = "7.3.3"
109 | description = "Code coverage measurement for Python"
110 | optional = false
111 | python-versions = ">=3.8"
112 | files = [
113 | {file = "coverage-7.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d874434e0cb7b90f7af2b6e3309b0733cde8ec1476eb47db148ed7deeb2a9494"},
114 | {file = "coverage-7.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee6621dccce8af666b8c4651f9f43467bfbf409607c604b840b78f4ff3619aeb"},
115 | {file = "coverage-7.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1367aa411afb4431ab58fd7ee102adb2665894d047c490649e86219327183134"},
116 | {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f0f8f0c497eb9c9f18f21de0750c8d8b4b9c7000b43996a094290b59d0e7523"},
117 | {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0338c4b0951d93d547e0ff8d8ea340fecf5885f5b00b23be5aa99549e14cfd"},
118 | {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d31650d313bd90d027f4be7663dfa2241079edd780b56ac416b56eebe0a21aab"},
119 | {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9437a4074b43c177c92c96d051957592afd85ba00d3e92002c8ef45ee75df438"},
120 | {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9e17d9cb06c13b4f2ef570355fa45797d10f19ca71395910b249e3f77942a837"},
121 | {file = "coverage-7.3.3-cp310-cp310-win32.whl", hash = "sha256:eee5e741b43ea1b49d98ab6e40f7e299e97715af2488d1c77a90de4a663a86e2"},
122 | {file = "coverage-7.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:593efa42160c15c59ee9b66c5f27a453ed3968718e6e58431cdfb2d50d5ad284"},
123 | {file = "coverage-7.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c944cf1775235c0857829c275c777a2c3e33032e544bcef614036f337ac37bb"},
124 | {file = "coverage-7.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eda7f6e92358ac9e1717ce1f0377ed2b9320cea070906ece4e5c11d172a45a39"},
125 | {file = "coverage-7.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c854c1d2c7d3e47f7120b560d1a30c1ca221e207439608d27bc4d08fd4aeae8"},
126 | {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:222b038f08a7ebed1e4e78ccf3c09a1ca4ac3da16de983e66520973443b546bc"},
127 | {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff4800783d85bff132f2cc7d007426ec698cdce08c3062c8d501ad3f4ea3d16c"},
128 | {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fc200cec654311ca2c3f5ab3ce2220521b3d4732f68e1b1e79bef8fcfc1f2b97"},
129 | {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:307aecb65bb77cbfebf2eb6e12009e9034d050c6c69d8a5f3f737b329f4f15fb"},
130 | {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ffb0eacbadb705c0a6969b0adf468f126b064f3362411df95f6d4f31c40d31c1"},
131 | {file = "coverage-7.3.3-cp311-cp311-win32.whl", hash = "sha256:79c32f875fd7c0ed8d642b221cf81feba98183d2ff14d1f37a1bbce6b0347d9f"},
132 | {file = "coverage-7.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:243576944f7c1a1205e5cd658533a50eba662c74f9be4c050d51c69bd4532936"},
133 | {file = "coverage-7.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a2ac4245f18057dfec3b0074c4eb366953bca6787f1ec397c004c78176a23d56"},
134 | {file = "coverage-7.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9191be7af41f0b54324ded600e8ddbcabea23e1e8ba419d9a53b241dece821d"},
135 | {file = "coverage-7.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c0b1b8b5a4aebf8fcd227237fc4263aa7fa0ddcd4d288d42f50eff18b0bac4"},
136 | {file = "coverage-7.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee453085279df1bac0996bc97004771a4a052b1f1e23f6101213e3796ff3cb85"},
137 | {file = "coverage-7.3.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1191270b06ecd68b1d00897b2daddb98e1719f63750969614ceb3438228c088e"},
138 | {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:007a7e49831cfe387473e92e9ff07377f6121120669ddc39674e7244350a6a29"},
139 | {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:af75cf83c2d57717a8493ed2246d34b1f3398cb8a92b10fd7a1858cad8e78f59"},
140 | {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:811ca7373da32f1ccee2927dc27dc523462fd30674a80102f86c6753d6681bc6"},
141 | {file = "coverage-7.3.3-cp312-cp312-win32.whl", hash = "sha256:733537a182b5d62184f2a72796eb6901299898231a8e4f84c858c68684b25a70"},
142 | {file = "coverage-7.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:e995efb191f04b01ced307dbd7407ebf6e6dc209b528d75583277b10fd1800ee"},
143 | {file = "coverage-7.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fbd8a5fe6c893de21a3c6835071ec116d79334fbdf641743332e442a3466f7ea"},
144 | {file = "coverage-7.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:50c472c1916540f8b2deef10cdc736cd2b3d1464d3945e4da0333862270dcb15"},
145 | {file = "coverage-7.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e9223a18f51d00d3ce239c39fc41410489ec7a248a84fab443fbb39c943616c"},
146 | {file = "coverage-7.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f501e36ac428c1b334c41e196ff6bd550c0353c7314716e80055b1f0a32ba394"},
147 | {file = "coverage-7.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475de8213ed95a6b6283056d180b2442eee38d5948d735cd3d3b52b86dd65b92"},
148 | {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afdcc10c01d0db217fc0a64f58c7edd635b8f27787fea0a3054b856a6dff8717"},
149 | {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fff0b2f249ac642fd735f009b8363c2b46cf406d3caec00e4deeb79b5ff39b40"},
150 | {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a1f76cfc122c9e0f62dbe0460ec9cc7696fc9a0293931a33b8870f78cf83a327"},
151 | {file = "coverage-7.3.3-cp38-cp38-win32.whl", hash = "sha256:757453848c18d7ab5d5b5f1827293d580f156f1c2c8cef45bfc21f37d8681069"},
152 | {file = "coverage-7.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad2453b852a1316c8a103c9c970db8fbc262f4f6b930aa6c606df9b2766eee06"},
153 | {file = "coverage-7.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b15e03b8ee6a908db48eccf4e4e42397f146ab1e91c6324da44197a45cb9132"},
154 | {file = "coverage-7.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89400aa1752e09f666cc48708eaa171eef0ebe3d5f74044b614729231763ae69"},
155 | {file = "coverage-7.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c59a3e59fb95e6d72e71dc915e6d7fa568863fad0a80b33bc7b82d6e9f844973"},
156 | {file = "coverage-7.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ede881c7618f9cf93e2df0421ee127afdfd267d1b5d0c59bcea771cf160ea4a"},
157 | {file = "coverage-7.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3bfd2c2f0e5384276e12b14882bf2c7621f97c35320c3e7132c156ce18436a1"},
158 | {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f3bad1a9313401ff2964e411ab7d57fb700a2d5478b727e13f156c8f89774a0"},
159 | {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:65d716b736f16e250435473c5ca01285d73c29f20097decdbb12571d5dfb2c94"},
160 | {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a702e66483b1fe602717020a0e90506e759c84a71dbc1616dd55d29d86a9b91f"},
161 | {file = "coverage-7.3.3-cp39-cp39-win32.whl", hash = "sha256:7fbf3f5756e7955174a31fb579307d69ffca91ad163467ed123858ce0f3fd4aa"},
162 | {file = "coverage-7.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cad9afc1644b979211989ec3ff7d82110b2ed52995c2f7263e7841c846a75348"},
163 | {file = "coverage-7.3.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:d299d379b676812e142fb57662a8d0d810b859421412b4d7af996154c00c31bb"},
164 | {file = "coverage-7.3.3.tar.gz", hash = "sha256:df04c64e58df96b4427db8d0559e95e2df3138c9916c96f9f6a4dd220db2fdb7"},
165 | ]
166 |
167 | [package.dependencies]
168 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
169 |
170 | [package.extras]
171 | toml = ["tomli"]
172 |
173 | [[package]]
174 | name = "exceptiongroup"
175 | version = "1.2.0"
176 | description = "Backport of PEP 654 (exception groups)"
177 | optional = false
178 | python-versions = ">=3.7"
179 | files = [
180 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
181 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
182 | ]
183 |
184 | [package.extras]
185 | test = ["pytest (>=6)"]
186 |
187 | [[package]]
188 | name = "h11"
189 | version = "0.14.0"
190 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
191 | optional = false
192 | python-versions = ">=3.7"
193 | files = [
194 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
195 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
196 | ]
197 |
198 | [[package]]
199 | name = "httpcore"
200 | version = "0.17.3"
201 | description = "A minimal low-level HTTP client."
202 | optional = false
203 | python-versions = ">=3.7"
204 | files = [
205 | {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
206 | {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
207 | ]
208 |
209 | [package.dependencies]
210 | anyio = ">=3.0,<5.0"
211 | certifi = "*"
212 | h11 = ">=0.13,<0.15"
213 | sniffio = "==1.*"
214 |
215 | [package.extras]
216 | http2 = ["h2 (>=3,<5)"]
217 | socks = ["socksio (==1.*)"]
218 |
219 | [[package]]
220 | name = "httpx"
221 | version = "0.24.1"
222 | description = "The next generation HTTP client."
223 | optional = false
224 | python-versions = ">=3.7"
225 | files = [
226 | {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
227 | {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
228 | ]
229 |
230 | [package.dependencies]
231 | certifi = "*"
232 | httpcore = ">=0.15.0,<0.18.0"
233 | idna = "*"
234 | sniffio = "*"
235 |
236 | [package.extras]
237 | brotli = ["brotli", "brotlicffi"]
238 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
239 | http2 = ["h2 (>=3,<5)"]
240 | socks = ["socksio (==1.*)"]
241 |
242 | [[package]]
243 | name = "hype-html"
244 | version = "2.1.2"
245 | description = "A minimal python dsl for generating html."
246 | optional = false
247 | python-versions = ">=3.7,<4.0"
248 | files = [
249 | {file = "hype_html-2.1.2-py3-none-any.whl", hash = "sha256:aa1286fee494804f6b95a3d8ec3c69154d34e84dec081852d30db822536afa79"},
250 | {file = "hype_html-2.1.2.tar.gz", hash = "sha256:6f522a11ce880e32638529717a66354e79936663d6b9cda46a61b7c56322bcb4"},
251 | ]
252 |
253 | [[package]]
254 | name = "idna"
255 | version = "3.6"
256 | description = "Internationalized Domain Names in Applications (IDNA)"
257 | optional = false
258 | python-versions = ">=3.5"
259 | files = [
260 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
261 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
262 | ]
263 |
264 | [[package]]
265 | name = "iniconfig"
266 | version = "2.0.0"
267 | description = "brain-dead simple config-ini parsing"
268 | optional = false
269 | python-versions = ">=3.7"
270 | files = [
271 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
272 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
273 | ]
274 |
275 | [[package]]
276 | name = "isort"
277 | version = "5.13.2"
278 | description = "A Python utility / library to sort Python imports."
279 | optional = false
280 | python-versions = ">=3.8.0"
281 | files = [
282 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
283 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
284 | ]
285 |
286 | [package.extras]
287 | colors = ["colorama (>=0.4.6)"]
288 |
289 | [[package]]
290 | name = "jinja2"
291 | version = "3.1.2"
292 | description = "A very fast and expressive template engine."
293 | optional = false
294 | python-versions = ">=3.7"
295 | files = [
296 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
297 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
298 | ]
299 |
300 | [package.dependencies]
301 | MarkupSafe = ">=2.0"
302 |
303 | [package.extras]
304 | i18n = ["Babel (>=2.7)"]
305 |
306 | [[package]]
307 | name = "markupsafe"
308 | version = "2.1.3"
309 | description = "Safely add untrusted strings to HTML/XML markup."
310 | optional = false
311 | python-versions = ">=3.7"
312 | files = [
313 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
314 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
315 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
316 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
317 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
318 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
319 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
320 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
321 | {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
322 | {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
323 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
324 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
325 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
326 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
327 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
328 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
329 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
330 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
331 | {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
332 | {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
333 | {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
334 | {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
335 | {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
336 | {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
337 | {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
338 | {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
339 | {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
340 | {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
341 | {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
342 | {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
343 | {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
344 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
345 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
346 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
347 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
348 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
349 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
350 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
351 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
352 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
353 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
354 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
355 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
356 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
357 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
358 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
359 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
360 | {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
361 | {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
362 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
363 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
364 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
365 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
366 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
367 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
368 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
369 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
370 | {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
371 | {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
372 | {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
373 | ]
374 |
375 | [[package]]
376 | name = "mypy"
377 | version = "1.7.1"
378 | description = "Optional static typing for Python"
379 | optional = false
380 | python-versions = ">=3.8"
381 | files = [
382 | {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"},
383 | {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"},
384 | {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"},
385 | {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"},
386 | {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"},
387 | {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
388 | {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
389 | {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
390 | {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
391 | {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
392 | {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
393 | {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
394 | {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
395 | {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
396 | {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
397 | {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"},
398 | {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"},
399 | {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"},
400 | {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"},
401 | {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"},
402 | {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"},
403 | {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"},
404 | {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"},
405 | {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"},
406 | {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"},
407 | {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
408 | {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
409 | ]
410 |
411 | [package.dependencies]
412 | mypy-extensions = ">=1.0.0"
413 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
414 | typing-extensions = ">=4.1.0"
415 |
416 | [package.extras]
417 | dmypy = ["psutil (>=4.0)"]
418 | install-types = ["pip"]
419 | mypyc = ["setuptools (>=50)"]
420 | reports = ["lxml"]
421 |
422 | [[package]]
423 | name = "mypy-extensions"
424 | version = "1.0.0"
425 | description = "Type system extensions for programs checked with the mypy type checker."
426 | optional = false
427 | python-versions = ">=3.5"
428 | files = [
429 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
430 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
431 | ]
432 |
433 | [[package]]
434 | name = "packaging"
435 | version = "23.2"
436 | description = "Core utilities for Python packages"
437 | optional = false
438 | python-versions = ">=3.7"
439 | files = [
440 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
441 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
442 | ]
443 |
444 | [[package]]
445 | name = "pathspec"
446 | version = "0.12.1"
447 | description = "Utility library for gitignore style pattern matching of file paths."
448 | optional = false
449 | python-versions = ">=3.8"
450 | files = [
451 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
452 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
453 | ]
454 |
455 | [[package]]
456 | name = "platformdirs"
457 | version = "4.2.0"
458 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
459 | optional = false
460 | python-versions = ">=3.8"
461 | files = [
462 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
463 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
464 | ]
465 |
466 | [package.extras]
467 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
468 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
469 |
470 | [[package]]
471 | name = "pluggy"
472 | version = "1.3.0"
473 | description = "plugin and hook calling mechanisms for python"
474 | optional = false
475 | python-versions = ">=3.8"
476 | files = [
477 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
478 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
479 | ]
480 |
481 | [package.extras]
482 | dev = ["pre-commit", "tox"]
483 | testing = ["pytest", "pytest-benchmark"]
484 |
485 | [[package]]
486 | name = "pycodestyle"
487 | version = "2.11.1"
488 | description = "Python style guide checker"
489 | optional = false
490 | python-versions = ">=3.8"
491 | files = [
492 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
493 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
494 | ]
495 |
496 | [[package]]
497 | name = "pytest"
498 | version = "7.4.3"
499 | description = "pytest: simple powerful testing with Python"
500 | optional = false
501 | python-versions = ">=3.7"
502 | files = [
503 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
504 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
505 | ]
506 |
507 | [package.dependencies]
508 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
509 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
510 | iniconfig = "*"
511 | packaging = "*"
512 | pluggy = ">=0.12,<2.0"
513 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
514 |
515 | [package.extras]
516 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
517 |
518 | [[package]]
519 | name = "pytest-asyncio"
520 | version = "0.21.1"
521 | description = "Pytest support for asyncio"
522 | optional = false
523 | python-versions = ">=3.7"
524 | files = [
525 | {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
526 | {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
527 | ]
528 |
529 | [package.dependencies]
530 | pytest = ">=7.0.0"
531 |
532 | [package.extras]
533 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
534 | testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
535 |
536 | [[package]]
537 | name = "pytest-cov"
538 | version = "4.1.0"
539 | description = "Pytest plugin for measuring coverage."
540 | optional = false
541 | python-versions = ">=3.7"
542 | files = [
543 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
544 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
545 | ]
546 |
547 | [package.dependencies]
548 | coverage = {version = ">=5.2.1", extras = ["toml"]}
549 | pytest = ">=4.6"
550 |
551 | [package.extras]
552 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
553 |
554 | [[package]]
555 | name = "python-multipart"
556 | version = "0.0.6"
557 | description = "A streaming multipart parser for Python"
558 | optional = false
559 | python-versions = ">=3.7"
560 | files = [
561 | {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"},
562 | {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"},
563 | ]
564 |
565 | [package.extras]
566 | dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"]
567 |
568 | [[package]]
569 | name = "sniffio"
570 | version = "1.3.0"
571 | description = "Sniff out which async library your code is running under"
572 | optional = false
573 | python-versions = ">=3.7"
574 | files = [
575 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
576 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
577 | ]
578 |
579 | [[package]]
580 | name = "starlette"
581 | version = "0.26.1"
582 | description = "The little ASGI library that shines."
583 | optional = false
584 | python-versions = ">=3.7"
585 | files = [
586 | {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"},
587 | {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"},
588 | ]
589 |
590 | [package.dependencies]
591 | anyio = ">=3.4.0,<5"
592 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
593 |
594 | [package.extras]
595 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
596 |
597 | [[package]]
598 | name = "tomli"
599 | version = "2.0.1"
600 | description = "A lil' TOML parser"
601 | optional = false
602 | python-versions = ">=3.7"
603 | files = [
604 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
605 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
606 | ]
607 |
608 | [[package]]
609 | name = "typing-extensions"
610 | version = "4.9.0"
611 | description = "Backported and Experimental Type Hints for Python 3.8+"
612 | optional = false
613 | python-versions = ">=3.8"
614 | files = [
615 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
616 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
617 | ]
618 |
619 | [[package]]
620 | name = "uvicorn"
621 | version = "0.22.0"
622 | description = "The lightning-fast ASGI server."
623 | optional = false
624 | python-versions = ">=3.7"
625 | files = [
626 | {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"},
627 | {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"},
628 | ]
629 |
630 | [package.dependencies]
631 | click = ">=7.0"
632 | h11 = ">=0.8"
633 |
634 | [package.extras]
635 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
636 |
637 | [metadata]
638 | lock-version = "2.0"
639 | python-versions = "^3.8"
640 | content-hash = "3cb985017ab12a9bb93f7afa9545dd1911df6d4cff18eef20a7439fb94ce58f4"
641 |
--------------------------------------------------------------------------------