Compare commits

...

8 Commits

20 changed files with 1913 additions and 334 deletions

View File

@@ -8,11 +8,12 @@ authors = [
license = "LGPL-3.0-or-later"
requires-python = ">=3.13"
dependencies = [
"megasniff>=0.2.3",
"megasniff>=0.2.4",
"breakshaft>=0.1.6",
"case-insensitive-dictionary>=0.2.1",
"mypy>=1.17.0",
"jinja2>=3.1.6",
"python-multipart>=0.0.20",
]
[tool.uv.sources]

View File

@@ -1,18 +1,28 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Optional
from typing import Optional, Literal, Annotated
import uvicorn
from python_multipart.multipart import File, Field
from turbosloth import SlothApp
from turbosloth.doc.openapi_app import OpenAPIApp
from turbosloth.doc.openapi_models import Info
from turbosloth.interfaces.base import BasicResponse, BasicRequest
from turbosloth.interfaces.serialize_selector import SerializeSelector
from turbosloth.interfaces.serialized import SerializedResponse, SerializedRequest
from turbosloth.internal_types import QTYPE, BTYPE, PTYPE, HTYPE
from turbosloth.req_schema import UnwrappedRequest
from turbosloth.schema import RequestBody, HeaderParam
from turbosloth.interfaces.serialized import SerializedResponse
from turbosloth.interfaces.serialized.multipart_form_data import MultipartFormSerializedRequest
from turbosloth.schema import RequestBody, HeaderParam, QueryParam, Resp
app = SlothApp(di_autodoc_prefix='/didoc', serialize_selector=SerializeSelector(default_content_type='application/json'))
# from turbosloth.types import HTTPResponse
app = SlothApp(di_autodoc_prefix='/didoc',
serialize_selector=SerializeSelector(
default_content_type='application/json',
default_accept_type='application/json'
),
openapi_app=OpenAPIApp(Info('asdf', '1.0.0')))
# @app.get("/")
@@ -75,19 +85,201 @@ class PTYPESchema:
# return SerializedResponse(200, {}, resp)
class DummyDbConnection:
constructions = [0]
def __init__(self):
self.constructions[0] += 1
@app.mark_injector()
def create_db_connection() -> DummyDbConnection:
return DummyDbConnection()
@dataclass
class User:
user_id: int
name: str
@app.mark_injector()
def auth_user(user: RequestBody(UserPostSchema)) -> User:
return User(user.user_id, f'user {user.user_id}')
@dataclass
class TestResp:
req: UserPostSchema
q1: str
h1: str
a: str
db: list[int]
user: User
q2: int
@dataclass
class TestResp1(TestResp):
hehe: str = 'haha'
@dataclass
class HeadersResp:
head1: str
@dataclass
class ErrResp:
err_info: str
@app.post("/test/body/{a}")
async def test_body(r: RequestBody(UserPostSchema),
q1: str,
a: str,
h1: HeaderParam(str, 'header1')) -> SerializedResponse:
h1: HeaderParam(str, 'header1'),
db: DummyDbConnection,
user: User,
q2: int = 321) -> (Resp((TestResp, HeadersResp), 200)
| Resp(TestResp1, 201)
| Resp(ErrResp, 500)):
print(r.user_id)
resp = {
'req': r,
'q1': q1,
'h1': h1,
'a': a
'a': a,
'db': db.constructions,
'user': user,
'q2': q2
}
return SerializedResponse(200, {}, resp)
if a == '123':
return ErrResp('hehe'), {}
if q1 == 'a':
return TestResp1(**resp)
return TestResp(**resp), HeadersResp('asdf')
@dataclass
class FieldData:
type_: Annotated[str, 'type']
name: str
value: str
@dataclass
class FileData:
type_: Annotated[str, 'type']
name: str
fname: str
@dataclass
class FileRespSchema:
fields: list[FileData | FieldData]
@app.post('/upload_multipart')
async def upload_multipart(req: MultipartFormSerializedRequest) -> Resp(FileRespSchema, 200):
fields = []
fields_raw = []
async for e in req:
ee = {}
if isinstance(e, File):
f = FileData('file', e.field_name.decode(), e.file_name.decode())
fields.append(f)
ee = f.__dict__
fields_raw.append(ee)
if e.in_memory:
# оно в любом случае будет сохраняться на диск если слишком большое, что мне не очень нравится
# TODO: кастомизация поведения File и Field полей, потоковое чтение multipart/*
e.flush_to_disk()
print(e.actual_file_name)
elif isinstance(e, Field):
f = FieldData('field', e.field_name.decode(), e.value.decode())
fields.append(f)
ee = f.__dict__
fields_raw.append(ee)
else:
pass
print(e)
return FileRespSchema(fields)
@app.get('/openapi.json')
async def openapi_schema(a: SlothApp, version: QueryParam(Optional[str], 'v') = None) -> SerializedResponse:
dat = a.openapi_app.export_as_dict()
if version is not None:
dat['openapi'] = version
return SerializedResponse(dat)
@app.get('/stoplight')
async def stoplight() -> BasicResponse:
ret = '''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Elements in HTML</title>
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
<style>
.sl-elements {height: 100vh}
body {background-color: #1c1f26;}
/* :root :not(sl-code-highlight) */
:root {
/* Canvas */
--color-canvas-50: #0d1117;
--color-canvas-100: #1b2432;
--color-canvas-200: #2c3e55;
--color-canvas-300: #3e5a78;
--color-canvas: #1c1f26;
--color-canvas-tint:#252a34;
--color-canvas-pure:#0a0f16;
/* Text */
--color-text: #e9eef7;
--color-text-heading: #f5f7fb;
--color-text-paragraph: #d7deeb;
--color-text-muted: #9caec9;
--color-text-primary: #8cbff2;
--color-text-light: #cfd9ec;
}
.sl-overflow-y-auto {
color-scheme: dark; /* For scrollbar */
}
/* Синтаксис JSON в Examples */
pre code {
color: #e9eef7 !important; /* общий текст */
}
.token.property { color: #8cbff2 !important; } /* ключи (пастельный голубой) */
.token.string { color: #f2a6a6 !important; } /* строки (мягкий красно-розовый) */
.token.number { color: #a6d8f2 !important; } /* числа (светлый голубой) */
.token.boolean { color: #f7c97f !important; } /* true/false (пастельный жёлтый) */
.token.null { color: #cba6f7 !important; } /* null (сиреневый) */
.token.punctuation { color: #9caec9 !important; } /* скобки, запятые */
.token.operator { color: #9caec9 !important; }
</style>
</head>
<body>
<elements-api
apiDescriptionUrl="/openapi.json"
router="hash"
layout="responsive"
/>
</body>
</html>'''
return BasicResponse(200, {}, ret.encode())
if __name__ == '__main__':

View File

@@ -1,16 +1,21 @@
from __future__ import annotations
import html
import traceback
import typing
from dataclasses import dataclass
from types import NoneType
from typing import Optional, Callable, Awaitable, Protocol, get_type_hints, get_origin, get_args, Any, Annotated, Tuple, \
Iterable
Iterable, TypeAliasType, Union
import breakshaft.util_mermaid
import megasniff.exceptions
from breakshaft.models import ConversionPoint, Callgraph
from megasniff import SchemaInflatorGenerator
from breakshaft.util import is_basic_type_annot
from megasniff import SchemaInflatorGenerator, SchemaDeflatorGenerator
from .didoc import create_di_autodoc_handler
from .doc.openapi_app import OpenAPIApp
from .doc.openapi_models import RequestBody as OpenApiRequestBody, MediaType, Schema, Reference, Parameter, Response
from .exceptions import HTTPException
from .interfaces.base import BasicRequest, BasicResponse
from .interfaces.serialize_selector import SerializeSelector
@@ -18,18 +23,21 @@ from .interfaces.serialized import SerializedResponse, SerializedRequest
from .interfaces.serialized.text import TextSerializedResponse
from .req_schema import UnwrappedRequest
from .router import Router, Route
from .schema import EndpointConfig, ParamSchema
from .types import HandlerType, InternalHandlerType, ContentType, Accept
from .schema import EndpointConfig, ParamSchema, ReturnSchemaItem
from .types import HandlerType, InternalHandlerType, ContentType, Accept, HTTPReturnCode, \
HTTPResponseBodyPlaceholder, HTTPResponseHeadersPlaceholder, PreSerializedResponseBody, \
PreSerializedResponseHeader
from .internal_types import Scope, Receive, Send, MethodType, QTYPE, BTYPE, PTYPE, HTYPE
from breakshaft.convertor import ConvRepo
from .util import parse_content_type
import breakshaft.util
class ASGIApp(Protocol):
router: Router
def route(self, method: MethodType, path_pattern: str):
def route(self, method: MethodType, path_pattern: str, ok_return_code: str):
raise RuntimeError('stub!')
def add_subroute(self, subr: Route | Router, basepath: str) -> None:
@@ -63,11 +71,16 @@ class HTTPApp(ASGIApp):
contenttype_header = req.headers.get('Accept')
if contenttype_header is None:
return ct
ctypes = set(map(str.strip, contenttype_header.split(',')))
if (self.serialize_selector.default_content_type in ctypes
and self.serialize_selector.default_content_type is not None):
contenttype = self.serialize_selector.default_content_type
charset = None
else:
properties: dict[str, str]
contenttype, properties = parse_content_type(contenttype_header)
properties: dict[str, str]
contenttype, properties = parse_content_type(contenttype_header)
charset = properties.get('charset')
charset = properties.get('charset')
if charset is None:
# TODO: extract default charsets based on content type
@@ -78,18 +91,24 @@ class HTTPApp(ASGIApp):
return ContentType(contenttype, charset)
def serialize_request(self, ct: ContentType, req: BasicRequest) -> SerializedRequest:
ser = self.serialize_selector.select(ct.contenttype, ct.charset)
return ser.req.deserialize(req, ct.charset)
async def serialize_request(self, ct: ContentType, req: BasicRequest) -> SerializedRequest:
ser = self.serialize_selector.select_req(ct.contenttype)
return await ser.deserialize(req, ct.charset)
def serialize_response(self, req: BasicRequest, sresp: SerializedResponse, ct: Accept) -> BasicResponse:
ser = self.serialize_selector.select(ct.contenttype, ct.charset)
sresponser = ser.resp
if type(sresp) is SerializedResponse:
ser = self.serialize_selector.select_resp(ct.contenttype)
sresponser = ser
try:
return sresponser.into_basic(sresp, ct.charset)
except UnicodeEncodeError:
return sresponser.into_basic(sresp, 'utf-8')
try:
return sresponser.into_basic(sresp, ct.charset)
except UnicodeEncodeError:
return sresponser.into_basic(sresp, 'utf-8')
else:
try:
return sresp.into_basic(ct.charset)
except UnicodeEncodeError:
return sresp.into_basic('utf-8')
async def send_answer(self, send: Send, resp: BasicResponse):
await send(resp.into_start_message())
@@ -102,15 +121,15 @@ class HTTPApp(ASGIApp):
method = scope['method']
path = scope['path']
body = b''
while True:
event = await receive()
body += event.get('body', b'')
if not event.get('more_body', False):
break
scope['body'] = body
# body = b''
# while True:
# event = await receive()
# body += event.get('body', b'')
# if not event.get('more_body', False):
# break
# scope['body'] = body
req = BasicRequest.from_scope(scope)
req = BasicRequest.from_scope(scope, receive)
sresponser: type[SerializedResponse] = TextSerializedResponse
charset = 'latin1'
@@ -124,9 +143,9 @@ class HTTPApp(ASGIApp):
return
except (megasniff.exceptions.FieldValidationException, megasniff.exceptions.MissingFieldException) as e:
print(e)
sresp = SerializedResponse(400, {}, 'Schema error')
sresp = SerializedResponse('Schema error', code=400)
except HTTPException as e:
sresp = SerializedResponse(e.code, {}, str(e))
sresp = SerializedResponse(str(e), code=e.code)
try:
ct = self.extract_content_type(req)
@@ -174,45 +193,60 @@ class LifespanApp:
class MethodRoutersApp(ASGIApp):
def get(self, path_pattern: str):
return self.route('GET', path_pattern)
def get(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('GET', path_pattern, ok_return_code)
def post(self, path_pattern: str):
return self.route('POST', path_pattern)
def post(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('POST', path_pattern, ok_return_code)
def push(self, path_pattern: str):
return self.route('PUSH', path_pattern)
def put(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('PUT', path_pattern, ok_return_code)
def put(self, path_pattern: str):
return self.route('PUT', path_pattern)
def patch(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('PATCH', path_pattern, ok_return_code)
def patch(self, path_pattern: str):
return self.route('PATCH', path_pattern)
def delete(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('DELETE', path_pattern, ok_return_code)
def delete(self, path_pattern: str):
return self.route('DELETE', path_pattern)
def head(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('HEAD', path_pattern, ok_return_code)
def head(self, path_pattern: str):
return self.route('HEAD', path_pattern)
def connect(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('CONNECT', path_pattern, ok_return_code)
def connect(self, path_pattern: str):
return self.route('CONNECT', path_pattern)
def options(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('OPTIONS', path_pattern, ok_return_code)
def options(self, path_pattern: str):
return self.route('OPTIONS', path_pattern)
def trace(self, path_pattern: str, ok_return_code: str = '200'):
return self.route('TRACE', path_pattern, ok_return_code)
def trace(self, path_pattern: str):
return self.route('TRACE', path_pattern)
def _gen_body_header_retcode_extraction(handler_hash):
def _body_header_retcode_extraction(ret):
if isinstance(ret, tuple):
body, headers = ret
else:
body = ret
headers = {}
code_map = getattr(body, '__turbosloth_http_code_mapping__', {})
http_ret_code = code_map.get(handler_hash, 200)
return (body, headers), http_ret_code
return _body_header_retcode_extraction
class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp):
di_autodoc_prefix: Optional[str] = None
di_autodoc_prefix: Optional[str]
openapi_app: Optional[OpenAPIApp]
def __init__(self,
on_startup: Optional[Callable[[], Awaitable[None]]] = None,
on_shutdown: Optional[Callable[[], Awaitable[None]]] = None,
di_autodoc_prefix: Optional[str] = None,
serialize_selector: Optional[SerializeSelector] = None,
openapi_app: Optional[OpenAPIApp] = None
):
self.router = Router()
self._on_startup = on_startup
@@ -221,34 +255,51 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp):
serialize_selector = SerializeSelector()
self.serialize_selector = serialize_selector
self.infl_generator = SchemaInflatorGenerator(strict_mode=True, store_sources=True)
self.defl_generator = SchemaDeflatorGenerator(strict_mode=True, explicit_casts=False, store_sources=True)
self.di_autodoc_prefix = di_autodoc_prefix
self.openapi_app = openapi_app
if di_autodoc_prefix is not None:
self.inj_repo = ConvRepo(store_sources=True, store_callseq=True)
else:
self.inj_repo = ConvRepo()
self.inj_repo = ConvRepo(store_callseq=True)
@self.inj_repo.mark_injector()
def extract_query(req: BasicRequest | SerializedRequest) -> QTYPE:
return req.query
@self.inj_repo.mark_injector()
def extract_path_matches(req: BasicRequest | SerializedRequest) -> PTYPE:
return req.path_matches
@self.inj_repo.mark_injector()
def extract_headers(req: BasicRequest | SerializedRequest) -> HTYPE:
return req.headers
def unwrap_request(req: BasicRequest | SerializedRequest) -> tuple[QTYPE, PTYPE, HTYPE]:
return req.query, req.path_matches, req.headers
@self.inj_repo.mark_injector()
def extract_body(req: SerializedRequest) -> BTYPE:
return req.body
@self.inj_repo.mark_injector()
def provide_sloth_app() -> SlothApp:
return self
@self.inj_repo.mark_injector()
def construct_response(b: HTTPResponseBodyPlaceholder,
h: HTTPResponseHeadersPlaceholder,
c: HTTPReturnCode) -> SerializedResponse:
return SerializedResponse(b, int(c), h)
self.inj_repo.add_injector(self.extract_content_type)
self.inj_repo.add_injector(self.extract_accept_type)
self.inj_repo.add_injector(self.serialize_request)
self.inj_repo.add_injector(self.serialize_response)
added_req_serializers = set()
for ser in self.serialize_selector.req_serializers.values():
if ser in added_req_serializers:
continue
added_req_serializers.add(ser)
async def _serialize(ct: ContentType, req: BasicRequest):
return await ser.deserialize(req, ct.charset)
hints = get_type_hints(_serialize, include_extras=True)
hints['return'] = ser
self.inj_repo.add_injector(_serialize, type_remap=hints)
async def __call__(self, scope: Scope, receive: Receive, send: Send):
t = scope['type']
if t == 'http':
@@ -260,69 +311,195 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp):
raise RuntimeError(f'Unsupported scope type: {t}')
def route(self, method: MethodType, path_pattern: str):
def decorator(fn: HandlerType):
def _create_infl_from_schemas(self, schemas: Iterable[ParamSchema], from_type_override: type):
if len(schemas) > 0:
infl = self.infl_generator.schema_to_inflator(
list(schemas),
strict_mode_override=False,
)
type_remap = {
'from_data': from_type_override,
'return': tuple[tuple(t.replacement_type for t in schemas)]
}
return infl, set(ConversionPoint.from_fn(infl, type_remap=type_remap))
return None, None
path_substs = self.router.find_pattern_substs(path_pattern)
config = EndpointConfig.from_handler(fn, path_substs)
@staticmethod
def schema_from_type(tp) -> Schema | Reference:
if isinstance(tp, TypeAliasType):
return SlothApp.schema_from_type(tp.__value__)
origin = get_origin(tp)
args = get_args(tp)
fork_with = set()
if origin is Annotated:
return SlothApp.schema_from_type(args[0])
inflators_for_didoc = {}
if origin is None:
# базовый тип
if tp == str:
return Schema(type='string')
elif tp == int:
return Schema(type='integer')
elif tp == float:
return Schema(type='number', format='float')
elif tp == bool:
return Schema(type='boolean')
elif tp == type(None):
return Schema(type='null')
else:
# кастомный класс → $ref
return Reference(ref=f'#/components/schemas/{tp.__name__}')
def create_infl_from_schemas(schemas: Iterable[ParamSchema], from_type_override: type):
if len(schemas) > 0:
infl = self.infl_generator.schema_to_inflator(
list(schemas),
strict_mode_override=False,
)
type_remap = {
'from_data': from_type_override,
'return': Tuple[tuple(t.replacement_type for t in schemas)]
}
return infl, set(ConversionPoint.from_fn(infl, type_remap=type_remap))
return None, None
elif origin is list:
item_type = args[0] if args else Any
return Schema(type='array', items=SlothApp.schema_from_type(item_type))
i, infl = create_infl_from_schemas(config.path_schemas.values(), PTYPE)
if infl is not None:
fork_with |= infl
inflators_for_didoc['Path inflator'] = i
i, infl = create_infl_from_schemas(config.query_schemas.values(), QTYPE)
if infl is not None:
fork_with |= infl
inflators_for_didoc['Query inflator'] = i
i, infl = create_infl_from_schemas(config.header_schemas.values(), HTYPE)
if infl is not None:
fork_with |= infl
inflators_for_didoc['Header inflator'] = i
if config.body_schema is not None:
infl = self.infl_generator.schema_to_inflator(
config.body_schema.schema,
strict_mode_override=False,
)
type_remap = {
'from_data': BTYPE,
'return': config.body_schema.replacement_type
}
fork_with |= set(ConversionPoint.from_fn(infl, type_remap=type_remap))
inflators_for_didoc['Body inflator'] = infl
tmp_repo = self.inj_repo.fork(fork_with)
fn_type_hints = get_type_hints(fn)
for k, v in config.type_replacement.items():
fn_type_hints[k] = v
p = tmp_repo.create_pipeline(
(Send, BasicRequest),
[ConversionPoint.from_fn(fn, type_remap=fn_type_hints), self.send_answer],
force_async=True
elif origin is dict:
key_type, value_type = args if args else (str, Any)
if key_type != str:
raise ValueError('OpenAPI dict keys must be strings')
return Schema(
type="object",
additionalProperties=SlothApp.schema_from_type(value_type)
)
self.router.add(method, path_pattern, p)
elif origin is typing.Union:
schemas = [SlothApp.schema_from_type(a) for a in args]
return Schema(oneOf=schemas)
else:
raise NotImplementedError(f"Тип {tp} не поддержан")
def register_endpoint_components(self, handler: Callable):
callseq = getattr(handler, '__breakshaft_callseq__', [])
if len(callseq) == 0:
return []
ret = []
for call in callseq:
call: ConversionPoint
config: EndpointConfig | None = getattr(call.fn, '__turbosloth_config__', None)
if config is None:
continue
for k, v in config.header_schemas.items():
t = v.replacement_type
s = self.schema_from_type(t)
if isinstance(s, Reference):
self.openapi_app.register_component(t)
for k, v in config.query_schemas.items():
t = v.replacement_type
s = self.schema_from_type(t)
if isinstance(s, Reference):
self.openapi_app.register_component(t)
for k, v in config.path_schemas.items():
t = v.replacement_type
s = self.schema_from_type(t)
if isinstance(s, Reference):
self.openapi_app.register_component(t)
if config.body_schema is not None:
t = config.body_schema.replacement_type
s = self.schema_from_type(t)
if isinstance(s, Reference):
self.openapi_app.register_component(t)
@staticmethod
def construct_req_body(handler: Callable) -> Schema | Reference | None:
callseq = getattr(handler, '__breakshaft_callseq__', [])
if len(callseq) == 0:
return None
ret = {}
for call in callseq:
call: ConversionPoint
if BTYPE in call.requires:
return SlothApp.schema_from_type(call.injects)
return None
@staticmethod
def construct_req_params(handler: Callable) -> list[Parameter]:
callseq = getattr(handler, '__breakshaft_callseq__', [])
if len(callseq) == 0:
return []
ret = []
for call in callseq:
call: ConversionPoint
config: EndpointConfig | None = getattr(call.fn, '__turbosloth_config__', None)
if config is None:
continue
for k, v in config.query_schemas.items():
v: ParamSchema
allow_empty = v.has_default
t = v.schema
if isinstance(t, TypeAliasType):
t = t.__value__
origin = get_origin(t)
if origin is Union:
args = get_args(t)
if None in args or NoneType in args:
allow_empty = True
ret.append(Parameter(
name=v.key_name,
in_='query',
required=not v.has_default,
allowEmptyValue=allow_empty,
schema=SlothApp.schema_from_type(v.schema)
))
for k, v in config.path_schemas.items():
v: ParamSchema
ret.append(Parameter(
name=v.key_name,
in_='path',
required=not v.has_default,
schema=SlothApp.schema_from_type(v.schema)
))
for k, v in config.header_schemas.items():
v: ParamSchema
ret.append(Parameter(
name=v.key_name,
in_='header',
required=not v.has_default,
schema=SlothApp.schema_from_type(v.schema)
))
return ret
def route(self, method: MethodType, path_pattern: str, ok_return_code: str):
def decorator(fn: HandlerType):
path_substs = self.router.find_pattern_substs(path_pattern)
config, fork_with, fn_type_hints = self._integrate_func(fn, path_substs, ok_return_code=ok_return_code)
tmp_repo = self.inj_repo.fork(fork_with)
try:
p = tmp_repo.create_pipeline(
(Send, BasicRequest),
[ConversionPoint.from_fn(fn, type_remap=fn_type_hints), self.send_answer],
force_async=True,
)
self.router.add(method, path_pattern, p)
except Exception as e:
print(f'Error: unable to register handler {method} {path_pattern}: {e}')
traceback.print_exc()
p = None
if self.di_autodoc_prefix is not None and not path_pattern.startswith(
self.di_autodoc_prefix + '/' + method + self.di_autodoc_prefix):
@@ -339,9 +516,193 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp):
)
)
self.route('GET', self.di_autodoc_prefix + '/' + method + path_pattern)(
create_di_autodoc_handler(method, path_pattern, p, inflators_for_didoc, depgraph))
self.get(self.di_autodoc_prefix + '/' + method + path_pattern)(
create_di_autodoc_handler(method, path_pattern, p, depgraph))
if self.openapi_app is not None:
responses = {}
if config.return_schema is not None:
for k, v in config.return_schema.code_map.items():
schema = self.schema_from_type(v.body)
self.openapi_app.register_component(v.body)
content = {}
for ctype in self.serialize_selector.resp_serializers.keys():
content[ctype] = MediaType(schema=schema)
headers = None
if v.headers_provided:
pass
responses[k] = Response(
description=f'{v.body}',
content=content,
headers=headers
)
req_body = {}
req_schema = self.construct_req_body(p)
for ctype in self.serialize_selector.req_serializers.keys():
req_body[ctype] = MediaType(req_schema)
self.openapi_app.register_endpoint(
method,
path_pattern,
self.construct_req_params(p),
responses,
request_body=OpenApiRequestBody(req_body)
)
self.register_endpoint_components(p)
return fn
return decorator
def _didoc_name(self, master_name: str, schemas: dict[str, ParamSchema], ret_schema: type):
name = f'{master_name}['
for s in schemas.values():
name += f'{s.key_name}: '
name += breakshaft.util.universal_qualname(s.schema) + ', '
name = name[:-2] + '] -> ' + breakshaft.util.universal_qualname(ret_schema)
return name
def _integrate_func(self,
func: Callable,
path_params: Optional[Iterable[str]] = None,
ok_return_code: Optional[str] = None):
injectors = self.inj_repo.filtered_injectors(True, True)
injected_types = list(map(lambda x: x.injects, injectors)) + [BasicRequest, SerializedRequest]
config = EndpointConfig.from_handler(func, path_params or set(), injected_types, ok_return_code=ok_return_code)
fork_with = set()
i_p, infl = self._create_infl_from_schemas(config.path_schemas.values(), PTYPE)
if infl is not None:
fork_with |= infl
i_q, infl = self._create_infl_from_schemas(config.query_schemas.values(), QTYPE)
if infl is not None:
fork_with |= infl
i_h, infl = self._create_infl_from_schemas(config.header_schemas.values(), HTYPE)
if infl is not None:
fork_with |= infl
if config.return_schema is not None and len(config.return_schema.code_map) > 0:
def process_class(current_class: type, key, value):
if '__turbosloth_http_code_mapping__' not in current_class.__dict__:
current_class.__turbosloth_http_code_mapping__ = {}
current_class.__turbosloth_http_code_mapping__[key] = value
for subclass in current_class.__subclasses__():
process_class(subclass, key, value)
ret_map = config.return_schema.code_map
return_schemas_to_unwrap = set()
combined_body_return_types = []
combined_header_return_types = []
handler_hash = hash(func)
for http_code, schema in ret_map.items():
process_class(schema.body, handler_hash, int(http_code))
combined_body_return_types.append(schema.body)
combined_header_return_types.append(schema.headers)
if len(combined_body_return_types) > 0:
combined_body_return_type = Union[*combined_body_return_types]
combined_header_return_type = Union[*combined_header_return_types]
combined_return_type = tuple[combined_body_return_type, combined_header_return_type]
fork_with |= set(
ConversionPoint.from_fn(self.defl_generator.schema_to_deflator(combined_body_return_type),
rettype=HTTPResponseBodyPlaceholder,
type_remap={
'return': HTTPResponseBodyPlaceholder,
'from_data': combined_body_return_type
})
)
is_only_dict_headers = True
for t in combined_header_return_types:
origin = t
while o := get_origin(origin):
origin = o
if isinstance(origin, dict):
is_only_dict_headers = False
break
if len(combined_header_return_types) > 0 and not is_only_dict_headers:
fork_with |= set(
ConversionPoint.from_fn(self.defl_generator.schema_to_deflator(combined_header_return_type),
rettype=HTTPResponseHeadersPlaceholder,
type_remap={
'return': HTTPResponseHeadersPlaceholder,
'from_data': combined_header_return_type
})
)
else:
combined_return_type = tuple[combined_body_return_type, HTTPResponseHeadersPlaceholder]
fork_with |= set(
ConversionPoint.from_fn(
_gen_body_header_retcode_extraction(handler_hash),
type_remap={
'return': tuple[combined_return_type, HTTPReturnCode],
'ret': config.type_replacement['return'],
})
)
i_b = None
if config.body_schema is not None:
i_b = self.infl_generator.schema_to_inflator(
config.body_schema.schema,
strict_mode_override=False,
)
type_remap = {
'from_data': BTYPE,
'return': config.body_schema.replacement_type
}
fork_with |= set(ConversionPoint.from_fn(i_b, type_remap=type_remap))
fn_type_hints = get_type_hints(func)
for k, v in config.type_replacement.items():
fn_type_hints[k] = v
main_cps = ConversionPoint.from_fn(func, type_remap=fn_type_hints)
main_injects = main_cps[0].injects
if self.di_autodoc_prefix is not None:
if len(config.header_schemas) > 0:
setattr(i_h, '__turbosloth_DI_name__',
self._didoc_name('Inflator Header', config.header_schemas, main_injects))
if len(config.query_schemas) > 0:
setattr(i_q, '__turbosloth_DI_name__',
self._didoc_name('Inflator Query', config.query_schemas, main_injects))
if len(config.path_schemas) > 0:
setattr(i_p, '__turbosloth_DI_name__',
self._didoc_name('Inflator Path', config.path_schemas, main_injects))
if i_b is not None:
setattr(i_b, '__turbosloth_DI_name__',
self._didoc_name('Inflator Body', {config.body_schema.key_name: config.body_schema},
main_injects))
setattr(func, '__turbosloth_DI_name__', breakshaft.util.universal_qualname(main_injects))
fork_with |= set(ConversionPoint.from_fn(func, type_remap=fn_type_hints))
setattr(func, '__turbosloth_config__', config)
return config, fork_with, fn_type_hints
def add_injector(self, func: Callable):
_, fork_with, _ = self._integrate_func(func)
self.inj_repo.add_conversion_points(fork_with)
def mark_injector(self):
def inner(func: Callable):
self.add_injector(func)
return func
return inner

View File

@@ -5,11 +5,13 @@ from typing import Callable, Awaitable
import breakshaft.util_mermaid
import jinja2
from breakshaft.models import ConversionPoint
from turbosloth.interfaces.serialized import SerializedResponse
from turbosloth.interfaces.serialized.html import HTMLSerializedResponse
from turbosloth.types import InternalHandlerType
import breakshaft.renderer
import breakshaft.util
@dataclass
@@ -21,14 +23,22 @@ class MMDiagramData:
def create_di_autodoc_handler(method: str,
path: str,
handler: InternalHandlerType,
param_inflators: dict[str, Callable],
depgraph: str) -> Awaitable[SerializedResponse]:
callseq = getattr(handler, '__breakshaft_callseq__', [])
callseq: list[ConversionPoint] = getattr(handler, '__breakshaft_callseq__', [])
mmd_flowchart = breakshaft.util_mermaid.draw_callseq_mermaid(
list(map(lambda x: x._injection, breakshaft.renderer.deduplicate_callseq(
breakshaft.renderer.render_data_from_callseq((), {}, callseq)
)))
)
escaped_sources = []
for c in callseq:
if hasattr(c.fn, '__megasniff_sources__'):
name = getattr(c.fn, '__turbosloth_DI_name__', breakshaft.util.universal_qualname(c.fn))
escaped_sources.append(
(name, html.escape(getattr(c.fn, '__megasniff_sources__', ''))))
sources = getattr(handler, '__breakshaft_render_src__', '')
pipeline_escaped_sources = html.escape(sources)
@@ -42,12 +52,8 @@ def create_di_autodoc_handler(method: str,
MMDiagramData('Dependency graph', html.escape(depgraph)),
]
escaped_sources = [('Injection pipeline', pipeline_escaped_sources)]
escaped_sources.append(('Injection pipeline', pipeline_escaped_sources))
for k, v in param_inflators.items():
sources = getattr(v, '__megasniff_sources__', '')
src = html.escape(sources)
escaped_sources.append((k, src))
html_content = template.render(
handler_method=method,
@@ -57,6 +63,6 @@ def create_di_autodoc_handler(method: str,
)
async def _h() -> SerializedResponse:
return HTMLSerializedResponse(200, {}, html_content)
return HTMLSerializedResponse(html_content, 200, {})
return _h

View File

@@ -0,0 +1,276 @@
import re
from dataclasses import is_dataclass, asdict, fields, MISSING
from typing import Sequence, Optional, Any, get_type_hints, get_origin, Annotated, get_args, Union, TypeAliasType
from mypy.checkpattern import defaultdict
from turbosloth.doc.openapi_models import Info, Server, PathItem, Components, SecurityRequirement, Tag, \
ExternalDocumentation, Parameter, RequestBody, Operation, Response, OpenAPI, Schema, Reference, SchemaType
from turbosloth.internal_types import MethodType
def resolve_type(tp):
if isinstance(tp, TypeAliasType):
return resolve_type(tp.__value__)
origin = get_origin(tp)
if origin is Annotated:
base_type, *metadata = get_args(tp)
return resolve_type(base_type)
return tp
def type_to_schema(tp, components: Components, *,
_visited_component_names: Optional[list[str]] = None) -> Schema | Reference:
if _visited_component_names is None:
_visited_component_names = []
tp = resolve_type(tp)
origin = get_origin(tp)
args = get_args(tp)
if hasattr(tp, '__name__'):
name = tp.__name__
else:
name = str(tp)
if origin is None:
# базовый тип
if tp == str:
return Schema(type='string')
elif tp == int:
return Schema(type='integer')
elif tp == float:
return Schema(type='number', format='float')
elif tp == bool:
return Schema(type='boolean')
elif tp is type(None):
return Schema(type='null')
# Optional / Union
if origin is Union:
schemas = []
nullable = False
for a in args:
if a is type(None):
nullable = True
else:
schemas.append(type_to_schema(a, components, _visited_component_names=_visited_component_names))
if len(schemas) == 1:
schema = schemas[0]
else:
schema = Schema(oneOf=schemas)
if nullable:
schema = Schema(oneOf=[schema, Schema(type='null')])
return schema
# Базовые типы
if tp in (str, int, float, bool):
mapping: dict[type, SchemaType] = {
str: 'string',
int: 'integer',
float: 'number',
bool: 'boolean',
None: 'null'
}
schema = Schema(type=mapping[tp])
if tp is float:
schema.format = 'float'
return schema
# List
if origin is list:
item_type = args[0] if args else Any
return Schema(type='array',
items=type_to_schema(item_type, components, _visited_component_names=_visited_component_names))
# Tuple
if origin is tuple:
if args and args[-1] is Ellipsis:
return Schema(type="array",
items=type_to_schema(args[0], components, _visited_component_names=_visited_component_names))
else:
return Schema(
type="array",
prefixItems=[type_to_schema(a, components, _visited_component_names=_visited_component_names) for a in
args],
minItems=len(args),
maxItems=len(args)
)
# Dict
if origin is dict:
key_type, value_type = args if args else (str, Any)
if key_type is not str:
raise ValueError("OpenAPI dict keys must be strings")
return Schema(
type="object",
additionalProperties=type_to_schema(value_type, components,
_visited_component_names=_visited_component_names)
)
if name in _visited_component_names:
return Reference(f'#/components/schemas/{name}')
_visited_component_names.append(name)
if hasattr(tp, '__init__'):
init_params = get_type_hints(tp.__init__)
props = {}
required = []
# Проходим по параметрам конструктора
for pname, ptype in init_params.items():
if pname == "return":
continue
schema = type_to_schema(ptype, components, _visited_component_names=_visited_component_names)
# Пытаемся получить alias через Annotated / metadata
field_alias = getattr(ptype, "__metadata__", None)
alias = pname
if field_alias:
for meta in field_alias:
if isinstance(meta, dict) and "alias" in meta:
alias = meta["alias"]
props[alias] = schema
# Поля без default — required
param_default = getattr(tp, pname, None)
if param_default is None:
required.append(alias)
comp_schema = Schema(type="object", properties=props)
if required:
comp_schema.required = required
components.schemas[name] = comp_schema
return comp_schema
raise NotImplementedError(f"Тип {tp} не поддержан")
class OpenAPIApp:
info: Info
servers: list[Server]
paths: dict[str, PathItem]
webhooks: dict[str, PathItem]
components: Components
security: list[SecurityRequirement]
tags: list[Tag]
externalDocs: Optional[ExternalDocumentation]
def __init__(self, info: Info,
servers: Optional[list[Server]] = None,
externalDocs: Optional[ExternalDocumentation] = None):
if servers is None:
servers = []
self.info = info
self.servers = servers
self.paths = defaultdict(lambda: PathItem())
self.webhooks = {}
self.components = Components()
self.security = []
self.tags = []
self.externalDocs = externalDocs
def register_endpoint(self,
method: MethodType,
path: str,
parameters: list[Parameter],
responses: dict[str, Response],
summary: Optional[str] = None,
request_body: Optional[RequestBody] = None,
desciption: Optional[str] = None, ):
path_params = []
substs = set(re.findall(r'\{(.*?)}', path))
for s in substs:
path_params.append(Parameter(
name=s,
in_='path',
schema=Schema(type='string'),
required=True
))
self.paths[path].parameters = path_params
op = Operation(
responses,
summary,
desciption,
parameters=parameters,
requestBody=request_body
)
match method:
case 'GET':
self.paths[path].get = op
case 'POST':
self.paths[path].post = op
case 'PUSH':
return
case 'PUT':
self.paths[path].put = op
case 'PATCH':
self.paths[path].patch = op
case 'DELETE':
self.paths[path].delete = op
case 'HEAD':
self.paths[path].head = op
case 'CONNECT':
return
case 'OPTIONS':
self.paths[path].options = op
case 'TRACE':
self.paths[path].trace = op
def register_component(self, tp: type):
tp = resolve_type(tp)
schema = type_to_schema(tp, self.components)
self.components.schemas[f'{tp.__name__}'] = schema
def as_openapi(self) -> OpenAPI:
return OpenAPI(
self.info,
None,
self.servers,
self.paths,
self.webhooks,
self.components,
self.security,
self.tags,
self.externalDocs
)
@staticmethod
def export_openapi(obj: Any):
if is_dataclass(obj):
result = {}
hints = get_type_hints(obj.__class__)
for f in fields(obj):
# Проверяем, есть ли alias в metadata
alias = f.metadata.get("alias") if "alias" in f.metadata else None
# Для Annotated
tp = hints.get(f.name)
if getattr(tp, "__metadata__", None):
for meta in tp.__metadata__:
if isinstance(meta, dict) and "alias" in meta:
alias = meta["alias"]
key = alias or f.name
value = getattr(obj, f.name)
if value is not None:
result[key] = OpenAPIApp.export_openapi(value)
return result
elif isinstance(obj, dict):
return {k: OpenAPIApp.export_openapi(v) for k, v in obj.items() if v is not None}
elif isinstance(obj, (list, tuple, set)):
return [OpenAPIApp.export_openapi(v) for v in obj if v is not None]
else:
return obj
def export_as_dict(self) -> dict:
obj = self.as_openapi()
return self.export_openapi(obj)

View File

@@ -0,0 +1,368 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Mapping, MutableMapping, Optional, Sequence, Tuple, TypeAlias, Union, Literal
# --- alias metadata key for fields whose JSON key differs from Python attr name ---
ALIAS: Literal["alias"] = "alias"
# --- Generic helpers ---
JsonValue: TypeAlias = Union[None, bool, int, float, str, list["JsonValue"], dict[str, "JsonValue"]]
ReferenceOr: TypeAlias = Union["Reference", Any] # refined per usage below
# --- Spec: https://spec.openapis.org/oas/v3.1.0 ---
# Components -------------------------------------------------------------------
@dataclass
class Components:
schemas: dict[str, Union[Schema, Reference]] = field(default_factory=dict)
responses: dict[str, Union[Response, Reference]] = field(default_factory=dict)
parameters: dict[str, Union[Parameter, Reference]] = field(default_factory=dict)
examples: dict[str, Union[Example, Reference]] = field(default_factory=dict)
requestBodies: dict[str, Union[RequestBody, Reference]] = field(default_factory=dict)
headers: dict[str, Union[Header, Reference]] = field(default_factory=dict)
securitySchemes: dict[str, Union[SecurityScheme, Reference]] = field(default_factory=dict)
links: dict[str, Union[Link, Reference]] = field(default_factory=dict)
callbacks: dict[str, Union[Callback, Reference]] = field(default_factory=dict)
pathItems: dict[str, Union[PathItem, Reference]] = field(default_factory=dict) # OAS 3.1
# Root -------------------------------------------------------------------------
@dataclass
class OpenAPI:
info: Info
jsonSchemaDialect: Optional[str] = None
servers: list[Server] = field(default_factory=list)
paths: dict[str, PathItem] = field(default_factory=list)
webhooks: dict[str, PathItem] = field(default_factory=list)
components: Components = field(default_factory=Components)
security: list[SecurityRequirement] = field(default_factory=list)
tags: list[Tag] = field(default_factory=list)
externalDocs: Optional[ExternalDocumentation] = None
openapi: str = "3.1.0"
# Info -------------------------------------------------------------------------
@dataclass
class Contact:
name: Optional[str] = None
url: Optional[str] = None
email: Optional[str] = None
@dataclass
class License:
name: str = ""
identifier: Optional[str] = None
url: Optional[str] = None
@dataclass
class Info:
title: str
version: str
description: Optional[str] = None
termsOfService: Optional[str] = None
contact: Optional[Contact] = None
license: Optional[License] = None
# Servers ----------------------------------------------------------------------
@dataclass
class ServerVariable:
enum: Optional[list[str]] = None
default: str = ""
description: Optional[str] = None
@dataclass
class Server:
url: str
description: Optional[str] = None
variables: Optional[dict[str, ServerVariable]] = None
# External Docs ----------------------------------------------------------------
@dataclass
class ExternalDocumentation:
url: str = ""
description: Optional[str] = None
# Tags -------------------------------------------------------------------------
@dataclass
class Tag:
name: str = ""
description: Optional[str] = None
externalDocs: Optional[ExternalDocumentation] = None
# Reference --------------------------------------------------------------------
@dataclass
class Reference:
ref: str = field(metadata={ALIAS: "$ref"})
summary: Optional[str] = None
description: Optional[str] = None
# XML Object (for XML-specific representations) --------------------------------
@dataclass
class XML:
name: Optional[str] = None
namespace: Optional[str] = None
prefix: Optional[str] = None
attribute: Optional[bool] = None
wrapped: Optional[bool] = None
# Schema (JSON Schema vocab with OAS extensions) --------------------------------
SchemaType: TypeAlias = Literal[
"null", "boolean", "object", "array", "number", "string", "integer"
]
@dataclass
class Discriminator:
propertyName: str = ""
mapping: Optional[dict[str, str]] = None
@dataclass
class Schema:
# Core JSON Schema
type: Optional[SchemaType] = None
enum: Optional[list[JsonValue]] = None
const: Optional[JsonValue] = None
multipleOf: Optional[float] = None
maximum: Optional[float] = None
exclusiveMaximum: Optional[float] = None
minimum: Optional[float] = None
exclusiveMinimum: Optional[float] = None
maxLength: Optional[int] = None
minLength: Optional[int] = None
pattern: Optional[str] = None
items: Optional[Union[Schema, list[Schema]]] = None
prefixItems: Optional[list[Schema]] = None
additionalItems: Optional[Union[bool, Schema]] = None
unevaluatedItems: Optional[Union[bool, Schema]] = None
maxItems: Optional[int] = None
minItems: Optional[int] = None
uniqueItems: Optional[bool] = None
contains: Optional[Schema] = None
maxProperties: Optional[int] = None
minProperties: Optional[int] = None
required: Optional[list[str]] = None
properties: Optional[dict[str, Schema]] = None
patternProperties: Optional[dict[str, Schema]] = None
additionalProperties: Optional[Union[bool, Schema]] = None
unevaluatedProperties: Optional[Union[bool, Schema]] = None
dependentRequired: Optional[dict[str, list[str]]] = None
dependentSchemas: Optional[dict[str, Schema]] = None
propertyNames: Optional[Schema] = None
allOf: Optional[list[Schema]] = None
anyOf: Optional[list[Schema]] = None
oneOf: Optional[list[Schema]] = None
not_: Optional[Schema] = field(default=None, metadata={ALIAS: "not"})
if_: Optional[Schema] = field(default=None, metadata={ALIAS: "if"})
then: Optional[Schema] = None
else_: Optional[Schema] = field(default=None, metadata={ALIAS: "else"})
format: Optional[str] = None
contentEncoding: Optional[str] = None
contentMediaType: Optional[str] = None
contentSchema: Optional[Schema] = None
examples: Optional[list[JsonValue]] = None
default: Optional[JsonValue] = None
title: Optional[str] = None
description: Optional[str] = None
deprecated: Optional[bool] = None
readOnly: Optional[bool] = None
writeOnly: Optional[bool] = None
# OAS schema add-ons
discriminator: Optional[Discriminator] = None
xml: Optional[XML] = None
externalDocs: Optional[ExternalDocumentation] = None
# Example ----------------------------------------------------------------------
@dataclass
class Example:
summary: Optional[str] = None
description: Optional[str] = None
value: Optional[JsonValue] = None
externalValue: Optional[str] = None
# Encoding (for multipart/form-data etc.) --------------------------------------
@dataclass
class Header:
description: Optional[str] = None
required: Optional[bool] = None
deprecated: Optional[bool] = None
allowEmptyValue: Optional[bool] = None
style: Optional[str] = None
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
schema: Optional[Union[Schema, Reference]] = None
example: Optional[JsonValue] = None
examples: Optional[dict[str, Union[Example, Reference]]] = None
content: Optional[dict[str, "MediaType"]] = None
@dataclass
class Encoding:
contentType: Optional[str] = None
headers: Optional[dict[str, Union[Header, Reference]]] = None
style: Optional[str] = None
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
# Media types / Request & Response bodies --------------------------------------
@dataclass
class MediaType:
schema: Optional[Union[Schema, Reference]] = None
example: Optional[JsonValue] = None
examples: Optional[dict[str, Union[Example, Reference]]] = None
encoding: Optional[dict[str, Encoding]] = None
@dataclass
class RequestBody:
content: dict[str, MediaType] = field(default_factory=dict)
description: Optional[str] = None
required: Optional[bool] = None
@dataclass
class Response:
description: str = ""
headers: Optional[dict[str, Union[Header, Reference]]] = None
content: Optional[dict[str, MediaType]] = None
links: Optional[dict[str, Union["Link", Reference]]] = None
@dataclass
class Responses:
# keys are HTTP status codes or "default"
_map: dict[str, Union[Response, Reference]] = field(default_factory=dict, metadata={ALIAS: ""})
# Links / Callbacks -------------------------------------------------------------
@dataclass
class Link:
operationId: Optional[str] = None
operationRef: Optional[str] = None
parameters: Optional[dict[str, JsonValue]] = None
requestBody: Optional[JsonValue] = None
description: Optional[str] = None
server: Optional[Server] = None
@dataclass
class Callback:
# expression -> PathItem
_map: dict[str, "PathItem"] = field(default_factory=dict, metadata={ALIAS: ""})
# Parameters -------------------------------------------------------------------
ParameterLocation: TypeAlias = Literal["query", "header", "path", "cookie"]
@dataclass
class Parameter:
name: str = ""
in_: ParameterLocation = field(default="query", metadata={ALIAS: "in"})
description: Optional[str] = None
required: Optional[bool] = None
deprecated: Optional[bool] = None
allowEmptyValue: Optional[bool] = None
style: Optional[str] = None
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
schema: Optional[Union[Schema, Reference]] = None
example: Optional[JsonValue] = None
examples: Optional[dict[str, Union[Example, Reference]]] = None
content: Optional[dict[str, MediaType]] = None
# Operation / PathItem ----------------------------------------------------------
@dataclass
class Operation:
responses: dict[str, Union[Response, Reference]] = field(default_factory=dict)
summary: Optional[str] = None
description: Optional[str] = None
operationId: Optional[str] = None
parameters: list[Union[Parameter, Reference]] = field(default_factory=list)
requestBody: Optional[Union[RequestBody, Reference]] = None
callbacks: dict[str, Union[Callback, Reference]] = field(default_factory=dict)
deprecated: Optional[bool] = None
security: list["SecurityRequirement"] = field(default_factory=list)
servers: list[Server] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
externalDocs: Optional[ExternalDocumentation] = None
@dataclass
class PathItem:
summary: Optional[str] = None
description: Optional[str] = None
get: Optional[Operation] = None
put: Optional[Operation] = None
post: Optional[Operation] = None
delete: Optional[Operation] = None
options: Optional[Operation] = None
head: Optional[Operation] = None
patch: Optional[Operation] = None
trace: Optional[Operation] = None
servers: Optional[list[Server]] = None
parameters: Optional[list[Union[Parameter, Reference]]] = None
# Security (incl. OAuth2) ------------------------------------------------------
SecuritySchemeType: TypeAlias = Literal["apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"]
ApiKeyLocation: TypeAlias = Literal["query", "header", "cookie"]
@dataclass
class OAuthFlow:
authorizationUrl: Optional[str] = None
tokenUrl: Optional[str] = None
refreshUrl: Optional[str] = None
scopes: dict[str, str] = field(default_factory=dict)
@dataclass
class OAuthFlows:
implicit: Optional[OAuthFlow] = None
password: Optional[OAuthFlow] = None
clientCredentials: Optional[OAuthFlow] = None
authorizationCode: Optional[OAuthFlow] = None
@dataclass
class SecurityScheme:
type: SecuritySchemeType = "http"
description: Optional[str] = None
# apiKey
name: Optional[str] = None
in_: Optional[ApiKeyLocation] = field(default=None, metadata={ALIAS: "in"})
# http
scheme: Optional[str] = None
bearerFormat: Optional[str] = None
# mutualTLS has no extra fields
# oauth2
flows: Optional[OAuthFlows] = None
# openIdConnect
openIdConnectUrl: Optional[str] = None
# A single security requirement (scheme name -> required scopes)
SecurityRequirement: TypeAlias = dict[str, list[str]]

View File

@@ -3,11 +3,15 @@ from __future__ import annotations
import typing
import urllib.parse
from dataclasses import dataclass
from typing import Any, Mapping
from typing import Any, Mapping, Awaitable, Callable
from case_insensitive_dict import CaseInsensitiveDict
from turbosloth.internal_types import MethodType, Scope, ASGIMessage
from turbosloth.internal_types import MethodType, Scope, ASGIMessage, Receive
async def _dummy_recv() -> ASGIMessage:
return {}
@dataclass
@@ -16,24 +20,49 @@ class BasicRequest:
path: str
headers: CaseInsensitiveDict[str, str]
query: dict[str, list[Any] | Any]
body: bytes
path_matches: dict[str, str]
_do_body_recv: Receive
_body_recv_all = False
@property
def is_body_recv_done(self):
return self._body_recv_all
async def fetch_full_body(self) -> bytes:
is_done = False
buf = b''
while not is_done:
b, is_done = await self.fetch_body_part()
buf += b
return buf
async def fetch_body_part(self) -> tuple[bytes | bytearray, bool]:
if self._body_recv_all:
return b'', True
event = await self._do_body_recv()
buf = event.get('body', b'')
is_done = not event.get('more_body', False)
if is_done:
self._body_recv_all = True
return buf, is_done
def __init__(self,
method: MethodType,
path: str,
headers: Mapping[str, str],
query: dict[str, list[Any] | Any],
body: bytes):
body_recv: Receive):
self.method = method
self.path = path
self.headers = CaseInsensitiveDict(headers)
self.query = query
self.body = body
self._do_body_recv = body_recv
self.path_matches = {}
@classmethod
def from_scope(cls, scope: Scope) -> BasicRequest:
def from_scope(cls,
scope: Scope,
recv: Receive) -> BasicRequest:
path = scope['path']
method = typing.cast(MethodType, scope['method'])
headers = {}
@@ -50,9 +79,7 @@ class BasicRequest:
query[k] = v
query = typing.cast(dict[str, list[Any] | Any], query)
body = scope['body']
return BasicRequest(method, path, headers, query, body)
return BasicRequest(method, path, headers, query, recv)
@dataclass

View File

@@ -4,42 +4,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.interfaces.serialized import SerializedRequest, SerializedResponse, default_req_serializers, \
default_resp_serializers
from turbosloth.util import parse_content_type
class SerializeChoise(NamedTuple):
req: type[SerializedRequest]
resp: type[SerializedResponse]
charset: str
type SerializeReqChoise = tuple[type[SerializedRequest], str]
type SerializeRespChoise = tuple[type[SerializedResponse], str]
class SerializeSelector:
default_content_type: Optional[str]
serializers: dict[str, tuple[type[SerializedRequest], type[SerializedResponse]]]
default_accept_type: Optional[str]
req_serializers: dict[str, type[SerializedRequest]]
resp_serializers: dict[str, type[SerializedResponse]]
def __init__(self,
default_content_type: Optional[str] = 'text/plain',
filter_content_types: Optional[list[str]] = None):
default_accept_type: Optional[str] = 'text/plain',
filter_content_types: Optional[list[str]] = None,
filter_accept_types: Optional[list[str]] = None):
self.default_content_type = default_content_type
ser = {}
self.default_accept_type = default_accept_type
req_ser = {}
resp_ser = {}
if filter_content_types is None:
filter_content_types = list(default_serializers.keys())
filter_content_types = list(default_req_serializers.keys())
for k, v in default_serializers.items():
if filter_accept_types is None:
filter_accept_types = list(default_resp_serializers.keys())
for k, v in default_req_serializers.items():
if k in filter_content_types:
ser[k] = v
self.serializers = ser
req_ser[k] = v
for k, v in default_resp_serializers.items():
if k in filter_accept_types:
resp_ser[k] = v
self.req_serializers = req_ser
self.resp_serializers = resp_ser
def select(self, contenttype: str, charset: str) -> SerializeChoise:
choise = self.serializers.get(typing.cast(str, contenttype))
if choise is None and self.default_content_type is not None:
choise = self.serializers.get(self.default_content_type)
def select_req(self, contenttype: str) -> type[SerializedRequest]:
choise = self.req_serializers.get(typing.cast(str, contenttype))
if choise is None and self.default_accept_type is not None:
choise = self.req_serializers.get(self.default_accept_type)
if choise is None:
raise NotAcceptableException('acceptable content types: ' + ', '.join(self.serializers.keys()))
req, resp = choise
raise NotAcceptableException('acceptable content types: ' + ', '.join(self.req_serializers.keys()))
return SerializeChoise(req, resp, charset)
return choise
def select_resp(self, accepttype: str) -> type[SerializedResponse]:
choise = self.resp_serializers.get(typing.cast(str, accepttype))
if choise is None and self.default_content_type is not None:
choise = self.resp_serializers.get(self.default_content_type)
if choise is None:
raise NotAcceptableException('acceptable content types: ' + ', '.join(self.resp_serializers.keys()))
return choise

View File

@@ -1,31 +1,44 @@
from .base import SerializedRequest, SerializedResponse
default_serializers: dict[str, tuple[type[SerializedRequest], type[SerializedResponse]]] = {}
default_req_serializers: dict[str, type[SerializedRequest]] = {}
default_resp_serializers: dict[str, type[SerializedResponse]] = {}
try:
from .text import TextSerializedRequest, TextSerializedResponse
default_serializers['text/plain'] = (TextSerializedRequest, TextSerializedResponse)
default_req_serializers['text/plain'] = TextSerializedRequest
default_resp_serializers['text/plain'] = TextSerializedResponse
except:
pass
try:
from .json import JsonSerializedRequest, JsonSerializedResponse
default_serializers['application/json'] = (JsonSerializedRequest, JsonSerializedResponse)
default_req_serializers['application/json'] = JsonSerializedRequest
default_resp_serializers['application/json'] = JsonSerializedResponse
except:
pass
try:
from .xml import XMLSerializedRequest, XMLSerializedResponse
default_serializers['application/xml'] = (XMLSerializedRequest, XMLSerializedResponse)
default_req_serializers['application/xml'] = XMLSerializedRequest
default_resp_serializers['application/xml'] = XMLSerializedResponse
except:
pass
try:
from .msgpack import MessagePackSerializedRequest, MessagePackSerializedResponse
default_serializers['application/vnd.msgpack'] = (MessagePackSerializedRequest, MessagePackSerializedResponse)
default_req_serializers['application/vnd.msgpack'] = MessagePackSerializedRequest
default_resp_serializers['application/vnd.msgpack'] = MessagePackSerializedResponse
except:
pass
try:
from .multipart_form_data import MultipartFormSerializedRequest
default_req_serializers['application/octet-stream'] = MultipartFormSerializedRequest
default_req_serializers['application/x-www-form-urlencoded'] = MultipartFormSerializedRequest
except:
pass

View File

@@ -1,5 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any, Mapping
from case_insensitive_dict import CaseInsensitiveDict
@@ -35,16 +35,15 @@ class SerializedRequest:
return self.basic.path_matches
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
async def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
raise NotImplementedError()
@dataclass
class SerializedResponse:
code: int
headers: Mapping[str, str]
body: dict[str, Any] | list[Any] | str
content_type: str = ''
code: int = 200
headers: Mapping[str, str] = field(default_factory=dict)
def into_basic(self, charset: str) -> BasicResponse:
raise NotImplementedError()

View File

@@ -9,9 +9,8 @@ from .base import SerializedResponse
@dataclass
class HTMLSerializedResponse(SerializedResponse):
body: str
content_type = 'text/html'
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = self.content_type + '; charset=' + charset
headers['content-type'] = 'text/html; charset=' + charset
return BasicResponse(self.code, headers, str(self.body).encode(charset))

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
import asyncio
from dataclasses import dataclass
from queue import Queue, Empty
from typing import AsyncIterable
import python_multipart
from case_insensitive_dict import CaseInsensitiveDict
from python_multipart import MultipartParser, FormParser
from turbosloth.interfaces.base import BasicRequest, BasicResponse
from .base import SerializedRequest, SerializedResponse
@dataclass
class MultipartFormSerializedRequest(SerializedRequest):
_parser: FormParser
_parse_q: Queue
async def _fetch_part(self) -> bool:
was_done = self.basic.is_body_recv_done
buf, _ = await self.basic.fetch_body_part()
self._parser.write(buf)
return was_done
async def __aiter__(self) -> AsyncIterable:
is_done = False
while not is_done:
try:
it = self._parse_q.get(block=False)
except Empty:
is_done = await self._fetch_part()
else:
yield it
def __init__(self, basic: BasicRequest, charset: str, ):
content_type = basic.headers.get('content-type', 'multipart/form-data')
self.basic = basic
self.body = None
self.charset = charset
self._parse_q = Queue()
self._parser = python_multipart.create_form_parser(
basic.headers,
on_field=self._parse_q.put,
on_file=self._parse_q.put,
)
@classmethod
async def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
return MultipartFormSerializedRequest(basic, charset)

View File

@@ -6,15 +6,15 @@ from .base import SerializedRequest, SerializedResponse
class TextSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
b = basic.body.decode(charset)
async def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
body = await basic.fetch_full_body()
b = body.decode(charset)
return cls(b, basic, charset)
class TextSerializedResponse(SerializedResponse):
content_type = 'text/plain'
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = self.content_type + '; charset=' + charset
headers['content-type'] = 'text/plain; charset=' + charset
return BasicResponse(self.code, headers, str(self.body).encode(charset))

View File

@@ -9,11 +9,12 @@ from .base import SerializedRequest, SerializedResponse
class XMLSerializedRequest(SerializedRequest):
@classmethod
def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
if len(basic.body) == 0:
async def deserialize(cls, basic: BasicRequest, charset: str) -> SerializedRequest:
body = await basic.fetch_full_body()
if len(body) == 0:
b = {}
else:
btxt = basic.body.decode(charset)
btxt = body.decode(charset)
parsed = etree.fromstring(btxt)
b = {child.tag: child.text for child in parsed}
@@ -21,11 +22,10 @@ class XMLSerializedRequest(SerializedRequest):
class XMLSerializedResponse(SerializedResponse):
content_type = 'application/xml'
def into_basic(self, charset: str) -> BasicResponse:
headers = CaseInsensitiveDict(self.headers).copy()
headers['content-type'] = self.content_type + '; charset=' + charset
headers['content-type'] = 'application/xml; charset=' + charset
btxt = _into_xml(self.body)

View File

@@ -10,7 +10,6 @@ type Send = Callable[[ASGIMessage], Awaitable[None]]
type MethodType = (
Literal['GET'] |
Literal['POST'] |
Literal['PUSH'] |
Literal['PUT'] |
Literal['PATCH'] |
Literal['DELETE'] |

View File

@@ -1,18 +1,43 @@
from __future__ import annotations
import inspect
from typing import TYPE_CHECKING, overload, Any, TypeAlias, Optional, get_origin, get_args, Callable, get_type_hints
import typing
from types import UnionType
from typing import TYPE_CHECKING, overload, Any, TypeAlias, Optional, get_origin, get_args, Callable, get_type_hints, \
Iterable, Union, Type, Tuple
from dataclasses import dataclass
from typing import TypeVar, Generic, Annotated
from megasniff.utils import TupleSchemaItem
from turbosloth.doc.openapi_app import resolve_type
from turbosloth.interfaces.base import BasicResponse
from turbosloth.interfaces.serialized import SerializedResponse
from turbosloth.types import HTTPCodeType
T = TypeVar("T")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
from typing import Annotated, TypeAlias
def Resp[T1, T2](tps: type[T1] | tuple[type[T1], type[T2]],
return_code: HTTPCodeType = 200) -> tuple[type[T1], type[T2]] | type[T1]:
if isinstance(tps, tuple):
tp = tps[0]
htp = tps[1]
else:
tp = tps
htp = None
if htp is None:
return Annotated[tp, "__turbosloth__response_schema__", int(return_code)]
else:
return Annotated[tuple[tp, htp], "__turbosloth__response_schema__", int(return_code)]
def RequestBody[T](tp: type[T]) -> type[T]:
return Annotated[tp, "__turbosloth__request_body__"]
@@ -62,30 +87,176 @@ def get_endpoint_params_info(func) -> dict[str, ParamSchema]:
return result
@dataclass
class ReturnSchemaItem:
body: type
_headers: type
http_code: HTTPCodeType
def __init__(self, body: type, headers: type, http_code: HTTPCodeType):
self.body = body
self._headers = headers
self.http_code = http_code
@property
def headers(self) -> type:
if self._headers is None:
return dict
else:
return self._headers
@property
def headers_provided(self) -> bool:
return self._headers is not None
@property
def restore_func_ret_schema(self) -> type:
if self._headers is None:
return self.body
else:
return tuple[self.body, self.headers]
def __hash__(self):
return hash((self.body, self.headers))
@dataclass
class ReturnSchema:
code_map: dict[str, type | ReturnSchemaItem]
@classmethod
def from_raw(cls, tp: type):
origin = get_origin(tp)
if origin == Union:
schemas = get_args(tp)
else:
schemas = [tp]
schema_map = {}
for schema in schemas:
origin = get_origin(schema)
if origin == Annotated:
args = get_args(schema)
schema_type = args[1]
if schema_type != '__turbosloth__response_schema__':
continue
body_header = args[0]
http_code = str(int(args[2]))
body_schema = body_header
header_schema = None
b_origin = get_origin(body_header)
if b_origin == Tuple or b_origin == tuple:
args = get_args(body_header)
body_schema = args[0]
header_schema = args[1]
if http_code in schema_map.keys():
raise ValueError(f'Duplicate return code {http_code}')
schema_map[http_code] = ReturnSchemaItem(body_schema,
header_schema,
typing.cast(HTTPCodeType, http_code))
else:
continue
return ReturnSchema(schema_map)
#
# @classmethod
# def from_handler(cls, h: Callable, default_ok_code: str = '200') -> ReturnSchema:
# handle_hints = get_type_hints(h)
# ret_schema = handle_hints.get('return', None)
# ret_type = resolve_type(ret_schema)
# origin = get_origin(ret_type)
#
# ret = {}
#
# if origin is Union or origin is UnionType:
# for return_variant in get_args(ret_type):
# variant_origin = get_origin(return_variant)
# variant = return_variant
#
# if variant_origin is not None and issubclass(variant_origin, HTTPResponse):
# body_t, code_literal, header_t = get_args(return_variant)
# if code_literal is None:
# code_literal = default_ok_code
# else:
# code_literal, = get_args(code_literal)
# variant = ReturnSchemaItem(body_t, header_t)
# else:
# code_literal = default_ok_code
#
# if code_literal in ret.keys():
# raise ValueError(f'Duplicate return code: {code_literal}')
# ret[code_literal] = variant
# # TODO: union response schemas, dosctring :raises
# # чтобы разбирать возвращаемые схемы вида A|B|C|Annotated[D,"HTTPCode"]|SerializedResponse|BasicResponse
# # будет необходимо научить breakshaft делать разбиение A|B|C->D на множество isinstance
# # при условии что существуют все преобразования {A->D,B->D,C->D}
# # также, для определения исключений через :raises в docstring метода, необходимо научить breakshaft
# # преобразовывать A->B(raises C|D) в A->B|C|D
# # raise NotImplementedError('Union return schemas does not implemented yet')
# else:
# variant_origin = get_origin(ret_type)
# if variant_origin is not None and issubclass(variant_origin, HTTPResponse):
# body_t, code_literal, header_t = get_args(ret_type)
# if code_literal is None:
# code_literal = default_ok_code
# else:
# code_literal, = get_args(code_literal)
# variant = ReturnSchemaItem(body_t, header_t)
# else:
# code_literal = default_ok_code
# variant = ret_type
#
# if code_literal in ret.keys():
# raise ValueError(f'Duplicate return code: {code_literal}')
# ret[code_literal] = variant
#
# if len(list(ret.values())) != len(set(ret.values())):
# raise ValueError(f'Duplicate return schema on different return codes: {list(ret.values())}')
#
# for v in ret.values():
# orig = get_origin(v)
# if orig is not None:
# v = orig
# if not isinstance(v, ReturnSchemaItem) and issubclass(v, (SerializedResponse, BasicResponse)):
# raise NotImplementedError(
# 'Mixing autoserialized and manually serialized responses are not supported yet')
#
# return ReturnSchema(ret)
@dataclass
class EndpointConfig:
body_schema: ParamSchema | None
query_schemas: dict[str, ParamSchema]
path_schemas: dict[str, ParamSchema]
header_schemas: dict[str, ParamSchema]
return_schema: ReturnSchema | None
fn: Callable
type_replacement: dict[str, type]
@classmethod
def from_handler(cls, h: Callable, path_substituts: set[str]) -> EndpointConfig:
def from_handler(cls,
h: Callable,
path_substituts: set[str],
ignore_types: Iterable[type],
ok_return_code: Optional[str] = None) -> EndpointConfig:
body_schema = None
query_schemas = {}
path_schemas = {}
header_schemas = {}
type_replacement = {}
return_schema = None
handle_hints = get_endpoint_params_info(h)
for argname, s in handle_hints.items():
tp = s.schema
type_replacement[argname] = s.replacement_type
if argname == 'return':
if tp in ignore_types:
continue
type_replacement[argname] = s.replacement_type
if get_origin(tp) == Annotated:
args = get_args(tp)
@@ -117,4 +288,26 @@ class EndpointConfig:
else:
query_schemas[argname] = s
return EndpointConfig(body_schema, query_schemas, path_schemas, header_schemas, h, type_replacement)
ret_type = get_type_hints(h, include_extras=True).get('return')
if ret_type is not None:
return_schema = ReturnSchema.from_raw(ret_type)
if len(return_schema.code_map) > 0:
type CustomReturnTypeReplacement = ret_type
type_replacement['return'] = CustomReturnTypeReplacement
# if origin is not None:
# ret_type = origin
# if ok_return_code is not None and (ret_type is Union or ret_type is UnionType or not issubclass(ret_type,
# (SerializedResponse,
# BasicResponse))):
# return_schema = ReturnSchema.from_handler(h, ok_return_code)
# type ReturnTypeReplacement = object
# type_replacement['return'] = ReturnTypeReplacement
return EndpointConfig(body_schema,
query_schemas,
path_schemas,
header_schemas,
return_schema,
h,
type_replacement)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Awaitable, Literal, Any
from typing import Callable, Awaitable, Literal, Any, Generic, TypeVar
from turbosloth.interfaces.base import BasicRequest
from turbosloth.interfaces.serialized import SerializedResponse, SerializedRequest
@@ -19,3 +19,50 @@ class ContentType:
type Accept = ContentType
type HTTPReturnCode = str | int
type PreSerializedResponseBody = dict | list | int | str | float | bool | None
type PreSerializedResponseHeader = dict[str, str]
type ResponseHeaders = dict[str, str]
type HTTPResponseBodyPlaceholder = Any
type HTTPResponseHeadersPlaceholder = Any
BodyT = TypeVar('BodyT')
HeaderT = TypeVar('HeaderT', default=dict[str, str])
type HTTPCodeType = (Literal["100"] | Literal["101"] | Literal["102"] | Literal["103"] | Literal["200"] |
Literal["201"] | Literal["202"] | Literal["203"] | Literal["204"] | Literal["205"] |
Literal["206"] | Literal["207"] | Literal["208"] | Literal["226"] | Literal["300"] |
Literal["301"] | Literal["302"] | Literal["303"] | Literal["304"] | Literal["305"] |
Literal["306"] | Literal["307"] | Literal["308"] | Literal["400"] | Literal["401"] |
Literal["402"] | Literal["403"] | Literal["404"] | Literal["405"] | Literal["406"] |
Literal["407"] | Literal["408"] | Literal["409"] | Literal["410"] | Literal["411"] |
Literal["412"] | Literal["413"] | Literal["414"] | Literal["415"] | Literal["416"] |
Literal["417"] | Literal["418"] | Literal["421"] | Literal["422"] | Literal["423"] |
Literal["424"] | Literal["425"] | Literal["426"] | Literal["428"] | Literal["429"] |
Literal["431"] | Literal["451"] | Literal["500"] | Literal["501"] | Literal["502"] |
Literal["503"] | Literal["504"] | Literal["505"] | Literal["506"] | Literal["507"] |
Literal["508"] | Literal["510"] | Literal["511"] |
Literal[100] | Literal[101] | Literal[102] | Literal[103] | Literal[200] |
Literal[201] | Literal[202] | Literal[203] | Literal[204] | Literal[205] |
Literal[206] | Literal[207] | Literal[208] | Literal[226] | Literal[300] |
Literal[301] | Literal[302] | Literal[303] | Literal[304] | Literal[305] |
Literal[306] | Literal[307] | Literal[308] | Literal[400] | Literal[401] |
Literal[402] | Literal[403] | Literal[404] | Literal[405] | Literal[406] |
Literal[407] | Literal[408] | Literal[409] | Literal[410] | Literal[411] |
Literal[412] | Literal[413] | Literal[414] | Literal[415] | Literal[416] |
Literal[417] | Literal[418] | Literal[421] | Literal[422] | Literal[423] |
Literal[424] | Literal[425] | Literal[426] | Literal[428] | Literal[429] |
Literal[431] | Literal[451] | Literal[500] | Literal[501] | Literal[502] |
Literal[503] | Literal[504] | Literal[505] | Literal[506] | Literal[507] |
Literal[508] | Literal[510] | Literal[511]
)
# HTTPCodeT = TypeVar('HTTPCodeT', bound=HTTPCodeType | None, default=None)
#
#
# class HTTPResponse(Generic[BodyT, HTTPCodeT, HeaderT]):
# body: BodyT
# header: HeaderT
#
# def __init__(self, body: BodyT, header: HeaderT):
# self.body = body
# self.header = header

287
uv.lock generated
View File

@@ -1,18 +1,18 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.13"
[[package]]
name = "breakshaft"
version = "0.1.6"
version = "0.1.6.post4"
source = { registry = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" }
dependencies = [
{ name = "hatchling" },
{ name = "jinja2" },
]
sdist = { url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/breakshaft/0.1.6/breakshaft-0.1.6.tar.gz", hash = "sha256:443777f9f13889e79b31f763659b2d84540e045afb0f1c696ebec955213b653a" }
sdist = { url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/breakshaft/0.1.6.post4/breakshaft-0.1.6.post4.tar.gz", hash = "sha256:d971832bbf7fc08785684355b69c1af8782bbc1fe0502d8057db53cbff8faaf6" }
wheels = [
{ url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/breakshaft/0.1.6/breakshaft-0.1.6-py3-none-any.whl", hash = "sha256:abc3e99269cac906a0aafbc1f6af628198986eef571f1e27f29c2cb7f7bfde08" },
{ url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/breakshaft/0.1.6.post4/breakshaft-0.1.6.post4-py3-none-any.whl", hash = "sha256:cb7b206330fcb7da37f6e5fc010ddc3dd4503603327b3d10e40d3095e1795d41" },
]
[[package]]
@@ -47,55 +47,55 @@ wheels = [
[[package]]
name = "coverage"
version = "7.10.4"
version = "7.10.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" }
sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" },
{ url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" },
{ url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" },
{ url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" },
{ url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" },
{ url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" },
{ url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" },
{ url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" },
{ url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" },
{ url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" },
{ url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" },
{ url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" },
{ url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" },
{ url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" },
{ url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" },
{ url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" },
{ url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" },
{ url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" },
{ url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" },
{ url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" },
{ url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" },
{ url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" },
{ url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" },
{ url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" },
{ url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" },
{ url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" },
{ url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" },
{ url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" },
{ url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" },
{ url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" },
{ url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" },
{ url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" },
{ url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" },
{ url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" },
{ url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" },
{ url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" },
{ url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" },
{ url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" },
{ url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" },
{ url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" },
{ url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" },
{ url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" },
{ url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" },
{ url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" },
{ url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" },
{ url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" },
{ url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" },
{ url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" },
{ url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" },
{ url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" },
{ url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" },
{ url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" },
{ url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" },
{ url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" },
{ url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" },
{ url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" },
{ url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" },
{ url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" },
{ url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" },
{ url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" },
{ url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" },
{ url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" },
{ url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" },
{ url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" },
{ url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" },
{ url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" },
{ url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" },
{ url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" },
{ url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" },
{ url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" },
{ url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" },
{ url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" },
{ url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" },
{ url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" },
{ url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" },
]
[[package]]
@@ -145,26 +145,46 @@ wheels = [
[[package]]
name = "lxml"
version = "6.0.0"
version = "6.0.1"
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" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/bd/f9d01fd4132d81c6f43ab01983caea69ec9614b913c290a26738431a015d/lxml-6.0.1.tar.gz", hash = "sha256:2b3a882ebf27dd026df3801a87cf49ff791336e0f94b0fad195db77e01240690", size = 4070214, upload-time = "2025-08-22T10:37:53.525Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/43/c4/cd757eeec4548e6652eff50b944079d18ce5f8182d2b2cf514e125e8fbcb/lxml-6.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:485eda5d81bb7358db96a83546949c5fe7474bec6c68ef3fa1fb61a584b00eea", size = 8405139, upload-time = "2025-08-22T10:33:34.09Z" },
{ url = "https://files.pythonhosted.org/packages/ff/99/0290bb86a7403893f5e9658490c705fcea103b9191f2039752b071b4ef07/lxml-6.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d12160adea318ce3d118f0b4fbdff7d1225c75fb7749429541b4d217b85c3f76", size = 4585954, upload-time = "2025-08-22T10:33:36.294Z" },
{ url = "https://files.pythonhosted.org/packages/88/a7/4bb54dd1e626342a0f7df6ec6ca44fdd5d0e100ace53acc00e9a689ead04/lxml-6.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48c8d335d8ab72f9265e7ba598ae5105a8272437403f4032107dbcb96d3f0b29", size = 4944052, upload-time = "2025-08-22T10:33:38.19Z" },
{ url = "https://files.pythonhosted.org/packages/71/8d/20f51cd07a7cbef6214675a8a5c62b2559a36d9303fe511645108887c458/lxml-6.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:405e7cf9dbdbb52722c231e0f1257214202dfa192327fab3de45fd62e0554082", size = 5098885, upload-time = "2025-08-22T10:33:40.035Z" },
{ url = "https://files.pythonhosted.org/packages/5a/63/efceeee7245d45f97d548e48132258a36244d3c13c6e3ddbd04db95ff496/lxml-6.0.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:299a790d403335a6a057ade46f92612ebab87b223e4e8c5308059f2dc36f45ed", size = 5017542, upload-time = "2025-08-22T10:33:41.896Z" },
{ url = "https://files.pythonhosted.org/packages/57/5d/92cb3d3499f5caba17f7933e6be3b6c7de767b715081863337ced42eb5f2/lxml-6.0.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:48da704672f6f9c461e9a73250440c647638cc6ff9567ead4c3b1f189a604ee8", size = 5347303, upload-time = "2025-08-22T10:33:43.868Z" },
{ url = "https://files.pythonhosted.org/packages/69/f8/606fa16a05d7ef5e916c6481c634f40870db605caffed9d08b1a4fb6b989/lxml-6.0.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21e364e1bb731489e3f4d51db416f991a5d5da5d88184728d80ecfb0904b1d68", size = 5641055, upload-time = "2025-08-22T10:33:45.784Z" },
{ url = "https://files.pythonhosted.org/packages/b3/01/15d5fc74ebb49eac4e5df031fbc50713dcc081f4e0068ed963a510b7d457/lxml-6.0.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bce45a2c32032afddbd84ed8ab092130649acb935536ef7a9559636ce7ffd4a", size = 5242719, upload-time = "2025-08-22T10:33:48.089Z" },
{ url = "https://files.pythonhosted.org/packages/42/a5/1b85e2aaaf8deaa67e04c33bddb41f8e73d07a077bf9db677cec7128bfb4/lxml-6.0.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:fa164387ff20ab0e575fa909b11b92ff1481e6876835014e70280769920c4433", size = 4717310, upload-time = "2025-08-22T10:33:49.852Z" },
{ url = "https://files.pythonhosted.org/packages/42/23/f3bb1292f55a725814317172eeb296615db3becac8f1a059b53c51fc1da8/lxml-6.0.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7587ac5e000e1594e62278422c5783b34a82b22f27688b1074d71376424b73e8", size = 5254024, upload-time = "2025-08-22T10:33:52.22Z" },
{ url = "https://files.pythonhosted.org/packages/b4/be/4d768f581ccd0386d424bac615d9002d805df7cc8482ae07d529f60a3c1e/lxml-6.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57478424ac4c9170eabf540237125e8d30fad1940648924c058e7bc9fb9cf6dd", size = 5055335, upload-time = "2025-08-22T10:33:54.041Z" },
{ url = "https://files.pythonhosted.org/packages/40/07/ed61d1a3e77d1a9f856c4fab15ee5c09a2853fb7af13b866bb469a3a6d42/lxml-6.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:09c74afc7786c10dd6afaa0be2e4805866beadc18f1d843cf517a7851151b499", size = 4784864, upload-time = "2025-08-22T10:33:56.382Z" },
{ url = "https://files.pythonhosted.org/packages/01/37/77e7971212e5c38a55431744f79dff27fd751771775165caea096d055ca4/lxml-6.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7fd70681aeed83b196482d42a9b0dc5b13bab55668d09ad75ed26dff3be5a2f5", size = 5657173, upload-time = "2025-08-22T10:33:58.698Z" },
{ url = "https://files.pythonhosted.org/packages/32/a3/e98806d483941cd9061cc838b1169626acef7b2807261fbe5e382fcef881/lxml-6.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:10a72e456319b030b3dd900df6b1f19d89adf06ebb688821636dc406788cf6ac", size = 5245896, upload-time = "2025-08-22T10:34:00.586Z" },
{ url = "https://files.pythonhosted.org/packages/07/de/9bb5a05e42e8623bf06b4638931ea8c8f5eb5a020fe31703abdbd2e83547/lxml-6.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0fa45fb5f55111ce75b56c703843b36baaf65908f8b8d2fbbc0e249dbc127ed", size = 5267417, upload-time = "2025-08-22T10:34:02.719Z" },
{ url = "https://files.pythonhosted.org/packages/f2/43/c1cb2a7c67226266c463ef8a53b82d42607228beb763b5fbf4867e88a21f/lxml-6.0.1-cp313-cp313-win32.whl", hash = "sha256:01dab65641201e00c69338c9c2b8a0f2f484b6b3a22d10779bb417599fae32b5", size = 3610051, upload-time = "2025-08-22T10:34:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/34/96/6a6c3b8aa480639c1a0b9b6faf2a63fb73ab79ffcd2a91cf28745faa22de/lxml-6.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:bdf8f7c8502552d7bff9e4c98971910a0a59f60f88b5048f608d0a1a75e94d1c", size = 4009325, upload-time = "2025-08-22T10:34:06.24Z" },
{ url = "https://files.pythonhosted.org/packages/8c/66/622e8515121e1fd773e3738dae71b8df14b12006d9fb554ce90886689fd0/lxml-6.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6aeca75959426b9fd8d4782c28723ba224fe07cfa9f26a141004210528dcbe2", size = 3670443, upload-time = "2025-08-22T10:34:07.974Z" },
{ url = "https://files.pythonhosted.org/packages/38/e3/b7eb612ce07abe766918a7e581ec6a0e5212352194001fd287c3ace945f0/lxml-6.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:29b0e849ec7030e3ecb6112564c9f7ad6881e3b2375dd4a0c486c5c1f3a33859", size = 8426160, upload-time = "2025-08-22T10:34:10.154Z" },
{ url = "https://files.pythonhosted.org/packages/35/8f/ab3639a33595cf284fe733c6526da2ca3afbc5fd7f244ae67f3303cec654/lxml-6.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:02a0f7e629f73cc0be598c8b0611bf28ec3b948c549578a26111b01307fd4051", size = 4589288, upload-time = "2025-08-22T10:34:12.972Z" },
{ url = "https://files.pythonhosted.org/packages/2c/65/819d54f2e94d5c4458c1db8c1ccac9d05230b27c1038937d3d788eb406f9/lxml-6.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:beab5e54de016e730875f612ba51e54c331e2fa6dc78ecf9a5415fc90d619348", size = 4964523, upload-time = "2025-08-22T10:34:15.474Z" },
{ url = "https://files.pythonhosted.org/packages/5b/4a/d4a74ce942e60025cdaa883c5a4478921a99ce8607fc3130f1e349a83b28/lxml-6.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a08aefecd19ecc4ebf053c27789dd92c87821df2583a4337131cf181a1dffa", size = 5101108, upload-time = "2025-08-22T10:34:17.348Z" },
{ url = "https://files.pythonhosted.org/packages/cb/48/67f15461884074edd58af17b1827b983644d1fae83b3d909e9045a08b61e/lxml-6.0.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36c8fa7e177649470bc3dcf7eae6bee1e4984aaee496b9ccbf30e97ac4127fa2", size = 5053498, upload-time = "2025-08-22T10:34:19.232Z" },
{ url = "https://files.pythonhosted.org/packages/b6/d4/ec1bf1614828a5492f4af0b6a9ee2eb3e92440aea3ac4fa158e5228b772b/lxml-6.0.1-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:5d08e0f1af6916267bb7eff21c09fa105620f07712424aaae09e8cb5dd4164d1", size = 5351057, upload-time = "2025-08-22T10:34:21.143Z" },
{ url = "https://files.pythonhosted.org/packages/65/2b/c85929dacac08821f2100cea3eb258ce5c8804a4e32b774f50ebd7592850/lxml-6.0.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9705cdfc05142f8c38c97a61bd3a29581ceceb973a014e302ee4a73cc6632476", size = 5671579, upload-time = "2025-08-22T10:34:23.528Z" },
{ url = "https://files.pythonhosted.org/packages/d0/36/cf544d75c269b9aad16752fd9f02d8e171c5a493ca225cb46bb7ba72868c/lxml-6.0.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74555e2da7c1636e30bff4e6e38d862a634cf020ffa591f1f63da96bf8b34772", size = 5250403, upload-time = "2025-08-22T10:34:25.642Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e8/83dbc946ee598fd75fdeae6151a725ddeaab39bb321354a9468d4c9f44f3/lxml-6.0.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e38b5f94c5a2a5dadaddd50084098dfd005e5a2a56cd200aaf5e0a20e8941782", size = 4696712, upload-time = "2025-08-22T10:34:27.753Z" },
{ url = "https://files.pythonhosted.org/packages/f4/72/889c633b47c06205743ba935f4d1f5aa4eb7f0325d701ed2b0540df1b004/lxml-6.0.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5ec101a92ddacb4791977acfc86c1afd624c032974bfb6a21269d1083c9bc49", size = 5268177, upload-time = "2025-08-22T10:34:29.804Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b6/f42a21a1428479b66ea0da7bd13e370436aecaff0cfe93270c7e165bd2a4/lxml-6.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5c17e70c82fd777df586c12114bbe56e4e6f823a971814fd40dec9c0de518772", size = 5094648, upload-time = "2025-08-22T10:34:31.703Z" },
{ url = "https://files.pythonhosted.org/packages/51/b0/5f8c1e8890e2ee1c2053c2eadd1cb0e4b79e2304e2912385f6ca666f48b1/lxml-6.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:45fdd0415a0c3d91640b5d7a650a8f37410966a2e9afebb35979d06166fd010e", size = 4745220, upload-time = "2025-08-22T10:34:33.595Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f9/820b5125660dae489ca3a21a36d9da2e75dd6b5ffe922088f94bbff3b8a0/lxml-6.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d417eba28981e720a14fcb98f95e44e7a772fe25982e584db38e5d3b6ee02e79", size = 5692913, upload-time = "2025-08-22T10:34:35.482Z" },
{ url = "https://files.pythonhosted.org/packages/23/8e/a557fae9eec236618aecf9ff35fec18df41b6556d825f3ad6017d9f6e878/lxml-6.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8e5d116b9e59be7934febb12c41cce2038491ec8fdb743aeacaaf36d6e7597e4", size = 5259816, upload-time = "2025-08-22T10:34:37.482Z" },
{ url = "https://files.pythonhosted.org/packages/fa/fd/b266cfaab81d93a539040be699b5854dd24c84e523a1711ee5f615aa7000/lxml-6.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c238f0d0d40fdcb695c439fe5787fa69d40f45789326b3bb6ef0d61c4b588d6e", size = 5276162, upload-time = "2025-08-22T10:34:39.507Z" },
{ url = "https://files.pythonhosted.org/packages/25/6c/6f9610fbf1de002048e80585ea4719591921a0316a8565968737d9f125ca/lxml-6.0.1-cp314-cp314-win32.whl", hash = "sha256:537b6cf1c5ab88cfd159195d412edb3e434fee880f206cbe68dff9c40e17a68a", size = 3669595, upload-time = "2025-08-22T10:34:41.783Z" },
{ url = "https://files.pythonhosted.org/packages/72/a5/506775e3988677db24dc75a7b03e04038e0b3d114ccd4bccea4ce0116c15/lxml-6.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:911d0a2bb3ef3df55b3d97ab325a9ca7e438d5112c102b8495321105d25a441b", size = 4079818, upload-time = "2025-08-22T10:34:44.04Z" },
{ url = "https://files.pythonhosted.org/packages/0a/44/9613f300201b8700215856e5edd056d4e58dd23368699196b58877d4408b/lxml-6.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:2834377b0145a471a654d699bdb3a2155312de492142ef5a1d426af2c60a0a31", size = 3753901, upload-time = "2025-08-22T10:34:45.799Z" },
]
[[package]]
@@ -206,15 +226,15 @@ wheels = [
[[package]]
name = "megasniff"
version = "0.2.3"
version = "0.2.5.post1"
source = { registry = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" }
dependencies = [
{ name = "hatchling" },
{ name = "jinja2" },
]
sdist = { url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/megasniff/0.2.3/megasniff-0.2.3.tar.gz", hash = "sha256:448776b495bb9b6a7c6d8c1a26afa1290585ef648b3e861c779f7530a2dd36bb" }
sdist = { url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/megasniff/0.2.5.post1/megasniff-0.2.5.post1.tar.gz", hash = "sha256:d9ac04dd2b6df734e6582252cb60f46cefad667f519f5801ba43e8b2f56546a5" }
wheels = [
{ url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/megasniff/0.2.3/megasniff-0.2.3-py3-none-any.whl", hash = "sha256:b58803cfbcd113f18f20850250ac9630dcc5cec9336ac0fc024142e921047b59" },
{ url = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/files/megasniff/0.2.5.post1/megasniff-0.2.5.post1-py3-none-any.whl", hash = "sha256:69ce434c27286e4d559736bcb003df884f418b9405d04834e2560f1aa0e94e54" },
]
[[package]]
@@ -237,28 +257,28 @@ wheels = [
[[package]]
name = "mypy"
version = "1.17.1"
version = "1.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" }
sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447, upload-time = "2025-09-11T23:00:47.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" },
{ url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" },
{ url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" },
{ url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" },
{ url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" },
{ url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" },
{ url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" },
{ url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" },
{ url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" },
{ url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" },
{ url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" },
{ url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671, upload-time = "2025-09-11T22:58:29.814Z" },
{ url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023, upload-time = "2025-09-11T23:00:29.049Z" },
{ url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355, upload-time = "2025-09-11T23:00:04.544Z" },
{ url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944, upload-time = "2025-09-11T22:58:23.024Z" },
{ url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574, upload-time = "2025-09-11T22:59:52.152Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684, upload-time = "2025-09-11T22:58:44.454Z" },
{ url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265, upload-time = "2025-09-11T22:59:03.4Z" },
{ url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890, upload-time = "2025-09-11T23:00:00.607Z" },
{ url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291, upload-time = "2025-09-11T22:59:43.425Z" },
{ url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610, upload-time = "2025-09-11T23:00:17.604Z" },
{ url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697, upload-time = "2025-09-11T22:58:59.534Z" },
{ url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739, upload-time = "2025-09-11T22:58:51.644Z" },
{ url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212, upload-time = "2025-09-11T22:59:26.576Z" },
]
[[package]]
@@ -272,36 +292,36 @@ wheels = [
[[package]]
name = "orjson"
version = "3.11.2"
version = "3.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739, upload-time = "2025-08-12T15:12:28.626Z" }
sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419, upload-time = "2025-08-12T15:11:25.517Z" },
{ url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803, upload-time = "2025-08-12T15:11:27.357Z" },
{ url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337, upload-time = "2025-08-12T15:11:28.805Z" },
{ url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222, upload-time = "2025-08-12T15:11:30.18Z" },
{ url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020, upload-time = "2025-08-12T15:11:31.59Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721, upload-time = "2025-08-12T15:11:33.035Z" },
{ url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574, upload-time = "2025-08-12T15:11:34.433Z" },
{ url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225, upload-time = "2025-08-12T15:11:36.133Z" },
{ url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201, upload-time = "2025-08-12T15:11:37.642Z" },
{ url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193, upload-time = "2025-08-12T15:11:39.153Z" },
{ url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548, upload-time = "2025-08-12T15:11:40.768Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798, upload-time = "2025-08-12T15:11:42.164Z" },
{ url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402, upload-time = "2025-08-12T15:11:44.036Z" },
{ url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498, upload-time = "2025-08-12T15:11:45.864Z" },
{ url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051, upload-time = "2025-08-12T15:11:47.555Z" },
{ url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406, upload-time = "2025-08-12T15:11:49.445Z" },
{ url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788, upload-time = "2025-08-12T15:11:50.992Z" },
{ url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318, upload-time = "2025-08-12T15:11:52.495Z" },
{ url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231, upload-time = "2025-08-12T15:11:53.941Z" },
{ url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204, upload-time = "2025-08-12T15:11:55.409Z" },
{ url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237, upload-time = "2025-08-12T15:11:57.18Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578, upload-time = "2025-08-12T15:11:58.844Z" },
{ url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799, upload-time = "2025-08-12T15:12:00.352Z" },
{ url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461, upload-time = "2025-08-12T15:12:01.854Z" },
{ url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494, upload-time = "2025-08-12T15:12:03.337Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046, upload-time = "2025-08-12T15:12:04.87Z" },
{ url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" },
{ url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" },
{ url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" },
{ url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" },
{ url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" },
{ url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" },
{ url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" },
{ url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" },
{ url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" },
{ url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" },
{ url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" },
{ url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" },
{ url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" },
{ url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" },
{ url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" },
{ url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" },
{ url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" },
{ url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" },
{ url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" },
{ url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" },
{ url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" },
{ url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" },
{ url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" },
]
[[package]]
@@ -342,7 +362,7 @@ wheels = [
[[package]]
name = "pytest"
version = "8.4.1"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -351,32 +371,41 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest-cov"
version = "6.2.1"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
[[package]]
name = "trove-classifiers"
version = "2025.8.6.13"
version = "2025.9.11.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/21/707af14daa638b0df15b5d5700349e0abdd3e5140069f9ab6e0ccb922806/trove_classifiers-2025.8.6.13.tar.gz", hash = "sha256:5a0abad839d2ed810f213ab133d555d267124ddea29f1d8a50d6eca12a50ae6e", size = 16932, upload-time = "2025-08-06T13:26:26.479Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/9a/778622bc06632529817c3c524c82749a112603ae2bbcf72ee3eb33a2c4f1/trove_classifiers-2025.9.11.17.tar.gz", hash = "sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd", size = 16975, upload-time = "2025-09-11T17:07:50.97Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/44/323a87d78f04d5329092aada803af3612dd004a64b69ba8b13046601a8c9/trove_classifiers-2025.8.6.13-py3-none-any.whl", hash = "sha256:c4e7fc83012770d80b3ae95816111c32b085716374dccee0d3fbf5c235495f9f", size = 14121, upload-time = "2025-08-06T13:26:25.063Z" },
{ url = "https://files.pythonhosted.org/packages/e1/85/a4ff8758c66f1fc32aa5e9a145908394bf9cf1c79ffd1113cfdeb77e74e4/trove_classifiers-2025.9.11.17-py3-none-any.whl", hash = "sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd", size = 14158, upload-time = "2025-09-11T17:07:49.886Z" },
]
[[package]]
@@ -389,6 +418,7 @@ dependencies = [
{ name = "jinja2" },
{ name = "megasniff" },
{ name = "mypy" },
{ name = "python-multipart" },
]
[package.dev-dependencies]
@@ -423,8 +453,9 @@ requires-dist = [
{ name = "breakshaft", specifier = ">=0.1.6", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
{ name = "case-insensitive-dictionary", specifier = ">=0.2.1" },
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "megasniff", specifier = ">=0.2.3", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
{ name = "megasniff", specifier = ">=0.2.4", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
{ name = "mypy", specifier = ">=1.17.0" },
{ name = "python-multipart", specifier = ">=0.0.20" },
]
[package.metadata.requires-dev]
@@ -452,11 +483,11 @@ xml = [
[[package]]
name = "typing-extensions"
version = "4.14.1"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]