Create request serializer selection based on content-type header
This commit is contained in:
@@ -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" },
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
71
src/turbosloth/interfaces/base.py
Normal file
71
src/turbosloth/interfaces/base.py
Normal 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
|
||||
}
|
||||
60
src/turbosloth/interfaces/serialize_selector.py
Normal file
60
src/turbosloth/interfaces/serialize_selector.py
Normal 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)
|
||||
30
src/turbosloth/interfaces/serialized/__init__.py
Normal file
30
src/turbosloth/interfaces/serialized/__init__.py
Normal 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
|
||||
25
src/turbosloth/interfaces/serialized/base.py
Normal file
25
src/turbosloth/interfaces/serialized/base.py
Normal 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()
|
||||
27
src/turbosloth/interfaces/serialized/json.py
Normal file
27
src/turbosloth/interfaces/serialized/json.py
Normal 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)
|
||||
23
src/turbosloth/interfaces/serialized/msgpack.py
Normal file
23
src/turbosloth/interfaces/serialized/msgpack.py
Normal 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)
|
||||
18
src/turbosloth/interfaces/serialized/text.py
Normal file
18
src/turbosloth/interfaces/serialized/text.py
Normal 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))
|
||||
63
src/turbosloth/interfaces/serialized/xml.py
Normal file
63
src/turbosloth/interfaces/serialized/xml.py
Normal 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")
|
||||
@@ -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
|
||||
}
|
||||
|
||||
6
src/turbosloth/util.py
Normal file
6
src/turbosloth/util.py
Normal 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
93
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user