Fix most of types

This commit is contained in:
2025-07-16 17:48:39 +03:00
parent ee25d17683
commit ddfd6e8d78
21 changed files with 197 additions and 67 deletions

5
mypy.ini Normal file
View File

@@ -0,0 +1,5 @@
[mypy]
python_version = 3.13
strict = True
ignore_missing_imports = True
files = src/

View File

@@ -11,6 +11,7 @@ dependencies = [
"megasniff>=0.2.0",
"breakshaft>=0.1.0",
"case-insensitive-dictionary>=0.2.1",
"mypy>=1.17.0",
]
[tool.uv.sources]
@@ -37,7 +38,7 @@ dev = [
{ "include-group" = "all_containers" },
]
xml = ["lxml>=6.0.0", ]
xml = ["lxml>=6.0.0", "lxml-stubs>=0.5.1"]
msgpack = ["msgpack>=1.1.1", ]
json = ["orjson>=3.11.0", ]

View File

@@ -1 +1,5 @@
import interfaces
import router
from .app import SlothApp
__all__ = ['SlothApp', 'router', 'interfaces']

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from typing import Any
from turbosloth import SlothApp
from turbosloth.exceptions import NotFoundException
from turbosloth.interfaces.serialized import SerializedResponse, SerializedRequest
from turbosloth.types import Scope, Receive, Send, BasicRequest, BasicResponse
app = SlothApp()
@@ -17,6 +17,5 @@ async def index(req: SerializedRequest) -> SerializedResponse:
@app.post("/user/")
async def get_user(req: SerializedRequest) -> SerializedResponse:
print(req.basic.query)
resp = {'message': f'Hello, User ы {req.basic.query["id"]}!', 'from': 'server'}
resp['echo'] = req.body
resp: dict[str, Any] = {'message': f'Hello, User ы {req.basic.query["id"]}!', 'from': 'server', 'echo': req.body}
return SerializedResponse(200, {}, resp)

View File

@@ -1,11 +1,12 @@
import json
from typing import Optional, Callable, Awaitable, Protocol
from .exceptions import HTTPException
from .interfaces.base import BasicRequest, BasicResponse
from .interfaces.serialize_selector import SerializeSelector
from .interfaces.serialized import SerializedResponse, TextSerializedResponse
from .interfaces.serialized import SerializedResponse
from .interfaces.serialized.text import TextSerializedResponse
from .router import Router
from .types import Scope, Receive, Send, MethodType, HandlerType, BasicRequest, BasicResponse
from .types import Scope, Receive, Send, MethodType, HandlerType
class ASGIApp(Protocol):
@@ -18,7 +19,7 @@ class ASGIApp(Protocol):
class HTTPApp(ASGIApp):
serialize_selector: SerializeSelector
async def _do_http(self, scope: Scope, receive: Receive, send: Send):
async def _do_http(self, scope: Scope, receive: Receive, send: Send) -> None:
method = scope['method']
path = scope['path']
@@ -31,9 +32,11 @@ class HTTPApp(ASGIApp):
scope['body'] = body
req = BasicRequest.from_scope(scope)
sresponser = TextSerializedResponse
sresponser: type[SerializedResponse] = TextSerializedResponse
charset = 'latin1'
sresp: SerializedResponse
resp: BasicResponse
try:
handler = self.router.match(method, path)
@@ -59,7 +62,7 @@ class HTTPApp(ASGIApp):
class WSApp(ASGIApp):
async def _do_websocket(self, scope: Scope, receive: Receive, send: Send):
async def _do_websocket(self, scope: Scope, receive: Receive, send: Send) -> None:
raise NotImplementedError()
@@ -67,7 +70,7 @@ class LifespanApp:
_on_startup: Optional[Callable[[], Awaitable[None]]]
_on_shutdown: Optional[Callable[[], Awaitable[None]]]
async def _do_startup(self, send: Send):
async def _do_startup(self, send: Send) -> None:
if self._on_startup:
try:
await self._on_startup()
@@ -77,12 +80,12 @@ class LifespanApp:
else:
await send({'type': 'lifespan.startup.complete'})
async def _do_shutdown(self, send: Send):
async def _do_shutdown(self, send: Send) -> None:
if self._on_shutdown:
await self._on_shutdown()
await send({'type': 'lifespan.shutdown.complete'})
async def _do_lifespan(self, receive: Receive, send: Send):
async def _do_lifespan(self, receive: Receive, send: Send) -> None:
while True:
event = await receive()
if event['type'] == 'lifespan.startup':

View File

@@ -1,2 +1,47 @@
from http_base import HTTPException
from .client_errors import *
from .server_errors import *
__all__ = [
'HTTPException',
'BadRequestException',
'UnauthorizedException',
'PaymentRequiredException',
'ForbiddenException',
'NotFoundException',
'MethodNotAllowedException',
'NotAcceptableException',
'ProxyAuthenticationRequiredException',
'RequestTimeoutException',
'ConflictException',
'GoneException',
'LengthRequiredException',
'PreconditionFailedException',
'ContentTooLargeException',
'URITooLongException',
'UnsupportedMediaTypeException',
'RangeNotSatisfiableException',
'ExpectationFailedException',
'ImaTeapotException',
'MisdirectedRequestException',
'UnprocessableContentException',
'LockedException',
'FailedDependencyException',
'TooEarlyExperimentalException',
'UpgradeRequiredException',
'PreconditionRequiredException',
'TooManyRequestsException',
'RequestHeaderFieldsTooLargeException',
'UnavailableForLegalReasonsException',
'InternalServerError',
'NotImplementedException',
'BadGatewayException',
'ServiceUnavailableException',
'GatewayTimeoutException',
'HTTPVersionNotSupportedException',
'VariantAlsoNegotiatesException',
'InsufficientStorageException',
'LoopDetectedException',
'NotExtendedException',
'NetworkAuthenticationRequiredException',
]

View File

@@ -13,7 +13,7 @@ class HTTPException(Exception):
def AutoException(code: int, description: str) -> Callable[[Optional[str]], HTTPException]:
def foo(message: str | None = None):
def foo(message: str | None = None) -> HTTPException:
return HTTPException(code, description, message)
return foo

View File

@@ -1,4 +1,4 @@
from turbosloth.exceptions import HTTPException
from turbosloth.exceptions.http_base import HTTPException
_server_error_codes = {
500: 'Internal Server Error',

View File

@@ -1,24 +1,28 @@
from __future__ import annotations
import typing
import urllib.parse
from dataclasses import dataclass
from typing import Any, Mapping
from case_insensitive_dict import CaseInsensitiveDict
import urllib.parse
from turbosloth.types import MethodType, Scope, ASGIMessage
@dataclass
class BasicRequest:
method: str
method: MethodType
path: str
headers: CaseInsensitiveDict[str, str]
query: dict[str, list[str] | str]
query: dict[str, list[Any] | Any]
body: bytes
def __init__(self,
method: str,
method: MethodType,
path: str,
headers: Mapping[str, str],
query: dict[str, list[str] | str],
query: dict[str, list[Any] | Any],
body: bytes):
self.method = method
self.path = path
@@ -27,9 +31,9 @@ class BasicRequest:
self.body = body
@classmethod
def from_scope(cls, scope: dict) -> BasicRequest:
def from_scope(cls, scope: Scope) -> BasicRequest:
path = scope['path']
method = scope['method']
method = typing.cast(MethodType, scope['method'])
headers = {}
for key, value in scope.get('headers', []):
headers[key.decode('latin1')] = value.decode('latin1')
@@ -42,6 +46,7 @@ class BasicRequest:
if len(v) == 1:
v = v[0]
query[k] = v
query = typing.cast(dict[str, list[Any] | Any], query)
body = scope['body']
@@ -59,7 +64,7 @@ class BasicResponse:
self.headers = CaseInsensitiveDict(headers)
self.body = body
def into_start_message(self) -> dict:
def into_start_message(self) -> ASGIMessage:
enc_headers = []
for k, v in self.headers.items():
enc_headers.append((k.encode('latin1'), v.encode('latin1')))

View File

@@ -1,10 +1,10 @@
import typing
from typing import NamedTuple, Optional
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.exceptions import NotAcceptableException
from turbosloth.interfaces.serialized import SerializedRequest, SerializedResponse, default_serializers
from turbosloth.util import parse_content_type
@@ -12,11 +12,11 @@ class SerializeChoise(NamedTuple):
req: type[SerializedRequest]
resp: type[SerializedResponse]
charset: str
content_properties: dict
content_properties: dict[str, str]
class SerializeSelector:
default_content_type: str
default_content_type: Optional[str]
serializers: dict[str, tuple[type[SerializedRequest], type[SerializedResponse]]]
def __init__(self,
@@ -27,7 +27,7 @@ class SerializeSelector:
ser = {}
if filter_content_types is None:
filter_content_types = default_serializers.keys()
filter_content_types = list(default_serializers.keys())
for k, v in default_serializers.items():
if k in filter_content_types:
@@ -36,6 +36,7 @@ class SerializeSelector:
def select(self, headers: CaseInsensitiveDict[str, str]) -> SerializeChoise:
contenttype_header = headers.get('Content-Type')
properties: dict[str, str]
if contenttype_header is None:
contenttype = self.default_content_type
properties = {}
@@ -50,8 +51,8 @@ class SerializeSelector:
else:
charset = 'latin1'
choise = self.serializers.get(contenttype)
if choise is None:
choise = self.serializers.get(typing.cast(str, contenttype))
if choise is None and self.default_content_type is not None:
choise = self.serializers.get(self.default_content_type)
if choise is None:
raise NotAcceptableException('acceptable content types: ' + ', '.join(self.serializers.keys()))

View File

@@ -1,6 +1,6 @@
from .base import SerializedRequest, SerializedResponse
default_serializers = {}
default_serializers: dict[str, tuple[type[SerializedRequest], type[SerializedResponse]]] = {}
try:
from .text import TextSerializedRequest, TextSerializedResponse
@@ -28,3 +28,5 @@ try:
default_serializers['application/vnd.msgpack'] = (MessagePackSerializedRequest, MessagePackSerializedResponse)
except:
pass
__all__ = ['SerializedRequest', 'SerializedResponse']

View File

@@ -1,17 +1,36 @@
from dataclasses import dataclass
from typing import Any, Mapping
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from turbosloth.types import MethodType
@dataclass
class SerializedRequest:
body: dict[str, Any] | list | str | None
body: dict[str, Any] | list[Any] | str | None
basic: BasicRequest
charset: str
@property
def query(self) -> dict[str, list[Any] | Any]:
return self.basic.query
@property
def path(self) -> str:
return self.basic.path
@property
def method(self) -> MethodType:
return self.basic.method
@property
def headers(self) -> CaseInsensitiveDict[str, str]:
return self.basic.headers
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
raise NotImplementedError()
@@ -19,7 +38,7 @@ class SerializedRequest:
class SerializedResponse:
code: int
headers: Mapping[str, str]
body: dict[str, Any] | list | str
body: dict[str, Any] | list[Any] | str
def into_basic(self, charset: str) -> BasicResponse:
raise NotImplementedError()

View File

@@ -1,5 +1,4 @@
import orjson
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.interfaces.base import BasicRequest, BasicResponse
@@ -8,7 +7,7 @@ from .base import SerializedRequest, SerializedResponse
class JsonSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
if len(basic.body) == 0:
b = None
elif charset.lower() in {'utf-8', 'utf8'}:

View File

@@ -7,7 +7,7 @@ from .base import SerializedRequest, SerializedResponse
class MessagePackSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
if len(basic.body) == 0:
b = None
else:

View File

@@ -6,7 +6,7 @@ from .base import SerializedRequest, SerializedResponse
class TextSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
b = basic.body.decode(charset)
return cls(b, basic, charset)

View File

@@ -1,17 +1,15 @@
import json
from typing import Any
from case_insensitive_dict import CaseInsensitiveDict
from lxml import etree
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from .base import SerializedRequest, SerializedResponse
from lxml import etree
class XMLSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
if len(basic.body) == 0:
b = {}
else:
@@ -45,19 +43,15 @@ def _into_xml(data: dict[str, Any] | list[Any] | str) -> str:
elif value is not None:
element.text = str(value)
# Создаем корневой элемент
root = etree.Element('response')
if isinstance(data, dict):
# Если входной тип — dict, используем его ключи как теги
for key, value in data.items():
_build_element(root, key, value)
elif isinstance(data, list):
# Если список, оборачиваем элементы в общий тег "item"
for item in data:
_build_element(root, 'item', item)
elif isinstance(data, str):
# Если строка, добавляем как текст корневого элемента
root.text = data
return etree.tostring(root, pretty_print=True, encoding="unicode")

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
import typing
from typing import Optional, Sequence, get_args
from typing import Optional, Sequence
from .exceptions import MethodNotAllowedException, NotFoundException, HTTPException
from .exceptions import MethodNotAllowedException, NotFoundException
from .types import HandlerType, MethodType
@@ -11,11 +11,11 @@ class Route:
static_subroutes: dict[str, Route]
handler: dict[MethodType, HandlerType]
def __init__(self):
def __init__(self) -> None:
self.static_subroutes = {}
self.handler = {}
def add(self, method: MethodType, sequence: Sequence[str], handler: HandlerType):
def add(self, method: MethodType, sequence: Sequence[str], handler: HandlerType) -> None:
if len(sequence) == 0:
self.handler[method] = handler
return
@@ -42,10 +42,10 @@ class Route:
class Router:
_root: Route
def __init__(self):
def __init__(self) -> None:
self._root = Route()
def add(self, method: MethodType, path_pattern: str, handler: HandlerType):
def add(self, method: MethodType, path_pattern: str, handler: HandlerType) -> None:
assert method.upper() == method
segments = path_pattern.split('/')
@@ -65,6 +65,8 @@ class Router:
raise NotFoundException(path or '/') from e
except MethodNotAllowedException as e:
raise MethodNotAllowedException(
f'{method} /{path}' + ', allowed: ' + (e.message if len(e.message) > 0 else 'none')) \
f'{method} /{path}' + ', allowed: ' + (e.message or '' if len(e.message or '') > 0 else 'none')) \
from e
if h is None:
raise NotFoundException(path or '/')
return h

View File

@@ -1,19 +1,13 @@
from __future__ import annotations
import collections
from dataclasses import dataclass
from typing import Callable, Awaitable, Dict, Literal, Any, TypeVar
from typing import Callable, Awaitable, Literal, Any
import urllib.parse
from case_insensitive_dict.case_insensitive_dict import CaseInsensitiveDict
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from turbosloth.interfaces.serialized import SerializedResponse, SerializedRequest
type Scope = Dict
type Receive = Callable[[], Awaitable[Dict]]
type Send = Callable[[Dict], Awaitable[None]]
type Scope = dict[str, Any]
type ASGIMessage = dict[str, Any]
type Receive = Callable[[], Awaitable[ASGIMessage]]
type Send = Callable[[ASGIMessage], Awaitable[None]]
type HandlerType = Callable[[SerializedRequest], Awaitable[SerializedResponse]]
type MethodType = (

View File

@@ -1,6 +1,6 @@
from email.policy import EmailPolicy
def parse_content_type(content_type: str) -> tuple[str, dict]:
def parse_content_type(content_type: str) -> tuple[str, dict[str, str]]:
header = EmailPolicy.header_factory('content-type', content_type)
return header.content_type, dict(header.params)

View File

@@ -1,7 +1,7 @@
import pytest
from src.turbosloth.router import Router
from src.turbosloth.exceptions import NotFoundException, MethodNotAllowedException
from src.turbosloth.router import Router
def test_router_root_handler():

59
uv.lock generated
View File

@@ -145,6 +145,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
]
[[package]]
name = "lxml-stubs"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
@@ -204,6 +213,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" },
]
[[package]]
name = "mypy"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" },
{ url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" },
{ url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" },
{ url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" },
{ url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" },
{ url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "orjson"
version = "3.11.0"
@@ -310,16 +348,19 @@ dependencies = [
{ name = "breakshaft" },
{ name = "case-insensitive-dictionary" },
{ name = "megasniff" },
{ name = "mypy" },
]
[package.dev-dependencies]
all-containers = [
{ name = "lxml" },
{ name = "lxml-stubs" },
{ name = "msgpack" },
{ name = "orjson" },
]
dev = [
{ name = "lxml" },
{ name = "lxml-stubs" },
{ name = "msgpack" },
{ name = "orjson" },
{ name = "pytest" },
@@ -334,6 +375,7 @@ msgpack = [
]
xml = [
{ name = "lxml" },
{ name = "lxml-stubs" },
]
[package.metadata]
@@ -341,16 +383,19 @@ requires-dist = [
{ name = "breakshaft", specifier = ">=0.1.0", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
{ name = "case-insensitive-dictionary", specifier = ">=0.2.1" },
{ name = "megasniff", specifier = ">=0.2.0", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
{ name = "mypy", specifier = ">=1.17.0" },
]
[package.metadata.requires-dev]
all-containers = [
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "lxml-stubs", specifier = ">=0.5.1" },
{ name = "msgpack", specifier = ">=1.1.1" },
{ name = "orjson", specifier = ">=3.11.0" },
]
dev = [
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "lxml-stubs", specifier = ">=0.5.1" },
{ name = "msgpack", specifier = ">=1.1.1" },
{ name = "orjson", specifier = ">=3.11.0" },
{ name = "pytest", specifier = ">=8.4.1" },
@@ -359,7 +404,19 @@ dev = [
]
json = [{ name = "orjson", specifier = ">=3.11.0" }]
msgpack = [{ name = "msgpack", specifier = ">=1.1.1" }]
xml = [{ name = "lxml", specifier = ">=6.0.0" }]
xml = [
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "lxml-stubs", specifier = ">=0.5.1" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "uvicorn"