Create request serializer selection based on content-type header

This commit is contained in:
2025-07-16 16:40:34 +03:00
parent 14f82885c4
commit ee25d17683
14 changed files with 462 additions and 62 deletions

View File

@@ -34,5 +34,15 @@ dev = [
"pytest>=8.4.1", "pytest>=8.4.1",
"pytest-cov>=6.2.1", "pytest-cov>=6.2.1",
"uvicorn>=0.35.0", "uvicorn>=0.35.0",
{ "include-group" = "all_containers" },
] ]
xml = ["lxml>=6.0.0", ]
msgpack = ["msgpack>=1.1.1", ]
json = ["orjson>=3.11.0", ]
all_containers = [
{ "include-group" = "xml" },
{ "include-group" = "msgpack" },
{ "include-group" = "json" },
]

View File

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

View File

@@ -2,6 +2,8 @@ import json
from typing import Optional, Callable, Awaitable, Protocol from typing import Optional, Callable, Awaitable, Protocol
from .exceptions import HTTPException from .exceptions import HTTPException
from .interfaces.serialize_selector import SerializeSelector
from .interfaces.serialized import SerializedResponse, TextSerializedResponse
from .router import Router from .router import Router
from .types import Scope, Receive, Send, MethodType, HandlerType, BasicRequest, BasicResponse from .types import Scope, Receive, Send, MethodType, HandlerType, BasicRequest, BasicResponse
@@ -14,6 +16,8 @@ class ASGIApp(Protocol):
class HTTPApp(ASGIApp): 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):
method = scope['method'] method = scope['method']
path = scope['path'] path = scope['path']
@@ -27,12 +31,25 @@ class HTTPApp(ASGIApp):
scope['body'] = body scope['body'] = body
req = BasicRequest.from_scope(scope) req = BasicRequest.from_scope(scope)
sresponser = TextSerializedResponse
charset = 'latin1'
try: try:
handler = self.router.match(method, path) handler = self.router.match(method, path)
resp = await handler(req)
ser = self.serialize_selector.select(req.headers)
sresponser = ser.resp
charset = ser.charset
sreq = ser.req.deserialize(req, charset)
sresp = await handler(sreq)
except HTTPException as e: except HTTPException as e:
resp = BasicResponse(e.code, {'content-type': 'text/plain'}, str(e).encode()) sresp = SerializedResponse(e.code, {}, str(e))
try:
resp = sresponser.into_basic(sresp, charset)
except UnicodeEncodeError:
resp = sresponser.into_basic(sresp, 'utf-8')
await send(resp.into_start_message()) await send(resp.into_start_message())
await send({ await send({
@@ -115,15 +132,16 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp):
self.router = Router() self.router = Router()
self._on_startup = on_startup self._on_startup = on_startup
self._on_shutdown = on_shutdown self._on_shutdown = on_shutdown
self.serialize_selector = SerializeSelector()
async def __call__(self, scope: Scope, receive: Receive, send: Send): async def __call__(self, scope: Scope, receive: Receive, send: Send):
t = scope['type'] t = scope['type']
if t == 'http': if t == 'http':
await self._do_http(scope, receive, send) return await self._do_http(scope, receive, send)
elif t == 'lifespan': elif t == 'lifespan':
await self._do_lifespan(receive, send) return await self._do_lifespan(receive, send)
elif t == 'websocket': elif t == 'websocket':
await self._do_websocket(scope, receive, send) return await self._do_websocket(scope, receive, send)
raise RuntimeError(f'Unsupported scope type: {t}') raise RuntimeError(f'Unsupported scope type: {t}')

View File

@@ -0,0 +1,71 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
from case_insensitive_dict import CaseInsensitiveDict
import urllib.parse
@dataclass
class BasicRequest:
method: str
path: str
headers: CaseInsensitiveDict[str, str]
query: dict[str, list[str] | str]
body: bytes
def __init__(self,
method: str,
path: str,
headers: Mapping[str, str],
query: dict[str, list[str] | str],
body: bytes):
self.method = method
self.path = path
self.headers = CaseInsensitiveDict(headers)
self.query = query
self.body = body
@classmethod
def from_scope(cls, scope: dict) -> BasicRequest:
path = scope['path']
method = scope['method']
headers = {}
for key, value in scope.get('headers', []):
headers[key.decode('latin1')] = value.decode('latin1')
qs = scope['query_string'].decode('latin1')
print(qs)
query_raw = urllib.parse.parse_qs(qs)
query = {}
for k, v in query_raw.items():
if len(v) == 1:
v = v[0]
query[k] = v
body = scope['body']
return BasicRequest(method, path, headers, query, body)
@dataclass
class BasicResponse:
code: int
headers: CaseInsensitiveDict[str, str]
body: bytes
def __init__(self, code: int, headers: Mapping[str, str], body: bytes):
self.code = code
self.headers = CaseInsensitiveDict(headers)
self.body = body
def into_start_message(self) -> dict:
enc_headers = []
for k, v in self.headers.items():
enc_headers.append((k.encode('latin1'), v.encode('latin1')))
return {
'type': 'http.response.start',
'status': self.code,
'headers': enc_headers
}

View File

@@ -0,0 +1,60 @@
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
class SerializeChoise(NamedTuple):
req: type[SerializedRequest]
resp: type[SerializedResponse]
charset: str
content_properties: dict
class SerializeSelector:
default_content_type: str
serializers: dict[str, tuple[type[SerializedRequest], type[SerializedResponse]]]
def __init__(self,
default_content_type: Optional[str] = 'text/plain',
filter_content_types: Optional[list[str]] = None):
self.default_content_type = default_content_type
ser = {}
if filter_content_types is None:
filter_content_types = default_serializers.keys()
for k, v in default_serializers.items():
if k in filter_content_types:
ser[k] = v
self.serializers = ser
def select(self, headers: CaseInsensitiveDict[str, str]) -> SerializeChoise:
contenttype_header = headers.get('Content-Type')
if contenttype_header is None:
contenttype = self.default_content_type
properties = {}
else:
contenttype, properties = parse_content_type(contenttype_header)
charset = properties.get('charset')
if charset is None:
if contenttype == 'application/json':
charset = 'utf-8'
else:
charset = 'latin1'
choise = self.serializers.get(contenttype)
if choise is None:
choise = self.serializers.get(self.default_content_type)
if choise is None:
raise NotAcceptableException('acceptable content types: ' + ', '.join(self.serializers.keys()))
req, resp = choise
return SerializeChoise(req, resp, charset, properties)

View File

@@ -0,0 +1,30 @@
from .base import SerializedRequest, SerializedResponse
default_serializers = {}
try:
from .text import TextSerializedRequest, TextSerializedResponse
default_serializers['text/plain'] = (TextSerializedRequest, TextSerializedResponse)
except:
pass
try:
from .json import JsonSerializedRequest, JsonSerializedResponse
default_serializers['application/json'] = (JsonSerializedRequest, JsonSerializedResponse)
except:
pass
try:
from .xml import XMLSerializedRequest, XMLSerializedResponse
default_serializers['application/xml'] = (XMLSerializedRequest, XMLSerializedResponse)
except:
pass
try:
from .msgpack import MessagePackSerializedRequest, MessagePackSerializedResponse
default_serializers['application/vnd.msgpack'] = (MessagePackSerializedRequest, MessagePackSerializedResponse)
except:
pass

View File

@@ -0,0 +1,25 @@
from dataclasses import dataclass
from typing import Any, Mapping
from turbosloth.interfaces.base import BasicRequest, BasicResponse
@dataclass
class SerializedRequest:
body: dict[str, Any] | list | str | None
basic: BasicRequest
charset: str
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
raise NotImplementedError()
@dataclass
class SerializedResponse:
code: int
headers: Mapping[str, str]
body: dict[str, Any] | list | str
def into_basic(self, charset: str) -> BasicResponse:
raise NotImplementedError()

View File

@@ -0,0 +1,27 @@
import orjson
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from .base import SerializedRequest, SerializedResponse
class JsonSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
if len(basic.body) == 0:
b = None
elif charset.lower() in {'utf-8', 'utf8'}:
b = orjson.loads(basic.body)
else:
btxt = basic.body.decode(charset)
b = orjson.loads(btxt.encode('utf-8'))
return cls(b, basic, charset)
class JsonSerializedResponse(SerializedResponse):
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = 'application/json; charset=utf-8'
b = orjson.dumps(self.body)
return BasicResponse(self.code, headers, b)

View File

@@ -0,0 +1,23 @@
import msgpack
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from .base import SerializedRequest, SerializedResponse
class MessagePackSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
if len(basic.body) == 0:
b = None
else:
b = msgpack.unpackb(basic.body)
return cls(b, basic, charset)
class MessagePackSerializedResponse(SerializedResponse):
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = 'application/vnd.msgpack'
b = msgpack.packb(self.body)
return BasicResponse(self.code, headers, b)

View File

@@ -0,0 +1,18 @@
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from .base import SerializedRequest, SerializedResponse
class TextSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str):
b = basic.body.decode(charset)
return cls(b, basic, charset)
class TextSerializedResponse(SerializedResponse):
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = 'text/plain; charset=' + charset
return BasicResponse(self.code, headers, str(self.body).encode(charset))

View File

@@ -0,0 +1,63 @@
import json
from typing import Any
from case_insensitive_dict import CaseInsensitiveDict
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):
if len(basic.body) == 0:
b = {}
else:
btxt = basic.body.decode(charset)
parsed = etree.fromstring(btxt)
b = {child.tag: child.text for child in parsed}
return cls(b, basic, charset)
class XMLSerializedResponse(SerializedResponse):
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = 'application/xml; charset=' + charset
btxt = _into_xml(self.body)
return BasicResponse(self.code, headers, btxt.encode(charset))
def _into_xml(data: dict[str, Any] | list[Any] | str) -> str:
def _build_element(parent: etree.Element, key: str, value: Any) -> None:
element = etree.SubElement(parent, key)
if isinstance(value, dict):
for k, v in value.items():
_build_element(element, k, v)
elif isinstance(value, list):
for item in value:
_build_element(element, key, item)
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

@@ -8,10 +8,13 @@ import urllib.parse
from case_insensitive_dict.case_insensitive_dict import CaseInsensitiveDict 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 Scope = Dict
type Receive = Callable[[], Awaitable[Dict]] type Receive = Callable[[], Awaitable[Dict]]
type Send = Callable[[Dict], Awaitable[None]] type Send = Callable[[Dict], Awaitable[None]]
type HandlerType = Callable[[BasicRequest], Awaitable[BasicResponse]] type HandlerType = Callable[[SerializedRequest], Awaitable[SerializedResponse]]
type MethodType = ( type MethodType = (
Literal['GET'] | Literal['GET'] |
@@ -24,54 +27,3 @@ type MethodType = (
Literal['CONNECT'] | Literal['CONNECT'] |
Literal['OPTIONS'] | Literal['OPTIONS'] |
Literal['TRACE']) Literal['TRACE'])
K = TypeVar('K')
V = TypeVar('V')
@dataclass
class BasicRequest:
method: str
path: str
headers: CaseInsensitiveDict[str, str]
query: dict[str, list[str] | str]
body: dict[str, Any]
@classmethod
def from_scope(cls, scope: Scope) -> BasicRequest:
path = scope['path']
method = scope['method']
headers = CaseInsensitiveDict()
for key, value in scope.get('headers', []):
headers[key.decode('latin1')] = value.decode('latin1')
qs = scope['query_string'].decode('latin1')
print(qs)
query_raw = urllib.parse.parse_qs(qs)
query = {}
for k, v in query_raw.items():
if len(v) == 1:
v = v[0]
query[k] = v
body = scope['body']
return BasicRequest(method, path, headers, query, body)
@dataclass
class BasicResponse:
code: int
headers: CaseInsensitiveDict[str, str]
body: bytes
def into_start_message(self) -> dict:
enc_headers = []
for k, v in self.headers.items():
enc_headers.append((k.encode('latin1'), v.encode('latin1')))
return {
'type': 'http.response.start',
'status': self.code,
'headers': enc_headers
}

6
src/turbosloth/util.py Normal file
View File

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

93
uv.lock generated
View File

@@ -121,6 +121,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
] ]
[[package]]
name = "lxml"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
{ url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" },
{ url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" },
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
{ url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" },
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
{ 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]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.2" version = "3.0.2"
@@ -162,6 +186,47 @@ wheels = [
{ url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/megasniff/0.2.0/megasniff-0.2.0-py3-none-any.whl", hash = "sha256:4f03a1e81dcb1020b6fe80f3fbc7b192a61b4dc55f7c6783141aa464dc3c77d1" }, { url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/megasniff/0.2.0/megasniff-0.2.0-py3-none-any.whl", hash = "sha256:4f03a1e81dcb1020b6fe80f3fbc7b192a61b4dc55f7c6783141aa464dc3c77d1" },
] ]
[[package]]
name = "msgpack"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" },
{ url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" },
{ url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" },
{ url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" },
{ url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" },
{ url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" },
{ url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" },
{ url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" },
{ url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" },
{ 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 = "orjson"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/87/03ababa86d984952304ac8ce9fbd3a317afb4a225b9a81f9b606ac60c873/orjson-3.11.0.tar.gz", hash = "sha256:2e4c129da624f291bcc607016a99e7f04a353f6874f3bd8d9b47b88597d5f700", size = 5318246, upload-time = "2025-07-15T16:08:29.194Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/63/82d9b6b48624009d230bc6038e54778af8f84dfd54402f9504f477c5cfd5/orjson-3.11.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a8ba9698655e16746fdf5266939427da0f9553305152aeb1a1cc14974a19cfb", size = 240125, upload-time = "2025-07-15T16:07:35.976Z" },
{ url = "https://files.pythonhosted.org/packages/16/3a/d557ed87c63237d4c97a7bac7ac054c347ab8c4b6da09748d162ca287175/orjson-3.11.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:67133847f9a35a5ef5acfa3325d4a2f7fe05c11f1505c4117bb086fc06f2a58f", size = 129189, upload-time = "2025-07-15T16:07:37.486Z" },
{ url = "https://files.pythonhosted.org/packages/69/5e/b2c9e22e2cd10aa7d76a629cee65d661e06a61fbaf4dc226386f5636dd44/orjson-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f797d57814975b78f5f5423acb003db6f9be5186b72d48bd97a1000e89d331d", size = 131953, upload-time = "2025-07-15T16:07:39.254Z" },
{ url = "https://files.pythonhosted.org/packages/e2/60/760fcd9b50eb44d1206f2b30c8d310b79714553b9d94a02f9ea3252ebe63/orjson-3.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28acd19822987c5163b9e03a6e60853a52acfee384af2b394d11cb413b889246", size = 126922, upload-time = "2025-07-15T16:07:41.282Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/8c46daa867ccc92da6de9567608be62052774b924a77c78382e30d50b579/orjson-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8d38d9e1e2cf9729658e35956cf01e13e89148beb4cb9e794c9c10c5cb252f8", size = 128787, upload-time = "2025-07-15T16:07:42.681Z" },
{ url = "https://files.pythonhosted.org/packages/f2/14/a2f1b123d85f11a19e8749f7d3f9ed6c9b331c61f7b47cfd3e9a1fedb9bc/orjson-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f094edd2b782650b0761fd78858d9254de1c1286f5af43145b3d08cdacfd51", size = 131895, upload-time = "2025-07-15T16:07:44.519Z" },
{ url = "https://files.pythonhosted.org/packages/c8/10/362e8192df7528e8086ea712c5cb01355c8d4e52c59a804417ba01e2eb2d/orjson-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d09176a4a9e04a5394a4a0edd758f645d53d903b306d02f2691b97d5c736a9e", size = 133868, upload-time = "2025-07-15T16:07:46.227Z" },
{ url = "https://files.pythonhosted.org/packages/f8/4e/ef43582ef3e3dfd2a39bc3106fa543364fde1ba58489841120219da6e22f/orjson-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a585042104e90a61eda2564d11317b6a304eb4e71cd33e839f5af6be56c34d3", size = 128234, upload-time = "2025-07-15T16:07:48.123Z" },
{ url = "https://files.pythonhosted.org/packages/d7/fa/02dabb2f1d605bee8c4bb1160cfc7467976b1ed359a62cc92e0681b53c45/orjson-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2218629dbfdeeb5c9e0573d59f809d42f9d49ae6464d2f479e667aee14c3ef4", size = 130232, upload-time = "2025-07-15T16:07:50.197Z" },
{ url = "https://files.pythonhosted.org/packages/16/76/951b5619605c8d2ede80cc989f32a66abc954530d86e84030db2250c63a1/orjson-3.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:613e54a2b10b51b656305c11235a9c4a5c5491ef5c283f86483d4e9e123ed5e4", size = 403648, upload-time = "2025-07-15T16:07:52.136Z" },
{ url = "https://files.pythonhosted.org/packages/96/e2/5fa53bb411455a63b3713db90b588e6ca5ed2db59ad49b3fb8a0e94e0dda/orjson-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9dac7fbf3b8b05965986c5cfae051eb9a30fced7f15f1d13a5adc608436eb486", size = 144572, upload-time = "2025-07-15T16:07:54.004Z" },
{ url = "https://files.pythonhosted.org/packages/ad/d0/7d6f91e1e0f034258c3a3358f20b0c9490070e8a7ab8880085547274c7f9/orjson-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b64b254414e2be55ac5257124b5602c5f0b4d06b80bd27d1165efe8f36e836", size = 132766, upload-time = "2025-07-15T16:07:55.936Z" },
{ url = "https://files.pythonhosted.org/packages/ff/f8/4d46481f1b3fb40dc826d62179f96c808eb470cdcc74b6593fb114d74af3/orjson-3.11.0-cp313-cp313-win32.whl", hash = "sha256:359cbe11bc940c64cb3848cf22000d2aef36aff7bfd09ca2c0b9cb309c387132", size = 134638, upload-time = "2025-07-15T16:07:57.343Z" },
{ url = "https://files.pythonhosted.org/packages/85/3f/544938dcfb7337d85ee1e43d7685cf8f3bfd452e0b15a32fe70cb4ca5094/orjson-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:0759b36428067dc777b202dd286fbdd33d7f261c6455c4238ea4e8474358b1e6", size = 129411, upload-time = "2025-07-15T16:07:58.852Z" },
{ url = "https://files.pythonhosted.org/packages/43/0c/f75015669d7817d222df1bb207f402277b77d22c4833950c8c8c7cf2d325/orjson-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:51cdca2f36e923126d0734efaf72ddbb5d6da01dbd20eab898bdc50de80d7b5a", size = 126349, upload-time = "2025-07-15T16:08:00.322Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@@ -248,11 +313,28 @@ dependencies = [
] ]
[package.dev-dependencies] [package.dev-dependencies]
all-containers = [
{ name = "lxml" },
{ name = "msgpack" },
{ name = "orjson" },
]
dev = [ dev = [
{ name = "lxml" },
{ name = "msgpack" },
{ name = "orjson" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
json = [
{ name = "orjson" },
]
msgpack = [
{ name = "msgpack" },
]
xml = [
{ name = "lxml" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
@@ -262,11 +344,22 @@ requires-dist = [
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
all-containers = [
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "msgpack", specifier = ">=1.1.1" },
{ name = "orjson", specifier = ">=3.11.0" },
]
dev = [ dev = [
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "msgpack", specifier = ">=1.1.1" },
{ name = "orjson", specifier = ">=3.11.0" },
{ name = "pytest", specifier = ">=8.4.1" }, { name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-cov", specifier = ">=6.2.1" },
{ name = "uvicorn", specifier = ">=0.35.0" }, { name = "uvicorn", specifier = ">=0.35.0" },
] ]
json = [{ name = "orjson", specifier = ">=3.11.0" }]
msgpack = [{ name = "msgpack", specifier = ">=1.1.1" }]
xml = [{ name = "lxml", specifier = ">=6.0.0" }]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"