Compare commits
15 Commits
dd0c896df6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e761dd3fdf | |||
| 4a5ca2cca7 | |||
| 24c7169883 | |||
| f2ea4d728f | |||
| e5ea595d91 | |||
| f823d2df5a | |||
| c40bdca9e4 | |||
| faaa43fdf1 | |||
| dc83b3278c | |||
| 1191ee0ada | |||
| 7480ad326b | |||
| b69dc614ff | |||
| d531a8c00b | |||
| a763f0960c | |||
| 6cb01f6204 |
@@ -8,10 +8,12 @@ authors = [
|
||||
license = "LGPL-3.0-or-later"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"megasniff>=0.2.0",
|
||||
"breakshaft>=0.1.0",
|
||||
"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
|
||||
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()
|
||||
# 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]) -> 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]) -> 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],
|
||||
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,11 +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
|
||||
from megasniff import SchemaInflatorGenerator
|
||||
from breakshaft.models import ConversionPoint, Callgraph
|
||||
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
|
||||
@@ -13,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 .internal_types import Scope, Receive, Send, MethodType, QTYPE, BTYPE, PTYPE
|
||||
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:
|
||||
@@ -45,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:
|
||||
@@ -52,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())
|
||||
@@ -76,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'
|
||||
|
||||
@@ -96,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)
|
||||
@@ -147,65 +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]
|
||||
openapi_app: Optional[OpenAPIApp]
|
||||
|
||||
def __init__(self,
|
||||
on_startup: Optional[Callable[[], Awaitable[None]]] = None,
|
||||
on_shutdown: 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)
|
||||
self.inj_repo = ConvRepo()
|
||||
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(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
|
||||
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':
|
||||
@@ -217,88 +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
|
||||
|
||||
if req_schema is None:
|
||||
raise ValueError(f'Unable to find request schema in handler {fn}')
|
||||
|
||||
query_type, body_type, path_type = get_args(req_schema)
|
||||
q_inflator = None
|
||||
b_inflator = None
|
||||
p_inflator = None
|
||||
|
||||
fork_with = set()
|
||||
|
||||
def none_generator(*args) -> None:
|
||||
return None
|
||||
|
||||
if path_type not in [PTYPE, None, Any]:
|
||||
p_inflator = self.infl_generator.schema_to_inflator(
|
||||
path_type,
|
||||
strict_mode_override=False,
|
||||
from_type_override=QTYPE
|
||||
)
|
||||
fork_with.add(ConversionPoint(p_inflator, path_type, (PTYPE,)))
|
||||
else:
|
||||
fork_with.add(ConversionPoint(none_generator, path_type, (PTYPE,)))
|
||||
|
||||
if query_type not in [QTYPE, None, Any]:
|
||||
q_inflator = self.infl_generator.schema_to_inflator(
|
||||
query_type,
|
||||
strict_mode_override=False,
|
||||
from_type_override=QTYPE
|
||||
)
|
||||
fork_with.add(ConversionPoint(q_inflator, query_type, (QTYPE,)))
|
||||
else:
|
||||
fork_with.add(ConversionPoint(none_generator, query_type, (QTYPE,)))
|
||||
|
||||
if body_type != [BTYPE, None, Any]:
|
||||
b_inflator = self.infl_generator.schema_to_inflator(
|
||||
body_type,
|
||||
from_type_override=BTYPE
|
||||
)
|
||||
fork_with.add(ConversionPoint(b_inflator, body_type, (BTYPE,)))
|
||||
else:
|
||||
fork_with.add(ConversionPoint(none_generator, body_type, (BTYPE,)))
|
||||
|
||||
def construct_unwrap(q, b, p) -> UnwrappedRequest:
|
||||
return UnwrappedRequest(q, b, p)
|
||||
|
||||
fork_with |= {
|
||||
ConversionPoint(
|
||||
construct_unwrap,
|
||||
req_schema,
|
||||
((query_type or QTYPE), (body_type or BTYPE), (path_type or PTYPE))
|
||||
)
|
||||
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
|
||||
|
||||
@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)
|
||||
|
||||
if origin is Annotated:
|
||||
return SlothApp.schema_from_type(args[0])
|
||||
|
||||
if origin is None:
|
||||
# базовый тип
|
||||
if tp == str:
|
||||
return Schema(type='string')
|
||||
elif tp == int:
|
||||
return Schema(type='integer')
|
||||
elif tp == float:
|
||||
return Schema(type='number', format='float')
|
||||
elif tp == bool:
|
||||
return Schema(type='boolean')
|
||||
elif tp == type(None):
|
||||
return Schema(type='null')
|
||||
else:
|
||||
# кастомный класс → $ref
|
||||
return Reference(ref=f'#/components/schemas/{tp.__name__}')
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
conv = tmp_repo.get_conversion(
|
||||
(BasicRequest,),
|
||||
fn,
|
||||
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
|
||||
|
||||
out_conv = self.inj_repo.get_conversion(
|
||||
(Send, BasicRequest, handle_hints['return'],),
|
||||
self.send_answer,
|
||||
force_async=True
|
||||
)
|
||||
if self.di_autodoc_prefix is not None and not path_pattern.startswith(
|
||||
self.di_autodoc_prefix + '/' + method + self.di_autodoc_prefix):
|
||||
def provided():
|
||||
pass
|
||||
|
||||
async def pipeline(send: Send, req: BasicRequest):
|
||||
ret = await conv(req)
|
||||
await out_conv(send, req, ret)
|
||||
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)
|
||||
|
||||
self.router.add(method, path_pattern, pipeline)
|
||||
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
|
||||
|
||||
68
src/turbosloth/didoc/__init__.py
Normal file
68
src/turbosloth/didoc/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import html
|
||||
import importlib.resources
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
class MMDiagramData:
|
||||
name: str
|
||||
data: str
|
||||
|
||||
|
||||
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__', '')
|
||||
pipeline_escaped_sources = html.escape(sources)
|
||||
|
||||
template_path = importlib.resources.files('turbosloth.didoc')
|
||||
loader = jinja2.FileSystemLoader(str(template_path))
|
||||
templateEnv = jinja2.Environment(loader=loader)
|
||||
template = templateEnv.get_template('page.jinja2')
|
||||
|
||||
mmd_diagrams = [
|
||||
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,
|
||||
escaped_sources=escaped_sources,
|
||||
mmd_diagrams=mmd_diagrams,
|
||||
)
|
||||
|
||||
async def _h() -> SerializedResponse:
|
||||
return HTMLSerializedResponse(html_content, 200, {})
|
||||
|
||||
return _h
|
||||
266
src/turbosloth/didoc/page.jinja2
Normal file
266
src/turbosloth/didoc/page.jinja2
Normal file
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Code + Mermaid</title>
|
||||
|
||||
<!-- Highlight.js с CDN -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.11.0/styles/atom-one-dark.min.css">
|
||||
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.11.0/highlight.min.js"></script>
|
||||
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.11.0/languages/go.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<!-- Mermaid.js с CDN -->
|
||||
<script src="https://unpkg.com/mermaid/dist/mermaid.min.js " defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Инициализация Mermaid
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
||||
|
||||
// Инициализация вкладок
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
// Активация первой вкладки
|
||||
if (tabs.length > 0) {
|
||||
tabs[0].classList.add('active');
|
||||
tabContents[0].style.display = 'block';
|
||||
mermaid.init(undefined, tabContents[0].querySelector('.mermaid'));
|
||||
}
|
||||
|
||||
// Обработчик клика по вкладкам
|
||||
tabs.forEach((tab, index) => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
tabContents.forEach(c => c.style.display = 'none');
|
||||
|
||||
tab.classList.add('active');
|
||||
tabContents[index].style.display = 'block';
|
||||
|
||||
// Инициализация Mermaid для активной вкладки, если еще не была инициализирована
|
||||
const diagram = tabContents[index].querySelector('.mermaid');
|
||||
if (!diagram.dataset.initialized) {
|
||||
mermaid.init(undefined, diagram);
|
||||
diagram.dataset.initialized = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Логика перетаскивания
|
||||
const resizer = document.getElementById('resizer');
|
||||
const codeContainer = document.querySelector('.code-blocks');
|
||||
const diagramContainer = document.querySelector('.diagram-container');
|
||||
|
||||
let isDragging = false;
|
||||
|
||||
resizer.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const containerWidth = document.body.clientWidth;
|
||||
const x = e.clientX;
|
||||
const codeWidth = (x / containerWidth) * 100;
|
||||
|
||||
if (codeWidth >= 5 && codeWidth <= 95) {
|
||||
codeContainer.style.flex = `0 0 ${codeWidth}%`;
|
||||
diagramContainer.style.flex = `0 0 ${100 - codeWidth}%`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
document.body.style.cursor = 'default';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseleave', () => {
|
||||
isDragging = false;
|
||||
document.body.style.cursor = 'default';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
font-family: sans-serif;
|
||||
background-color: #1e1e1e;
|
||||
color: #e6e6e6;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
background-color: #2d2d2d;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid #444;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.topbar span {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-container, .diagram-container {
|
||||
flex: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
.code-container pre {
|
||||
background: #1e1e1e;
|
||||
color: #dcdcdc;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #444;
|
||||
padding: 0 1rem;
|
||||
background-color: #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
color: #ccc;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom: 2px solid #007acc;
|
||||
color: #007acc;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
display: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.mermaid .node rect,
|
||||
.mermaid .node circle,
|
||||
.mermaid .node ellipse {
|
||||
fill: #3a3a3a !important;
|
||||
stroke: #888 !important;
|
||||
}
|
||||
|
||||
.mermaid .edgePath path {
|
||||
stroke: #ccc !important;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
width: 5px;
|
||||
min-width: 5px;
|
||||
background-color: #444;
|
||||
cursor: ew-resize;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resizer::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: #888;
|
||||
left: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<span style="color: #007acc;">{{ handler_method }}</span>
|
||||
<span style="color: #888;">::</span>
|
||||
<span>{{ handler_path }}</span>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<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>
|
||||
|
||||
<div class="diagram-container">
|
||||
<ul class="tabs">
|
||||
{% for diagram in mmd_diagrams %}
|
||||
<li class="tab">{{ diagram.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% for diagram in mmd_diagrams %}
|
||||
<div class="tab-content">
|
||||
<div class="mermaid">
|
||||
{{ diagram.data }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
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,15 +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
|
||||
code: int = 200
|
||||
headers: Mapping[str, str] = field(default_factory=dict)
|
||||
|
||||
def into_basic(self, charset: str) -> BasicResponse:
|
||||
raise NotImplementedError()
|
||||
|
||||
16
src/turbosloth/interfaces/serialized/html.py
Normal file
16
src/turbosloth/interfaces/serialized/html.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from case_insensitive_dict import CaseInsensitiveDict
|
||||
|
||||
from turbosloth.interfaces.base import BasicResponse
|
||||
from .base import SerializedResponse
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTMLSerializedResponse(SerializedResponse):
|
||||
body: str
|
||||
|
||||
def into_basic(self, charset: str) -> BasicResponse:
|
||||
headers = CaseInsensitiveDict(self.headers).copy()
|
||||
headers['content-type'] = 'text/html; charset=' + charset
|
||||
return BasicResponse(self.code, headers, str(self.body).encode(charset))
|
||||
@@ -7,18 +7,20 @@ 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):
|
||||
|
||||
def into_basic(self, charset: str) -> BasicResponse:
|
||||
headers = CaseInsensitiveDict(self.headers).copy()
|
||||
headers['content-type'] = 'application/json; charset=utf-8'
|
||||
|
||||
@@ -7,15 +7,17 @@ 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):
|
||||
|
||||
def into_basic(self, charset: str) -> BasicResponse:
|
||||
headers = CaseInsensitiveDict(self.headers).copy()
|
||||
headers['content-type'] = 'application/vnd.msgpack'
|
||||
|
||||
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,12 +6,14 @@ 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):
|
||||
|
||||
def into_basic(self, charset: str) -> BasicResponse:
|
||||
headers = CaseInsensitiveDict(self.headers).copy()
|
||||
headers['content-type'] = 'text/plain; charset=' + 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,6 +22,7 @@ class XMLSerializedRequest(SerializedRequest):
|
||||
|
||||
|
||||
class XMLSerializedResponse(SerializedResponse):
|
||||
|
||||
def into_basic(self, charset: str) -> BasicResponse:
|
||||
headers = CaseInsensitiveDict(self.headers).copy()
|
||||
headers['content-type'] = 'application/xml; charset=' + charset
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Any, Callable, Awaitable, Literal, Annotated
|
||||
|
||||
from case_insensitive_dict import CaseInsensitiveDict
|
||||
|
||||
type Scope = dict[str, Any]
|
||||
type ASGIMessage = dict[str, Any]
|
||||
type Receive = Callable[[], Awaitable[ASGIMessage]]
|
||||
@@ -8,7 +10,6 @@ type Send = Callable[[ASGIMessage], Awaitable[None]]
|
||||
type MethodType = (
|
||||
Literal['GET'] |
|
||||
Literal['POST'] |
|
||||
Literal['PUSH'] |
|
||||
Literal['PUT'] |
|
||||
Literal['PATCH'] |
|
||||
Literal['DELETE'] |
|
||||
@@ -20,3 +21,4 @@ type MethodType = (
|
||||
type QTYPE = Annotated[dict[str, Any], 'query_params']
|
||||
type BTYPE = Annotated[dict[str, Any] | list[Any] | str | None, 'body']
|
||||
type PTYPE = Annotated[dict[str, str], 'path_matches']
|
||||
type HTYPE = Annotated[CaseInsensitiveDict[str, str], 'headers']
|
||||
|
||||
@@ -4,10 +4,12 @@ from typing import Any, TypeVar, Generic
|
||||
Q = TypeVar('Q')
|
||||
B = TypeVar('B')
|
||||
P = TypeVar('P')
|
||||
H = TypeVar('H')
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnwrappedRequest(Generic[Q, B, P]):
|
||||
class UnwrappedRequest(Generic[Q, B, P, H]):
|
||||
query: Q
|
||||
body: B
|
||||
path_matches: P
|
||||
headers: H
|
||||
|
||||
@@ -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
|
||||
|
||||
252
uv.lock
generated
252
uv.lock
generated
@@ -1,18 +1,18 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "breakshaft"
|
||||
version = "0.1.0.post2"
|
||||
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.0.post2/breakshaft-0.1.0.post2.tar.gz", hash = "sha256:d04f8685336080cddb5111422a9624c8afcc8c47264e1b847339139bfd690570" }
|
||||
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.0.post2/breakshaft-0.1.0.post2-py3-none-any.whl", hash = "sha256:fd4c9b213e2569c2c4d4f86cef2fe04065c08f9bffac3aa46b8ae21459f35074" },
|
||||
{ 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,33 +47,55 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.9.2"
|
||||
version = "7.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" }
|
||||
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/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" },
|
||||
{ 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]]
|
||||
@@ -123,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]]
|
||||
@@ -184,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]]
|
||||
@@ -215,22 +257,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.17.0"
|
||||
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/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" }
|
||||
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/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" },
|
||||
{ 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]]
|
||||
@@ -244,25 +292,36 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.11.0"
|
||||
version = "3.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/87/03ababa86d984952304ac8ce9fbd3a317afb4a225b9a81f9b606ac60c873/orjson-3.11.0.tar.gz", hash = "sha256:2e4c129da624f291bcc607016a99e7f04a353f6874f3bd8d9b47b88597d5f700", size = 5318246, upload-time = "2025-07-15T16:08:29.194Z" }
|
||||
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/31/63/82d9b6b48624009d230bc6038e54778af8f84dfd54402f9504f477c5cfd5/orjson-3.11.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a8ba9698655e16746fdf5266939427da0f9553305152aeb1a1cc14974a19cfb", size = 240125, upload-time = "2025-07-15T16:07:35.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/3a/d557ed87c63237d4c97a7bac7ac054c347ab8c4b6da09748d162ca287175/orjson-3.11.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:67133847f9a35a5ef5acfa3325d4a2f7fe05c11f1505c4117bb086fc06f2a58f", size = 129189, upload-time = "2025-07-15T16:07:37.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/5e/b2c9e22e2cd10aa7d76a629cee65d661e06a61fbaf4dc226386f5636dd44/orjson-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f797d57814975b78f5f5423acb003db6f9be5186b72d48bd97a1000e89d331d", size = 131953, upload-time = "2025-07-15T16:07:39.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/60/760fcd9b50eb44d1206f2b30c8d310b79714553b9d94a02f9ea3252ebe63/orjson-3.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28acd19822987c5163b9e03a6e60853a52acfee384af2b394d11cb413b889246", size = 126922, upload-time = "2025-07-15T16:07:41.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/8c46daa867ccc92da6de9567608be62052774b924a77c78382e30d50b579/orjson-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8d38d9e1e2cf9729658e35956cf01e13e89148beb4cb9e794c9c10c5cb252f8", size = 128787, upload-time = "2025-07-15T16:07:42.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/14/a2f1b123d85f11a19e8749f7d3f9ed6c9b331c61f7b47cfd3e9a1fedb9bc/orjson-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f094edd2b782650b0761fd78858d9254de1c1286f5af43145b3d08cdacfd51", size = 131895, upload-time = "2025-07-15T16:07:44.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/10/362e8192df7528e8086ea712c5cb01355c8d4e52c59a804417ba01e2eb2d/orjson-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d09176a4a9e04a5394a4a0edd758f645d53d903b306d02f2691b97d5c736a9e", size = 133868, upload-time = "2025-07-15T16:07:46.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4e/ef43582ef3e3dfd2a39bc3106fa543364fde1ba58489841120219da6e22f/orjson-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a585042104e90a61eda2564d11317b6a304eb4e71cd33e839f5af6be56c34d3", size = 128234, upload-time = "2025-07-15T16:07:48.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/fa/02dabb2f1d605bee8c4bb1160cfc7467976b1ed359a62cc92e0681b53c45/orjson-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2218629dbfdeeb5c9e0573d59f809d42f9d49ae6464d2f479e667aee14c3ef4", size = 130232, upload-time = "2025-07-15T16:07:50.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/76/951b5619605c8d2ede80cc989f32a66abc954530d86e84030db2250c63a1/orjson-3.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:613e54a2b10b51b656305c11235a9c4a5c5491ef5c283f86483d4e9e123ed5e4", size = 403648, upload-time = "2025-07-15T16:07:52.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e2/5fa53bb411455a63b3713db90b588e6ca5ed2db59ad49b3fb8a0e94e0dda/orjson-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9dac7fbf3b8b05965986c5cfae051eb9a30fced7f15f1d13a5adc608436eb486", size = 144572, upload-time = "2025-07-15T16:07:54.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/d0/7d6f91e1e0f034258c3a3358f20b0c9490070e8a7ab8880085547274c7f9/orjson-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b64b254414e2be55ac5257124b5602c5f0b4d06b80bd27d1165efe8f36e836", size = 132766, upload-time = "2025-07-15T16:07:55.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f8/4d46481f1b3fb40dc826d62179f96c808eb470cdcc74b6593fb114d74af3/orjson-3.11.0-cp313-cp313-win32.whl", hash = "sha256:359cbe11bc940c64cb3848cf22000d2aef36aff7bfd09ca2c0b9cb309c387132", size = 134638, upload-time = "2025-07-15T16:07:57.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/3f/544938dcfb7337d85ee1e43d7685cf8f3bfd452e0b15a32fe70cb4ca5094/orjson-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:0759b36428067dc777b202dd286fbdd33d7f261c6455c4238ea4e8474358b1e6", size = 129411, upload-time = "2025-07-15T16:07:58.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/0c/f75015669d7817d222df1bb207f402277b77d22c4833950c8c8c7cf2d325/orjson-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:51cdca2f36e923126d0734efaf72ddbb5d6da01dbd20eab898bdc50de80d7b5a", size = 126349, upload-time = "2025-07-15T16:08:00.322Z" },
|
||||
{ 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]]
|
||||
@@ -303,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'" },
|
||||
@@ -312,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]]
|
||||
@@ -347,8 +415,10 @@ source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "breakshaft" },
|
||||
{ name = "case-insensitive-dictionary" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "megasniff" },
|
||||
{ name = "mypy" },
|
||||
{ name = "python-multipart" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -380,10 +450,12 @@ xml = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "breakshaft", specifier = ">=0.1.0", 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 = "megasniff", specifier = ">=0.2.0", index = "https://git.nikto-b.ru/api/packages/nikto_b/pypi/simple" },
|
||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||
{ 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]
|
||||
@@ -411,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