Compare commits
11 Commits
b69dc614ff
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e761dd3fdf | |||
| 4a5ca2cca7 | |||
| 24c7169883 | |||
| f2ea4d728f | |||
| e5ea595d91 | |||
| f823d2df5a | |||
| c40bdca9e4 | |||
| faaa43fdf1 | |||
| dc83b3278c | |||
| 1191ee0ada | |||
| 7480ad326b |
@@ -8,11 +8,12 @@ authors = [
|
||||
license = "LGPL-3.0-or-later"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"megasniff>=0.2.0",
|
||||
"breakshaft>=0.1.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]
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
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.interfaces.serialized import SerializedResponse, SerializedRequest
|
||||
from turbosloth.internal_types import QTYPE, BTYPE, PTYPE, HTYPE
|
||||
from turbosloth.req_schema import UnwrappedRequest
|
||||
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
|
||||
from turbosloth.interfaces.serialized.multipart_form_data import MultipartFormSerializedRequest
|
||||
from turbosloth.schema import RequestBody, HeaderParam, QueryParam, Resp
|
||||
|
||||
app = SlothApp(di_autodoc_prefix='/didoc')
|
||||
# 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("/")
|
||||
async def index(req: UnwrappedRequest[QTYPE, BTYPE, PTYPE, HTYPE]) -> SerializedResponse:
|
||||
return SerializedResponse(200, {}, 'Hello, ASGI Router!')
|
||||
# @app.get("/")
|
||||
# async def index() -> SerializedResponse:
|
||||
# return SerializedResponse(200, {}, 'Hello, ASGI Router!')
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -23,11 +35,11 @@ class UserIdSchema:
|
||||
user_id: int
|
||||
|
||||
|
||||
@app.get("/user/")
|
||||
async def get_user(req: UnwrappedRequest[UserIdSchema, BTYPE, PTYPE, HTYPE]) -> SerializedResponse:
|
||||
print(req)
|
||||
resp: dict[str, Any] = {'message': f'Hello, User ы {req.query.user_id}!', 'from': 'server', 'echo': req.body}
|
||||
return SerializedResponse(200, {}, resp)
|
||||
# @app.get("/user/")
|
||||
# async def get_user(req: UnwrappedRequest[UserIdSchema, BTYPE, PTYPE, HTYPE]) -> SerializedResponse:
|
||||
# print(req)
|
||||
# resp: dict[str, Any] = {'message': f'Hello, User ы {req.query.user_id}!', 'from': 'server', 'echo': req.body}
|
||||
# return SerializedResponse(200, {}, resp)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -58,19 +70,216 @@ class PTYPESchema:
|
||||
user_id: int
|
||||
|
||||
|
||||
@app.post("/user/u{user_id}r")
|
||||
async def post_user(req: UnwrappedRequest[QTYPE, UserPostSchema, PTYPESchema, HTYPE],
|
||||
dat: SomeInternalData) -> SerializedResponse:
|
||||
print(req)
|
||||
print(dat)
|
||||
resp: dict[str, Any] = {
|
||||
'message': f'Hello, User {req.body.user_id}!',
|
||||
'from': 'server',
|
||||
'data': req.body.data,
|
||||
'inj': dat.a,
|
||||
'user_id': req.path_matches.user_id
|
||||
# @app.post("/user/u{user_id}r")
|
||||
# async def post_user(req: UnwrappedRequest[QTYPE, UserPostSchema, PTYPESchema, HTYPE],
|
||||
# dat: SomeInternalData) -> SerializedResponse:
|
||||
# print(req)
|
||||
# print(dat)
|
||||
# resp: dict[str, Any] = {
|
||||
# 'message': f'Hello, User {req.body.user_id}!',
|
||||
# 'from': 'server',
|
||||
# 'data': req.body.data,
|
||||
# 'inj': dat.a,
|
||||
# 'user_id': req.path_matches.user_id
|
||||
# }
|
||||
# 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'),
|
||||
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,
|
||||
'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__':
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
from __future__ import annotations
|
||||
import html
|
||||
import traceback
|
||||
import typing
|
||||
from typing import Optional, Callable, Awaitable, Protocol, get_type_hints, get_origin, get_args, Any, Annotated
|
||||
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, 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
|
||||
@@ -15,17 +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 .types import HandlerType, InternalHandlerType, ContentType
|
||||
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:
|
||||
@@ -47,6 +59,7 @@ class HTTPApp(ASGIApp):
|
||||
charset = properties.get('charset')
|
||||
|
||||
if charset is None:
|
||||
# TODO: extract default charsets based on content type
|
||||
if contenttype == 'application/json':
|
||||
charset = 'utf-8'
|
||||
else:
|
||||
@@ -54,18 +67,48 @@ 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)
|
||||
def extract_accept_type(self, req: BasicRequest, ct: ContentType) -> Accept:
|
||||
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)
|
||||
|
||||
def serialize_response(self, req: BasicRequest, sresp: SerializedResponse, ct: ContentType) -> BasicResponse:
|
||||
ser = self.serialize_selector.select(ct.contenttype, ct.charset)
|
||||
sresponser = ser.resp
|
||||
charset = properties.get('charset')
|
||||
|
||||
try:
|
||||
return sresponser.into_basic(sresp, ct.charset)
|
||||
except UnicodeEncodeError:
|
||||
return sresponser.into_basic(sresp, 'utf-8')
|
||||
if charset is None:
|
||||
# TODO: extract default charsets based on content type
|
||||
if contenttype == 'application/json':
|
||||
charset = 'utf-8'
|
||||
else:
|
||||
charset = 'latin1'
|
||||
|
||||
return ContentType(contenttype, 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:
|
||||
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')
|
||||
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())
|
||||
@@ -78,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'
|
||||
|
||||
@@ -98,10 +141,11 @@ class HTTPApp(ASGIApp):
|
||||
|
||||
await handler(send, req)
|
||||
return
|
||||
except (megasniff.exceptions.FieldValidationException, megasniff.exceptions.MissingFieldException):
|
||||
sresp = SerializedResponse(400, {}, 'Schema error')
|
||||
except (megasniff.exceptions.FieldValidationException, megasniff.exceptions.MissingFieldException) as e:
|
||||
print(e)
|
||||
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)
|
||||
@@ -149,77 +193,113 @@ 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
|
||||
self._on_shutdown = on_shutdown
|
||||
self.serialize_selector = SerializeSelector()
|
||||
self.infl_generator = SchemaInflatorGenerator(strict_mode=True)
|
||||
if serialize_selector is None:
|
||||
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':
|
||||
@@ -231,67 +311,398 @@ 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):
|
||||
handle_hints = get_type_hints(fn)
|
||||
req_schema = None
|
||||
for argname, tp in handle_hints.items():
|
||||
if argname == 'return':
|
||||
continue
|
||||
if get_origin(tp) == UnwrappedRequest:
|
||||
req_schema = tp
|
||||
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
|
||||
|
||||
if req_schema is not None:
|
||||
@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)
|
||||
|
||||
unwrap_types = get_args(req_schema)
|
||||
defaults = (QTYPE, BTYPE, PTYPE, HTYPE)
|
||||
if origin is Annotated:
|
||||
return SlothApp.schema_from_type(args[0])
|
||||
|
||||
def none_generator(*args) -> None:
|
||||
return None
|
||||
|
||||
def create_convertor(t, def_type):
|
||||
infl = None
|
||||
if t not in [def_type, None, Any]:
|
||||
infl = self.infl_generator.schema_to_inflator(
|
||||
t,
|
||||
strict_mode_override=False,
|
||||
from_type_override=def_type
|
||||
)
|
||||
return ConversionPoint(infl, t, (def_type,), (), )
|
||||
else:
|
||||
return ConversionPoint(none_generator, t, (def_type,), (), )
|
||||
|
||||
fork_with = set(map(lambda x: create_convertor(*x), zip(unwrap_types, defaults)))
|
||||
|
||||
def construct_unwrap(q: QTYPE, b: BTYPE, p: PTYPE, h: HTYPE) -> UnwrappedRequest:
|
||||
return UnwrappedRequest(q, b, p, h)
|
||||
|
||||
fork_with |= {
|
||||
ConversionPoint(
|
||||
construct_unwrap,
|
||||
req_schema,
|
||||
unwrap_types,
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
tmp_repo = self.inj_repo.fork(fork_with)
|
||||
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:
|
||||
tmp_repo = self.inj_repo
|
||||
# кастомный класс → $ref
|
||||
return Reference(ref=f'#/components/schemas/{tp.__name__}')
|
||||
|
||||
p = tmp_repo.create_pipeline(
|
||||
(Send, BasicRequest),
|
||||
[fn, self.send_answer],
|
||||
force_async=True
|
||||
elif origin is list:
|
||||
item_type = args[0] if args else Any
|
||||
return Schema(type='array', items=SlothApp.schema_from_type(item_type))
|
||||
|
||||
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):
|
||||
self.route('GET', self.di_autodoc_prefix + '/' + method + path_pattern)(
|
||||
create_di_autodoc_handler(method, path_pattern, p))
|
||||
def provided():
|
||||
pass
|
||||
|
||||
setattr(provided, '__qualname__', 'provided')
|
||||
|
||||
depgraph = breakshaft.util_mermaid.draw_depgraph_mermaid(
|
||||
tmp_repo.walker.generate_full_depgraph(
|
||||
tmp_repo.filtered_injectors(True, True)
|
||||
| set(ConversionPoint.from_fn(fn, type_remap=fn_type_hints))
|
||||
| set(ConversionPoint.from_fn(provided, rettype=BasicRequest))
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import html
|
||||
import importlib.resources
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
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
|
||||
@@ -17,11 +20,27 @@ class MMDiagramData:
|
||||
data: str
|
||||
|
||||
|
||||
def create_di_autodoc_handler(method: str, path: str, handler: InternalHandlerType) -> Callable[[], SerializedResponse]:
|
||||
callseq = getattr(handler, '__breakshaft_callseq__', [])
|
||||
mmd_flowchart = breakshaft.util_mermaid.draw_callseq_mermaid(callseq)
|
||||
def create_di_autodoc_handler(method: str,
|
||||
path: str,
|
||||
handler: InternalHandlerType,
|
||||
depgraph: str) -> Awaitable[SerializedResponse]:
|
||||
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__', '')
|
||||
escaped_sources = html.escape(sources)
|
||||
pipeline_escaped_sources = html.escape(sources)
|
||||
|
||||
template_path = importlib.resources.files('turbosloth.didoc')
|
||||
loader = jinja2.FileSystemLoader(str(template_path))
|
||||
@@ -29,11 +48,13 @@ def create_di_autodoc_handler(method: str, path: str, handler: InternalHandlerTy
|
||||
template = templateEnv.get_template('page.jinja2')
|
||||
|
||||
mmd_diagrams = [
|
||||
MMDiagramData('1', html.escape(mmd_flowchart)),
|
||||
MMDiagramData('2', html.escape(mmd_flowchart1)),
|
||||
MMDiagramData('3', html.escape(mmd_flowchart2)),
|
||||
MMDiagramData('Call sequence', html.escape(mmd_flowchart)),
|
||||
MMDiagramData('Dependency graph', html.escape(depgraph)),
|
||||
]
|
||||
|
||||
escaped_sources.append(('Injection pipeline', pipeline_escaped_sources))
|
||||
|
||||
|
||||
html_content = template.render(
|
||||
handler_method=method,
|
||||
handler_path=path,
|
||||
@@ -41,7 +62,7 @@ def create_di_autodoc_handler(method: str, path: str, handler: InternalHandlerTy
|
||||
mmd_diagrams=mmd_diagrams,
|
||||
)
|
||||
|
||||
def _h() -> SerializedResponse:
|
||||
return HTMLSerializedResponse(200, {}, html_content)
|
||||
async def _h() -> SerializedResponse:
|
||||
return HTMLSerializedResponse(html_content, 200, {})
|
||||
|
||||
return _h
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
// Логика перетаскивания
|
||||
const resizer = document.getElementById('resizer');
|
||||
const codeContainer = document.querySelector('.code-container');
|
||||
const codeContainer = document.querySelector('.code-blocks');
|
||||
const diagramContainer = document.querySelector('.diagram-container');
|
||||
|
||||
let isDragging = false;
|
||||
@@ -118,11 +118,18 @@
|
||||
|
||||
.code-container, .diagram-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-blocks {
|
||||
background-color: #2d2d2d;
|
||||
border-right: 1px solid #444;
|
||||
padding: 1rem;
|
||||
@@ -136,6 +143,15 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
transition: width 0.2s ease;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
background-color: #2d2d2d;
|
||||
display: flex;
|
||||
@@ -192,6 +208,7 @@
|
||||
|
||||
.resizer {
|
||||
width: 5px;
|
||||
min-width: 5px;
|
||||
background-color: #444;
|
||||
cursor: ew-resize;
|
||||
z-index: 10;
|
||||
@@ -216,10 +233,15 @@
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="code-container">
|
||||
<pre><code class="language-python">
|
||||
{{ escaped_sources }}
|
||||
</code></pre>
|
||||
<div class="code-blocks">
|
||||
{% for escaped_source in escaped_sources %}
|
||||
<div class="code-container">
|
||||
<h3>{{escaped_source[0]}}</h3>
|
||||
<pre><code class="language-python">
|
||||
{{ escaped_source[1] }}
|
||||
</code></pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="resizer" id="resizer"></div>
|
||||
|
||||
276
src/turbosloth/doc/openapi_app.py
Normal file
276
src/turbosloth/doc/openapi_app.py
Normal 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)
|
||||
368
src/turbosloth/doc/openapi_models.py
Normal file
368
src/turbosloth/doc/openapi_models.py
Normal 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]]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
49
src/turbosloth/interfaces/serialized/multipart_form_data.py
Normal file
49
src/turbosloth/interfaces/serialized/multipart_form_data.py
Normal 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)
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ type Send = Callable[[ASGIMessage], Awaitable[None]]
|
||||
type MethodType = (
|
||||
Literal['GET'] |
|
||||
Literal['POST'] |
|
||||
Literal['PUSH'] |
|
||||
Literal['PUT'] |
|
||||
Literal['PATCH'] |
|
||||
Literal['DELETE'] |
|
||||
|
||||
@@ -141,6 +141,10 @@ class Router:
|
||||
def __init__(self) -> None:
|
||||
self._root = Route()
|
||||
|
||||
@classmethod
|
||||
def find_pattern_substs(cls, path_pattern: str) -> set[str]:
|
||||
return set(re.findall(r'\{(.*?)}', path_pattern))
|
||||
|
||||
def add(self, method: MethodType, path_pattern: str, handler: InternalHandlerType) -> None:
|
||||
method = typing.cast(MethodType, method.upper())
|
||||
|
||||
|
||||
313
src/turbosloth/schema.py
Normal file
313
src/turbosloth/schema.py
Normal file
@@ -0,0 +1,313 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
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__"]
|
||||
|
||||
|
||||
def QueryParam[T](tp: type[T], rename: str | None = None) -> type[T]:
|
||||
return Annotated[tp, "__turbosloth__query_param__", rename]
|
||||
|
||||
|
||||
def HeaderParam[T](tp: type[T], rename: str | None = None) -> type[T]:
|
||||
return Annotated[tp, "__turbosloth__header_param__", rename]
|
||||
|
||||
|
||||
def PathParam[T](tp: type[T]) -> type[T]:
|
||||
return Annotated[tp, "__turbosloth__path_param__"]
|
||||
|
||||
|
||||
class ParamSchema(TupleSchemaItem):
|
||||
replacement_type: type
|
||||
|
||||
def __init__(self, schema: type, key_name: str, has_default: bool, default: Any,
|
||||
replacement_type: Optional[type] = None):
|
||||
super().__init__(schema, key_name, has_default, default)
|
||||
|
||||
type _replacement_type = schema
|
||||
if replacement_type is None:
|
||||
replacement_type = _replacement_type
|
||||
self.replacement_type = replacement_type
|
||||
|
||||
def copy_with_schema(self, nt: type, key_name=None) -> ParamSchema:
|
||||
return ParamSchema(nt, key_name or self.key_name, self.has_default, self.default, self.replacement_type)
|
||||
|
||||
|
||||
def get_endpoint_params_info(func) -> dict[str, ParamSchema]:
|
||||
sig = inspect.signature(func)
|
||||
type_hints = get_type_hints(func, include_extras=True)
|
||||
|
||||
result = {}
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
result[param_name] = ParamSchema(
|
||||
schema=type_hints.get(param_name, param.annotation),
|
||||
key_name=param_name,
|
||||
has_default=param.default is not param.empty,
|
||||
default=param.default if param.default is not param.empty else None,
|
||||
)
|
||||
|
||||
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],
|
||||
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
|
||||
if tp in ignore_types:
|
||||
continue
|
||||
type_replacement[argname] = s.replacement_type
|
||||
|
||||
if get_origin(tp) == Annotated:
|
||||
args = get_args(tp)
|
||||
t = args[0]
|
||||
schema_type = args[1]
|
||||
if schema_type == '__turbosloth__request_body__':
|
||||
body_schema = s.copy_with_schema(t)
|
||||
elif schema_type == '__turbosloth__query_param__':
|
||||
q_name = args[2] or argname
|
||||
if q_name in query_schemas.keys():
|
||||
raise ValueError(f'Duplicate query key: {q_name} on handler {h}')
|
||||
query_schemas[q_name] = s.copy_with_schema(t, q_name)
|
||||
elif schema_type == '__turbosloth__header_param__':
|
||||
h_name = args[2] or argname
|
||||
if h_name in header_schemas.keys():
|
||||
raise ValueError(f'Duplicate header key: {h_name} on handler {h}')
|
||||
header_schemas[argname] = s.copy_with_schema(t, h_name)
|
||||
elif schema_type == '__turbosloth__path_param__':
|
||||
if argname not in path_substituts:
|
||||
raise ValueError(f'Invalid path param: {argname}, does not exists in a pattern')
|
||||
path_schemas[argname] = s.copy_with_schema(t)
|
||||
elif argname in path_substituts:
|
||||
path_schemas[argname] = s.copy_with_schema(t)
|
||||
else:
|
||||
query_schemas[argname] = s.copy_with_schema(t)
|
||||
|
||||
elif argname in path_substituts:
|
||||
path_schemas[argname] = s
|
||||
else:
|
||||
query_schemas[argname] = s
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
@@ -16,3 +16,53 @@ type InternalHandlerType = Callable[[Send, BasicRequest], Awaitable[None]]
|
||||
class ContentType:
|
||||
contenttype: str
|
||||
charset: str
|
||||
|
||||
|
||||
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
|
||||
|
||||
289
uv.lock
generated
289
uv.lock
generated
@@ -1,18 +1,18 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "breakshaft"
|
||||
version = "0.1.3.post1"
|
||||
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.3.post1/breakshaft-0.1.3.post1.tar.gz", hash = "sha256:1f420b01b3f2c00a0aba65011736c723d3b889a71992cea76ab07e8267a68f96" }
|
||||
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.3.post1/breakshaft-0.1.3.post1-py3-none-any.whl", hash = "sha256:e9f5c2440774de5b6194e473614b20141b9696f9ace6759e84c83205168d3f1b" },
|
||||
{ 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.2"
|
||||
version = "7.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" }
|
||||
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/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" },
|
||||
{ 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.1"
|
||||
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.1/megasniff-0.2.1.tar.gz", hash = "sha256:c6ed47b40fbdc92d7aa061267d158eea457b560f1bc8f84d297b55e5e4a0ef2e" }
|
||||
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.1/megasniff-0.2.1-py3-none-any.whl", hash = "sha256:cff759fd61e9a4b8634329620cf13a941d7d5b38b834f8eab68e6ac78fa23589" },
|
||||
{ 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.1"
|
||||
version = "3.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/3b/fd9ff8ff64ae3900f11554d5cfc835fb73e501e043c420ad32ec574fe27f/orjson-3.11.1.tar.gz", hash = "sha256:48d82770a5fd88778063604c566f9c7c71820270c9cc9338d25147cbf34afd96", size = 5393373, upload-time = "2025-07-25T14:33:52.898Z" }
|
||||
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/c9/e9/880ef869e6f66279ce3a381a32afa0f34e29a94250146911eee029e56efc/orjson-3.11.1-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53cfefe4af059e65aabe9683f76b9c88bf34b4341a77d329227c2424e0e59b0e", size = 240835, upload-time = "2025-07-25T14:32:54.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/1f/52039ef3d03eeea21763b46bc99ebe11d9de8510c72b7b5569433084a17e/orjson-3.11.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:93d5abed5a6f9e1b6f9b5bf6ed4423c11932b5447c2f7281d3b64e0f26c6d064", size = 129226, upload-time = "2025-07-25T14:32:55.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/da/59fdffc9465a760be2cd3764ef9cd5535eec8f095419f972fddb123b6d0e/orjson-3.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf06642f3db2966df504944cdd0eb68ca2717f0353bb20b20acd78109374a6", size = 132261, upload-time = "2025-07-25T14:32:57.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/5c/8610911c7e969db7cf928c8baac4b2f1e68d314bc3057acf5ca64f758435/orjson-3.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dddf4e78747fa7f2188273f84562017a3c4f0824485b78372513c1681ea7a894", size = 128614, upload-time = "2025-07-25T14:32:58.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a1/a1db9d4310d014c90f3b7e9b72c6fb162cba82c5f46d0b345669eaebdd3a/orjson-3.11.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa3fe8653c9f57f0e16f008e43626485b6723b84b2f741f54d1258095b655912", size = 130968, upload-time = "2025-07-25T14:33:00.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/ff/11acd1fd7c38ea7a1b5d6bf582ae3da05931bee64620995eb08fd63c77fe/orjson-3.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6334d2382aff975a61f6f4d1c3daf39368b887c7de08f7c16c58f485dcf7adb2", size = 132439, upload-time = "2025-07-25T14:33:01.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f9/bb564dd9450bf8725e034a8ad7f4ae9d4710a34caf63b85ce1c0c6d40af0/orjson-3.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3d0855b643f259ee0cb76fe3df4c04483354409a520a902b067c674842eb6b8", size = 135299, upload-time = "2025-07-25T14:33:03.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/bb/c8eafe6051405e241dda3691db4d9132d3c3462d1d10a17f50837dd130b4/orjson-3.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eacdfeefd0a79987926476eb16e0245546bedeb8febbbbcf4b653e79257a8e4", size = 131004, upload-time = "2025-07-25T14:33:04.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/40/bed8d7dcf1bd2df8813bf010a25f645863a2f75e8e0ebdb2b55784cf1a62/orjson-3.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ed07faf9e4873518c60480325dcbc16d17c59a165532cccfb409b4cdbaeff24", size = 130583, upload-time = "2025-07-25T14:33:05.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e7/cfa2eb803ad52d74fbb5424a429b5be164e51d23f1d853e5e037173a5c48/orjson-3.11.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d308dd578ae3658f62bb9eba54801533225823cd3248c902be1ebc79b5e014", size = 404218, upload-time = "2025-07-25T14:33:07.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/21/bc703af5bc6e9c7e18dcf4404dcc4ec305ab9bb6c82d3aee5952c0c56abf/orjson-3.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c4aa13ca959ba6b15c0a98d3d204b850f9dc36c08c9ce422ffb024eb30d6e058", size = 146605, upload-time = "2025-07-25T14:33:08.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d26a0150534c4965a06f556aa68bf3c3b82999d5d7b0facd3af7b390c4af/orjson-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:be3d0653322abc9b68e5bcdaee6cfd58fcbe9973740ab222b87f4d687232ab1f", size = 135434, upload-time = "2025-07-25T14:33:09.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/b6/1cb28365f08cbcffc464f8512320c6eb6db6a653f03d66de47ea3c19385f/orjson-3.11.1-cp313-cp313-win32.whl", hash = "sha256:4dd34e7e2518de8d7834268846f8cab7204364f427c56fb2251e098da86f5092", size = 136596, upload-time = "2025-07-25T14:33:11.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/35/7870d0d3ed843652676d84d8a6038791113eacc85237b673b925802826b8/orjson-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6895d32032b6362540e6d0694b19130bb4f2ad04694002dce7d8af588ca5f77", size = 131319, upload-time = "2025-07-25T14:33:12.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/3e/5bcd50fd865eb664d4edfdaaaff51e333593ceb5695a22c0d0a0d2b187ba/orjson-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:bb7c36d5d3570fcbb01d24fa447a21a7fe5a41141fd88e78f7994053cc4e28f4", size = 126613, upload-time = "2025-07-25T14:33:13.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/d8/0a5cd31ed100b4e569e143cb0cddefc21f0bcb8ce284f44bca0bb0e10f3d/orjson-3.11.1-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7b71ef394327b3d0b39f6ea7ade2ecda2731a56c6a7cbf0d6a7301203b92a89b", size = 240819, upload-time = "2025-07-25T14:33:15.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/95/7eb2c76c92192ceca16bc81845ff100bbb93f568b4b94d914b6a4da47d61/orjson-3.11.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:77c0fe28ed659b62273995244ae2aa430e432c71f86e4573ab16caa2f2e3ca5e", size = 129218, upload-time = "2025-07-25T14:33:16.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/84/e6b67f301b18adbbc346882f456bea44daebbd032ba725dbd7b741e3a7f1/orjson-3.11.1-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:1495692f1f1ba2467df429343388a0ed259382835922e124c0cfdd56b3d1f727", size = 132238, upload-time = "2025-07-25T14:33:17.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/78/a45a86e29d9b2f391f9d00b22da51bc4b46b86b788fd42df2c5fcf3e8005/orjson-3.11.1-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:08c6a762fca63ca4dc04f66c48ea5d2428db55839fec996890e1bfaf057b658c", size = 130998, upload-time = "2025-07-25T14:33:19.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/8f/6eb3ee6760d93b2ce996a8529164ee1f5bafbdf64b74c7314b68db622b32/orjson-3.11.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e26794fe3976810b2c01fda29bd9ac7c91a3c1284b29cc9a383989f7b614037", size = 130559, upload-time = "2025-07-25T14:33:20.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/78/9572ae94bdba6813917c9387e7834224c011ea6b4530ade07d718fd31598/orjson-3.11.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4b4b4f8f0b1d3ef8dc73e55363a0ffe012a42f4e2f1a140bf559698dca39b3fa", size = 404231, upload-time = "2025-07-25T14:33:22.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a3/68381ad0757e084927c5ee6cfdeab1c6c89405949ee493db557e60871c4c/orjson-3.11.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:848be553ea35aa89bfefbed2e27c8a41244c862956ab8ba00dc0b27e84fd58de", size = 146658, upload-time = "2025-07-25T14:33:23.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/db/fac56acf77aab778296c3f541a3eec643266f28ecd71d6c0cba251e47655/orjson-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c964c29711a4b1df52f8d9966f015402a6cf87753a406c1c4405c407dd66fd45", size = 135443, upload-time = "2025-07-25T14:33:25.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/b1/326fa4b87426197ead61c1eec2eeb3babc9eb33b480ac1f93894e40c8c08/orjson-3.11.1-cp314-cp314-win32.whl", hash = "sha256:33aada2e6b6bc9c540d396528b91e666cedb383740fee6e6a917f561b390ecb1", size = 136643, upload-time = "2025-07-25T14:33:26.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/8e/2987ae2109f3bfd39680f8a187d1bc09ad7f8fb019dcdc719b08c7242ade/orjson-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:68e10fd804e44e36188b9952543e3fa22f5aa8394da1b5283ca2b423735c06e8", size = 131324, upload-time = "2025-07-25T14:33:27.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5f/253e08e6974752b124fbf3a4de3ad53baa766b0cb4a333d47706d307e396/orjson-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:f3cf6c07f8b32127d836be8e1c55d4f34843f7df346536da768e9f73f22078a1", size = 126605, upload-time = "2025-07-25T14:33:29.244Z" },
|
||||
{ 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.5.9.12"
|
||||
version = "2025.9.11.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/04/1cd43f72c241fedcf0d9a18d0783953ee301eac9e5d9db1df0f0f089d9af/trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5", size = 16940, upload-time = "2025-05-09T12:04:48.829Z" }
|
||||
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/92/ef/c6deb083748be3bcad6f471b6ae983950c161890bf5ae1b2af80cc56c530/trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce", size = 14119, upload-time = "2025-05-09T12:04:46.38Z" },
|
||||
{ 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]
|
||||
@@ -420,11 +450,12 @@ xml = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "breakshaft", specifier = ">=0.1.3", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
|
||||
{ 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.0", 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]]
|
||||
|
||||
Reference in New Issue
Block a user