diff --git a/pyproject.toml b/pyproject.toml index bb69bc3..78e0bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,5 +34,15 @@ dev = [ "pytest>=8.4.1", "pytest-cov>=6.2.1", "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" }, +] \ No newline at end of file diff --git a/src/turbosloth/__main__.py b/src/turbosloth/__main__.py index 14c21d8..2c5840c 100644 --- a/src/turbosloth/__main__.py +++ b/src/turbosloth/__main__.py @@ -2,17 +2,21 @@ from __future__ import annotations 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() @app.get("/") -async def index(req: BasicRequest) -> BasicResponse: - return BasicResponse(200, {}, b'Hello, ASGI Router!') +async def index(req: SerializedRequest) -> SerializedResponse: + return SerializedResponse(200, {}, 'Hello, ASGI Router!') @app.get("/user/") -async def get_user(req: BasicRequest) -> BasicResponse: - print(req.query) - return BasicResponse(200, {}, f'Hello, User {req.query["id"]}!'.encode()) +@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 + return SerializedResponse(200, {}, resp) diff --git a/src/turbosloth/app.py b/src/turbosloth/app.py index 82956ab..916f1b4 100644 --- a/src/turbosloth/app.py +++ b/src/turbosloth/app.py @@ -2,6 +2,8 @@ import json from typing import Optional, Callable, Awaitable, Protocol from .exceptions import HTTPException +from .interfaces.serialize_selector import SerializeSelector +from .interfaces.serialized import SerializedResponse, TextSerializedResponse from .router import Router from .types import Scope, Receive, Send, MethodType, HandlerType, BasicRequest, BasicResponse @@ -14,6 +16,8 @@ class ASGIApp(Protocol): class HTTPApp(ASGIApp): + serialize_selector: SerializeSelector + async def _do_http(self, scope: Scope, receive: Receive, send: Send): method = scope['method'] path = scope['path'] @@ -27,12 +31,25 @@ class HTTPApp(ASGIApp): scope['body'] = body req = BasicRequest.from_scope(scope) + sresponser = TextSerializedResponse + charset = 'latin1' try: 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: - 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({ @@ -115,15 +132,16 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp): self.router = Router() self._on_startup = on_startup self._on_shutdown = on_shutdown + self.serialize_selector = SerializeSelector() async def __call__(self, scope: Scope, receive: Receive, send: Send): t = scope['type'] if t == 'http': - await self._do_http(scope, receive, send) + return await self._do_http(scope, receive, send) elif t == 'lifespan': - await self._do_lifespan(receive, send) + return await self._do_lifespan(receive, send) 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}') diff --git a/src/turbosloth/interfaces/base.py b/src/turbosloth/interfaces/base.py new file mode 100644 index 0000000..dd4c8b5 --- /dev/null +++ b/src/turbosloth/interfaces/base.py @@ -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 + } diff --git a/src/turbosloth/interfaces/serialize_selector.py b/src/turbosloth/interfaces/serialize_selector.py new file mode 100644 index 0000000..5268416 --- /dev/null +++ b/src/turbosloth/interfaces/serialize_selector.py @@ -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) diff --git a/src/turbosloth/interfaces/serialized/__init__.py b/src/turbosloth/interfaces/serialized/__init__.py new file mode 100644 index 0000000..083cd3a --- /dev/null +++ b/src/turbosloth/interfaces/serialized/__init__.py @@ -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 diff --git a/src/turbosloth/interfaces/serialized/base.py b/src/turbosloth/interfaces/serialized/base.py new file mode 100644 index 0000000..a7ac00f --- /dev/null +++ b/src/turbosloth/interfaces/serialized/base.py @@ -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() diff --git a/src/turbosloth/interfaces/serialized/json.py b/src/turbosloth/interfaces/serialized/json.py new file mode 100644 index 0000000..2b7c002 --- /dev/null +++ b/src/turbosloth/interfaces/serialized/json.py @@ -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) diff --git a/src/turbosloth/interfaces/serialized/msgpack.py b/src/turbosloth/interfaces/serialized/msgpack.py new file mode 100644 index 0000000..00fe86b --- /dev/null +++ b/src/turbosloth/interfaces/serialized/msgpack.py @@ -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) diff --git a/src/turbosloth/interfaces/serialized/text.py b/src/turbosloth/interfaces/serialized/text.py new file mode 100644 index 0000000..03f495b --- /dev/null +++ b/src/turbosloth/interfaces/serialized/text.py @@ -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)) diff --git a/src/turbosloth/interfaces/serialized/xml.py b/src/turbosloth/interfaces/serialized/xml.py new file mode 100644 index 0000000..f2358a9 --- /dev/null +++ b/src/turbosloth/interfaces/serialized/xml.py @@ -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") diff --git a/src/turbosloth/types.py b/src/turbosloth/types.py index cc014fb..fec375e 100644 --- a/src/turbosloth/types.py +++ b/src/turbosloth/types.py @@ -8,10 +8,13 @@ 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 HandlerType = Callable[[BasicRequest], Awaitable[BasicResponse]] +type HandlerType = Callable[[SerializedRequest], Awaitable[SerializedResponse]] type MethodType = ( Literal['GET'] | @@ -24,54 +27,3 @@ type MethodType = ( Literal['CONNECT'] | Literal['OPTIONS'] | 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 - } diff --git a/src/turbosloth/util.py b/src/turbosloth/util.py new file mode 100644 index 0000000..81a0630 --- /dev/null +++ b/src/turbosloth/util.py @@ -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) diff --git a/uv.lock b/uv.lock index 6097818..a4834af 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] +[[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]] name = "markupsafe" 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" }, ] +[[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]] name = "packaging" version = "25.0" @@ -248,11 +313,28 @@ dependencies = [ ] [package.dev-dependencies] +all-containers = [ + { name = "lxml" }, + { name = "msgpack" }, + { name = "orjson" }, +] dev = [ + { name = "lxml" }, + { name = "msgpack" }, + { name = "orjson" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "uvicorn" }, ] +json = [ + { name = "orjson" }, +] +msgpack = [ + { name = "msgpack" }, +] +xml = [ + { name = "lxml" }, +] [package.metadata] requires-dist = [ @@ -262,11 +344,22 @@ requires-dist = [ ] [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 = [ + { 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-cov", specifier = ">=6.2.1" }, { 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]] name = "uvicorn"