diff --git a/flask_openapi/blueprint.py b/flask_openapi/blueprint.py index 5131d789..62a5d4b1 100644 --- a/flask_openapi/blueprint.py +++ b/flask_openapi/blueprint.py @@ -100,30 +100,20 @@ def register_api(self, api: "APIBlueprint") -> None: # Register the nested APIBlueprint as a blueprint self.register_blueprint(api) - def _add_url_rule( - self, - rule, - endpoint=None, - view_func=None, - provide_automatic_options=None, - **options, - ) -> None: - self.add_url_rule(rule, endpoint, view_func, provide_automatic_options, **options) - def _collect_openapi_info( self, rule: str, func: FunctionType, *, - tags: list[Tag] | None = None, + tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, responses: ResponseDict | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, doc_ui: bool = True, method: str = HTTPMethod.GET, @@ -203,14 +193,14 @@ def get( self, rule: str, *, - tags: list[Tag] | None = None, + tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -278,14 +268,14 @@ def post( self, rule: str, *, - tags: list[Tag] | None = None, + tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -353,14 +343,14 @@ def put( self, rule: str, *, - tags: list[Tag] | None = None, + tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -428,14 +418,14 @@ def delete( self, rule: str, *, - tags: list[Tag] | None = None, + tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -503,14 +493,14 @@ def patch( self, rule: str, *, - tags: list[Tag] | None = None, + tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, diff --git a/flask_openapi/endpoint.py b/flask_openapi/endpoint.py index c5e64cc2..c70cbe42 100644 --- a/flask_openapi/endpoint.py +++ b/flask_openapi/endpoint.py @@ -44,19 +44,16 @@ async def view_func(**kwargs) -> FlaskResponse: signature = inspect.signature(view_class.__init__) parameters = signature.parameters if parameters.get("view_kwargs"): - view_object = view_class(view_kwargs=view_kwargs) + view_object = view_class(view_kwargs=view_kwargs) # pragma: no cover else: view_object = view_class() response = await func(view_object, **func_kwargs) else: response = await func(**func_kwargs) - if hasattr(current_app, "validate_response"): - _validate_response = validate_response or current_app.validate_response - else: - _validate_response = validate_response + _validate_response = validate_response or current_app.validate_response # type: ignore - if _validate_response and responses: + if _validate_response and responses: # pragma: no cover validate_response_callback = getattr(current_app, "validate_response_callback") return validate_response_callback(response, responses) @@ -90,10 +87,7 @@ def view_func(**kwargs) -> FlaskResponse: else: response = func(**func_kwargs) - if hasattr(current_app, "validate_response"): - _validate_response = validate_response or current_app.validate_response - else: - _validate_response = validate_response + _validate_response = validate_response or current_app.validate_response # type: ignore if _validate_response and responses: validate_response_callback = getattr(current_app, "validate_response_callback") diff --git a/flask_openapi/models/operation.py b/flask_openapi/models/operation.py index 61fca4a0..21ba9058 100644 --- a/flask_openapi/models/operation.py +++ b/flask_openapi/models/operation.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import BaseModel from .callback import Callback @@ -18,7 +20,7 @@ class Operation(BaseModel): tags: list[str] | None = None summary: str | None = None description: str | None = None - externalDocs: ExternalDocumentation | None = None + externalDocs: ExternalDocumentation | dict[str, Any] | None = None operationId: str | None = None parameters: list[Parameter] | None = None requestBody: RequestBody | Reference | None = None @@ -27,6 +29,6 @@ class Operation(BaseModel): deprecated: bool | None = False security: list[SecurityRequirement] | None = None - servers: list[Server] | None = None + servers: list[Server | dict[str, Any]] | None = None model_config = {"extra": "allow"} diff --git a/flask_openapi/openapi.py b/flask_openapi/openapi.py index 2ccf1b09..4404a2d0 100644 --- a/flask_openapi/openapi.py +++ b/flask_openapi/openapi.py @@ -348,7 +348,7 @@ def register_api_view( self.components_schemas.update(**api_view.components_schemas) # Register the APIView with the current instance - api_view.register(self, url_prefix=url_prefix, view_kwargs=view_kwargs) + api_view.register(self, view_kwargs=view_kwargs) def _collect_openapi_info( self, @@ -358,12 +358,12 @@ def _collect_openapi_info( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, responses: ResponseDict | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, doc_ui: bool = True, method: str = HTTPMethod.GET, @@ -443,11 +443,11 @@ def get( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -518,11 +518,11 @@ def post( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -593,11 +593,11 @@ def put( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -668,11 +668,11 @@ def delete( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, @@ -743,11 +743,11 @@ def patch( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, responses: ResponseDict | None = None, validate_response: bool | None = None, diff --git a/flask_openapi/request.py b/flask_openapi/request.py index 9377a1e3..c948683c 100644 --- a/flask_openapi/request.py +++ b/flask_openapi/request.py @@ -57,7 +57,7 @@ def _validate_header(header: Type[BaseModel], func_kwargs: dict): value = request_headers.get(key_alias_title) else: key = model_field_key - value = request_headers[key_title] + value = request_headers.get(key_title) if value is not None: header_dict[key] = value if model_field_schema.get("type") == "null": diff --git a/flask_openapi/types.py b/flask_openapi/types.py index 3d466e17..8343657e 100644 --- a/flask_openapi/types.py +++ b/flask_openapi/types.py @@ -3,7 +3,9 @@ from pydantic import BaseModel -_ResponseDictValue = Type[BaseModel] | dict[Any, Any] | None +from .models import Response + +_ResponseDictValue = Type[BaseModel] | Response | dict[Any, Any] | None ResponseDict = dict[str | int | HTTPStatus, _ResponseDictValue] diff --git a/flask_openapi/utils.py b/flask_openapi/utils.py index ef1163f6..cf577ea9 100644 --- a/flask_openapi/utils.py +++ b/flask_openapi/utils.py @@ -293,6 +293,8 @@ def get_responses(responses: ResponseStrKeyDict, components_schemas: dict, opera elif isinstance(response, dict): response["description"] = response.get("description", HTTP_STATUS.get(key, "")) _responses[key] = Response(**response) + elif isinstance(response, Response): + _responses[key] = response else: # OpenAPI 3 support ^[a-zA-Z0-9\.\-_]+$ so we should normalize __name__ schema = get_model_schema(response, mode="serialization") @@ -477,18 +479,16 @@ def make_validation_error_response(e: ValidationError) -> FlaskResponse: return response -def run_validate_response(response: Any, responses: ResponseDict | None = None) -> Any: +def run_validate_response(response: Any, responses: ResponseDict) -> Any: """Validate response""" - if responses is None: - return response - if isinstance(response, tuple): # noqa + if isinstance(response, tuple): _resp, status_code = response[:2] elif isinstance(response, FlaskResponse): if response.mimetype != "application/json": # only application/json return response - _resp, status_code = response.json, response.status_code # noqa + _resp, status_code = response.json, response.status_code else: _resp, status_code = response, 200 diff --git a/flask_openapi/view.py b/flask_openapi/view.py index d3eb82bd..677323f3 100644 --- a/flask_openapi/view.py +++ b/flask_openapi/view.py @@ -105,12 +105,12 @@ def doc( tags: list[Tag | dict[str, Any]] | None = None, summary: str | None = None, description: str | None = None, - external_docs: ExternalDocumentation | None = None, + external_docs: ExternalDocumentation | dict[str, Any] | None = None, operation_id: str | None = None, responses: ResponseDict | None = None, deprecated: bool | None = None, security: list[dict[str, list[Any]]] | None = None, - servers: list[Server] | None = None, + servers: list[Server | dict[str, Any]] | None = None, openapi_extensions: dict[str, Any] | None = None, validate_response: bool | None = None, doc_ui: bool = True, @@ -188,15 +188,12 @@ def decorator(func): return decorator - def register( - self, app: "OpenAPI", url_prefix: str | None = None, view_kwargs: dict[Any, Any] | None = None - ) -> None: + def register(self, app: "OpenAPI", view_kwargs: dict[Any, Any] | None = None) -> None: """ Register the API views with the given OpenAPI app. Args: app: An instance of the OpenAPI app. - url_prefix: A path to prepend to all the APIView's urls view_kwargs: Additional keyword arguments to pass to the API views. """ for rule, (cls, methods) in self.views.items(): @@ -214,14 +211,9 @@ def register( body, view_class=cls, view_kwargs=view_kwargs, - responses=func.responses, + responses=getattr(func, "responses", None), validate_response=_validate_response, ) - if url_prefix and self.url_prefix and url_prefix != self.url_prefix: - rule = url_prefix + rule.removeprefix(self.url_prefix) - elif url_prefix and not self.url_prefix: - rule = url_prefix.rstrip("/") + "/" + rule.lstrip("/") - options: dict[str, Any] = {"endpoint": cls.__name__ + "." + method.lower(), "methods": [method.upper()]} app.add_url_rule(rule, view_func=view_func, **options) diff --git a/tests/config.py b/tests/config.py new file mode 100644 index 00000000..0c95293b --- /dev/null +++ b/tests/config.py @@ -0,0 +1,10 @@ +SWAGGER_CONFIG = { + "docExpansion": "none", + "validatorUrl": None, + "tryItOutEnabled": True, + "filter": True, + "tagsSorter": "alpha", + "persistAuthorization": True, +} + +JWT = [{"jwt": []}] diff --git a/tests/test_api_blueprint.py b/tests/test_api_blueprint.py index 40d7a33f..8908bb3c 100644 --- a/tests/test_api_blueprint.py +++ b/tests/test_api_blueprint.py @@ -1,40 +1,12 @@ import pytest from pydantic import BaseModel, Field -from flask_openapi import APIBlueprint, Info, OpenAPI, Tag +from flask_openapi import APIBlueprint, OpenAPI -info = Info(title="book API", version="1.0.0") - -jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} -security_schemes = {"jwt": jwt} - - -def operation_id_callback(*, bp_name: str = None, name: str, path: str, method: str) -> str: - assert bp_name == "/book" - return name - - -app = OpenAPI(__name__, info=info, security_schemes=security_schemes) +app = OpenAPI(__name__) app.config["TESTING"] = True -tag = Tag(name="book", description="Book") -security = [{"jwt": []}] - - -class Unauthorized(BaseModel): - code: int = Field(-1, description="Status Code") - message: str = Field("Unauthorized!", description="Exception Information") - - -api = APIBlueprint( - "/book", - __name__, - url_prefix="/api", - abp_tags=[tag], - abp_security=security, - abp_responses={"401": Unauthorized}, - operation_id_callback=operation_id_callback, -) +api = APIBlueprint("/book", __name__, url_prefix="/api") try: api.register_api(api) @@ -55,63 +27,48 @@ class BookBody(BaseModel): class BookPath(BaseModel): - bid: int = Field(..., description="book id") + id: int = Field(..., description="book id") -@api.post("/book", doc_ui=False) -def create_book(body: BookBody): - assert body.age == 3 +@api.get("/book/") +def get_book(path: BookPath): + assert path.id == 1 return {"code": 0, "message": "ok"} -@api.put("/book/", operation_id="update") -def update_book(path: BookPath, body: BookBody): - assert path.bid == 1 +@api.post("/book") +def create_book(body: BookBody): assert body.age == 3 return {"code": 0, "message": "ok"} -@api.patch("/book/") -def update_book1(path: BookPath, body: BookBody): - assert path.bid == 1 +@api.put("/book/", operation_id="update") +def update_book(path: BookPath, body: BookBody): + assert path.id == 1 assert body.age == 3 return {"code": 0, "message": "ok"} -@api.patch("/v2/book/") -def update_book1_v2(path: BookPath, body: BookBody): - assert path.bid == 1 +@api.patch("/book/") +def update_book1(path: BookPath, body: BookBody): + assert path.id == 1 assert body.age == 3 return {"code": 0, "message": "ok"} -@api.delete("/book/") +@api.delete("/book/") def delete_book(path: BookPath): - assert path.bid == 1 + assert path.id == 1 return {"code": 0, "message": "ok"} -@api.get("/book/") -def get_book(path: BookPath): - """Get Book - Here is a book - Here's another line in the description - """ - return {"title": "test", "Author": "author"} - - # register api app.register_api(api) -def test_openapi(client): - resp = client.get("/openapi/openapi.json") +def test_get(client): + resp = client.get("/api/book/1") assert resp.status_code == 200 - assert resp.json == app.api_doc - assert resp.json["paths"]["/api/book/{bid}"]["put"]["operationId"] == "update" - assert resp.json["paths"]["/api/book/{bid}"]["delete"]["operationId"] == "delete_book" - expected_description = "Here is a book
Here's another line in the description" - assert resp.json["paths"]["/api/book/{bid}"]["get"]["description"] == expected_description def test_post(client): @@ -128,45 +85,25 @@ def test_patch(client): resp = client.patch("/api/book/1", json={"age": 3}) assert resp.status_code == 200 - resp = client.patch("/api/v2/book/1", json={"age": 3}) - assert resp.status_code == 200 - def test_delete(client): resp = client.delete("/api/book/1") assert resp.status_code == 200 -# Create a second blueprint here to test when `url_prefix` is None -author_api = APIBlueprint( - "/author", - __name__, - abp_tags=[tag], - abp_security=security, - abp_responses={"401": Unauthorized}, -) - - class AuthorBody(BaseModel): age: int | None = Field(..., ge=1, le=100, description="Age") -@author_api.post("/") -def get_author(body: AuthorBody): - pass - - -def create_app(): - app = OpenAPI(__name__, info=info, security_schemes=security_schemes) - app.register_api(api, url_prefix="/1.0") - app.register_api(author_api, url_prefix="/1.0/author") +def register_apis(): + _app = OpenAPI(__name__) + _app.register_api(api, url_prefix="/1.0") # Invoke twice to ensure that call is idempotent -create_app() -create_app() +register_apis() +register_apis() def test_blueprint_path_and_prefix(): - assert list(api.paths.keys()) == ["/1.0/book/{bid}", "/1.0/v2/book/{bid}"] - assert list(author_api.paths.keys()) == ["/1.0/author/{aid}"] + assert list(api.paths.keys()) == ["/1.0/book/{id}", "/1.0/book"] diff --git a/tests/test_api_view.py b/tests/test_api_view.py index bfc94096..92f2e14c 100644 --- a/tests/test_api_view.py +++ b/tests/test_api_view.py @@ -1,24 +1,26 @@ import pytest from pydantic import BaseModel, Field -from flask_openapi import APIView, Info, OpenAPI, Tag +from flask_openapi import APIView, OpenAPI +from tests.config import JWT -info = Info(title="book API", version="1.0.0") -jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} -security_schemes = {"jwt": jwt} - -app = OpenAPI(__name__, info=info, security_schemes=security_schemes) +app = OpenAPI(__name__) app.config["TESTING"] = True -security = [{"jwt": []}] -api_view = APIView(url_prefix="/api/v1/", view_tags=[Tag(name="book")], view_security=security) -api_view2 = APIView(doc_ui=False) -api_view_no_url = APIView(view_tags=[Tag(name="book")], view_security=security) +api_view = APIView(url_prefix="/api") + +servers = [{"url": "https://www.openapis.org/", "description": "openapi"}] +external_docs = {"url": "https://www.openapis.org/", "description": "Something great got better, get excited!"} +tags = [{"name": "book", "description": "book description"}] + + +class ErrorModel(BaseModel): + code: int + message: str class BookPath(BaseModel): id: int = Field(..., description="book ID") - name: str class BookQuery(BaseModel): @@ -32,16 +34,22 @@ class BookBody(BaseModel): @api_view.route("/book") class BookListAPIView: - a = 1 - - @api_view.doc(summary="get book list", responses={204: None}, doc_ui=False) + @api_view.doc( + summary="get book list", + description="Book description", + external_docs=external_docs, + operation_id="get_book_list", + deprecated=False, + security=JWT, + servers=servers, + tags=tags, + responses={"422": ErrorModel}, + ) def get(self, query: BookQuery): - print(self.a) return query.model_dump_json() @api_view.doc(summary="create book") def post(self, body: BookBody): - """description for a created book""" return body.model_dump_json() @@ -49,36 +57,18 @@ def post(self, body: BookBody): class BookAPIView: @api_view.doc(summary="get book") def get(self, path: BookPath): - print(path) - return "get" + return path.model_dump() - @api_view.doc(summary="update book", operation_id="update") + @api_view.doc(summary="update book") def put(self, path: BookPath): - print(path) - return "put" - - @api_view.doc(summary="delete book", deprecated=True) - def delete(self, path: BookPath): - print(path) - return "delete" - - -@api_view2.route("//book2/") -class BookAPIView2: - @api_view2.doc(summary="get book") - def get(self, path: BookPath): return path.model_dump() - -@api_view_no_url.route("/book3") -class BookAPIViewNoUrl: - @api_view_no_url.doc(summary="get book3") - def get(self, path: BookPath): + @api_view.doc(summary="delete book") + def delete(self, path: BookPath): return path.model_dump() app.register_api_view(api_view) -app.register_api_view(api_view2) @pytest.fixture @@ -88,55 +78,41 @@ def client(): return client -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json == app.api_doc - assert resp.json["paths"]["/api/v1/{name}/book/{id}"]["put"]["operationId"] == "update" - assert ( - resp.json["paths"]["/api/v1/{name}/book/{id}"]["delete"]["operationId"] == "BookAPIView_delete_book__id__delete" - ) - - def test_get_list(client): - resp = client.get("/api/v1/name1/book") + resp = client.get("/api/book") assert resp.status_code == 200 def test_post(client): - resp = client.post("/api/v1/name1/book", json={"age": 3}) + resp = client.post("/api/book", json={"age": 3}) assert resp.status_code == 200 def test_put(client): - resp = client.put("/api/v1/name1/book/1", json={"age": 3}) + resp = client.put("/api/book/1", json={"age": 3}) assert resp.status_code == 200 def test_get(client): - resp = client.get("/api/v1/name1/book/1") - assert resp.status_code == 200 - - resp = client.get("/name2/book2/1") + resp = client.get("/api/book/1") assert resp.status_code == 200 def test_delete(client): - resp = client.delete("/api/v1/name1/book/1") + resp = client.delete("/api/book/1") assert resp.status_code == 200 -def create_app(): - app = OpenAPI(__name__, info=info, security_schemes=security_schemes) - app.register_api_view(api_view, url_prefix="/api/1.0") - app.register_api_view(api_view_no_url, url_prefix="/api/1.0") +def register_api_view(): + _app = OpenAPI(__name__) + _app.register_api_view(api_view, url_prefix="/api/1.0") -# Invoke twice to ensure that call is idempotent -create_app() -create_app() +def test_register_api_view(): + # Invoke twice to ensure that call is idempotent + register_api_view() + register_api_view() def test_register_api_view_idempotency(): - assert list(api_view.paths.keys()) == ["/api/1.0/api/v1/{name}/book", "/api/1.0/api/v1/{name}/book/{id}"] - assert list(api_view_no_url.paths.keys()) == ["/api/1.0/book3"] + assert list(api_view.paths.keys()) == ["/api/1.0/book", "/api/1.0/book/{id}"] diff --git a/tests/test_api_view_args.py b/tests/test_api_view_args.py index 1e3af3b5..b44124f1 100644 --- a/tests/test_api_view_args.py +++ b/tests/test_api_view_args.py @@ -6,7 +6,7 @@ app = OpenAPI(__name__) app.config["TESTING"] = True -api_view = APIView(url_prefix="/api/v1") +api_view = APIView(url_prefix="/api/v1", doc_ui=False) class BookPath(BaseModel): @@ -29,8 +29,8 @@ def __init__(self, view_kwargs=None): self.b = view_kwargs.get("b") @api_view.doc(summary="get book list") - async def get(self, path: BookPath): - print(path) + def get(self, path: BookPath): + assert path.id == 1 return {"b": self.b} diff --git a/tests/test_async.py b/tests/test_async.py index 4cd3f956..47500b7e 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -55,12 +55,6 @@ def client(): return client -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json == app.api_doc - - def test_get_openapi(client): resp = client.get("/open/api?q=1") assert resp.status_code == 200 diff --git a/tests/test_body.py b/tests/test_body.py new file mode 100644 index 00000000..ad988b2e --- /dev/null +++ b/tests/test_body.py @@ -0,0 +1,48 @@ +import pytest +from pydantic import BaseModel + +from flask_openapi import OpenAPI + +app = OpenAPI(__name__) +app.config["TESTING"] = True + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +class AuthorModel(BaseModel): + name: str + age: int + + +class BookModel(BaseModel): + name: str + authors: list[AuthorModel] | None = None + files: list[str] + + +@app.post("/book") +def create_book(body: BookModel): + return body.model_dump() + + +def test_post(client): + data = {"name": "test", "files": ["file1", "file2"]} + response = client.post("/book", json=data) + assert response.status_code == 200 + assert response.json == {"authors": None, "files": ["file1", "file2"], "name": "test"} + + +def test_post_with_error_json(client): + error_json = '{"aaa:111}'.encode("utf8") + response = client.post("/book", data=error_json) + assert response.status_code == 422 + + +def test_str_body(client): + resp = client.post("/book", json='{"authors":null,"files":["file1","file2"],"name":"test"}') + assert resp.status_code == 200 diff --git a/tests/test_cookie.py b/tests/test_cookie.py new file mode 100644 index 00000000..b81d6b7c --- /dev/null +++ b/tests/test_cookie.py @@ -0,0 +1,53 @@ +from enum import Enum + +import pytest +from pydantic import BaseModel, Field + +from flask_openapi import OpenAPI + +app = OpenAPI(__name__) + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +class TypeEnum(str, Enum): + A = "A" + B = "B" + + +class AuthorModel(BaseModel): + name: str + age: int + + +class BookCookie(BaseModel): + name: str = Field( + None, + description="Name", + deprecated=True, + json_schema_extra={ + "example": 1, + "examples": {"example1": {"value": 1}, "example2": {"value": 2}}, + }, + ) + authors: list[AuthorModel] | None = None + token: str | None = None + token_type: TypeEnum | None = None + + +@app.post("/cookie") +def post_cookie(cookie: BookCookie): + return cookie.model_dump(by_alias=True) + + +def test_cookie(client): + client.set_cookie("token", "xxx") + client.set_cookie("token_type", "A") + r = client.post("/cookie") + assert r.status_code == 200 + assert r.json == {"authors": None, "name": None, "token": "xxx", "token_type": "A"} diff --git a/tests/test_default_query.py b/tests/test_default_query.py deleted file mode 100644 index 84f102f5..00000000 --- a/tests/test_default_query.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest -from pydantic import BaseModel, Field - -from flask_openapi import Info, OpenAPI - -info = Info(title="book API", version="1.0.0") - -app = OpenAPI(__name__, info=info) -app.config["TESTING"] = True - - -class BookQuery(BaseModel): - page: int = Field(1, description="current page") - page_size: int = Field(15, description="size of per page") - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -@app.get("/book") -def get_book(query: BookQuery): - print(query) - return {"code": 0, "message": "ok"} - - -def test_get(client): - resp = client.get("/book?page=2") - print(resp.json) - assert resp.status_code == 200 diff --git a/tests/test_empty_body.py b/tests/test_empty_body.py deleted file mode 100644 index a3a8bcad..00000000 --- a/tests/test_empty_body.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest -from pydantic import BaseModel - -from flask_openapi import Info, OpenAPI - -info = Info(title="book API", version="1.0.0") - -app = OpenAPI(__name__, info=info) -app.config["TESTING"] = True - - -class CreateBookBody(BaseModel): - pass - - model_config = { - "extra": "allow", - } - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -@app.post("/book") -def create_book(body: CreateBookBody): - print(body.model_dump()) - return {"code": 0, "message": "ok"} - - -def test_post(client): - resp = client.post("/book", json={"aaa": 111, "bbb": 222}) - print(resp.json) - assert resp.status_code == 200 diff --git a/tests/test_enum.py b/tests/test_enum.py deleted file mode 100644 index 227d97b8..00000000 --- a/tests/test_enum.py +++ /dev/null @@ -1,42 +0,0 @@ -from enum import Enum - -import pytest -from pydantic import BaseModel, Field - -from flask_openapi import Info, OpenAPI - -app = OpenAPI(__name__, info=Info(title="Enum demo", version="1.0.0")) - -app.config["TESTING"] = True - - -class Language(str, Enum): - cn = "Chinese" - en = "English" - - -class LanguagePath(BaseModel): - language: Language = Field(..., description="Language") - - -@app.get("/") -def get_enum(path: LanguagePath): - print(path) - return {} - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - _json = resp.json - assert resp.status_code == 200 - assert _json["components"]["schemas"].get("Language") is not None - - resp = client.get("/English") - assert resp.status_code == 200 diff --git a/tests/test_form.py b/tests/test_form.py index ec3aa5b5..3576a46a 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -102,8 +102,7 @@ def test_openapi(client): "number_list": ["3.4", "5.6"], "obj": '{"a": 2}', "parameter": '{"tag": "string"}', - "parameter_dict": '{"additionalProp1": {"tag": "string"}, "additionalProp2": {"tag": "string"},' - '"additionalProp3": {"tag": "string"}}', + "parameter_dict": '{"additionalProp1": {"tag": "string"}, "additionalProp2": {"tag": "string"},"additionalProp3": {"tag": "string"}}', "parameter_list": ['{"tag": "string"}', '{"tag": "string"}'], "parameter_list_union": ["ok", '{"tag": "string"}', "7.8"], "parameter_union": '{"tag2": "string"}', @@ -112,7 +111,6 @@ def test_openapi(client): "string_list": ["a", "b", "c"], } resp = client.post("/example", data=data, content_type="multipart/form-data") - print(resp.text) assert resp.status_code == 200 diff --git a/tests/test_header.py b/tests/test_header.py new file mode 100644 index 00000000..c9272dd2 --- /dev/null +++ b/tests/test_header.py @@ -0,0 +1,91 @@ +from enum import Enum + +import pytest +from pydantic import BaseModel, Field + +from flask_openapi import OpenAPI + +app = OpenAPI(__name__) + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +class TypeEnum(str, Enum): + A = "A" + B = "B" + + +class AuthorModel(BaseModel): + name: str + age: int + + +class BookHeader(BaseModel): + name: str = Field( + None, + description="Name", + deprecated=True, + json_schema_extra={ + "example": 1, + "examples": {"example1": {"value": 1}, "example2": {"value": 2}}, + }, + ) + authors: list[AuthorModel] | None = None + Hello1: str = Field("what's up", max_length=12, description="sds") + # required + hello2: str = Field(..., max_length=12, description="sds") + api_key: str = Field(..., description="API Key") + api_type: TypeEnum | None = None + x_hello: str = Field(..., max_length=12, description="Header with alias to support dash", alias="x-hello") + null: None = None + + +class BookHeaderPopulateByName(BookHeader): + model_config = {"populate_by_name": True} + + +@app.get("/header") +def get_book(header: BookHeader): + return header.model_dump(by_alias=True) + + +@app.get("/header/populate_by_name") +def get_book_populate_by_name(header: BookHeaderPopulateByName): + return header.model_dump(by_alias=True) + + +def test_header(client): + headers = {"Hello1": "111", "hello2": "222", "api_key": "333", "api_type": "A", "x-hello": "444"} + resp = client.get("/header", headers=headers) + assert resp.status_code == 200 + assert resp.json == { + "Hello1": "111", + "api_key": "333", + "api_type": "A", + "authors": None, + "hello2": "222", + "name": None, + "null": None, + "x-hello": "444", + } + + +def test_header_populate_by_name(client): + headers = {"Hello1": "111", "hello2": "222", "api_key": "333", "api_type": "A", "x-hello": "444"} + resp = client.get("/header/populate_by_name", headers=headers) + assert resp.status_code == 200 + assert resp.json == { + "Hello1": "111", + "api_key": "333", + "api_type": "A", + "authors": None, + "hello2": "222", + "name": None, + "null": None, + "x-hello": "444", + } diff --git a/tests/test_list_with_default_value.py b/tests/test_list_with_default_value.py deleted file mode 100644 index 171823ad..00000000 --- a/tests/test_list_with_default_value.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from pydantic import BaseModel - -from flask_openapi import OpenAPI - -app = OpenAPI(__name__) -app.config["TESTING"] = True - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -class BookQuery(BaseModel): - age: list[int] = [1, 2] - - -class BookForm(BaseModel): - age: list[float] = [3, 4] - - -@app.get("/query") -def api_query(query: BookQuery): - assert query.age == [1, 2] - return {"code": 0, "message": "ok"} - - -@app.post("/form") -def api_form(form: BookForm): - assert form.age == [3, 4] - return {"code": 0, "message": "ok"} - - -def test_query(client): - client.get("/query") - - -def test_form(client): - client.post("/form") diff --git a/tests/test_model_config.py b/tests/test_model_config.py index 4466fdd7..71a94506 100644 --- a/tests/test_model_config.py +++ b/tests/test_model_config.py @@ -1,54 +1,12 @@ import pytest from pydantic import BaseModel, Field -from flask_openapi import FileStorage, OpenAPI +from flask_openapi import OpenAPI app = OpenAPI(__name__) app.config["TESTING"] = True -class UploadFilesForm(BaseModel): - file: FileStorage - str_list: list[str] - - model_config = dict( - openapi_extra={ - # "example": {"a": 123}, - "examples": { - "Example 01": { - "summary": "An example", - "value": {"file": "Example-01.jpg", "str_list": ["a", "b", "c"]}, - }, - "Example 02": {"summary": "Another example", "value": {"str_list": ["1", "2", "3"]}}, - } - } - ) - - -class BookBody(BaseModel): - age: int - author: str - - model_config = dict( - openapi_extra={ - "description": "This is post RequestBody", - "example": {"age": 12, "author": "author1"}, - "examples": { - "example1": { - "summary": "example summary1", - "description": "example description1", - "value": {"age": 24, "author": "author2"}, - }, - "example2": { - "summary": "example summary2", - "description": "example description2", - "value": {"age": 48, "author": "author3"}, - }, - }, - } - ) - - class MessageResponse(BaseModel): message: str = Field(..., description="The message") metadata: dict[str, str] = Field(alias="metadata_") @@ -56,14 +14,8 @@ class MessageResponse(BaseModel): model_config = dict(by_alias=False) -@app.post("/form") -def api_form(form: UploadFilesForm): - print(form) # pragma: no cover - - @app.post("/body", responses={"200": MessageResponse}) -def api_error_json(body: BookBody): - print(body) # pragma: no cover +def api_error_json(): ... @pytest.fixture @@ -77,17 +29,4 @@ def test_openapi(client): resp = client.get("/openapi/openapi.json") _json = resp.json assert resp.status_code == 200 - # assert _json["paths"]["/form"]["post"]["requestBody"]["content"]["multipart/form-data"]["examples"] == { - # "Example 01": {"summary": "An example", "value": {"file": "Example-01.jpg", "str_list": ["a", "b", "c"]}}, - # "Example 02": {"summary": "Another example", "value": {"str_list": ["1", "2", "3"]}}, - # } - # assert _json["paths"]["/body"]["post"]["requestBody"]["description"] == "This is post RequestBody" - # assert _json["paths"]["/body"]["post"]["requestBody"]["content"]["application/json"]["example"] == { - # "age": 12, - # "author": "author1", - # } - # assert _json["paths"]["/body"]["post"]["responses"]["200"]["content"]["application/json"]["examples"] == { - # "example1": {"summary": "example1 summary", "value": {"message": "bbb"}}, - # "example2": {"summary": "example2 summary", "value": {"message": "ccc"}}, - # } assert _json["components"]["schemas"]["MessageResponse"]["properties"].get("metadata") is not None diff --git a/tests/test_model_extra.py b/tests/test_model_extra.py deleted file mode 100644 index 98319c89..00000000 --- a/tests/test_model_extra.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest -from pydantic import BaseModel, ConfigDict, Field - -from flask_openapi import OpenAPI - -app = OpenAPI(__name__) -app.config["TESTING"] = True - - -class BookQuery(BaseModel): - age: int | None = Field(None, description="Age") - - model_config = ConfigDict(extra="allow") - - -class BookForm(BaseModel): - string: str - - model_config = ConfigDict(extra="forbid") - - -class BookHeader(BaseModel): - api_key: str = Field(..., description="API Key") - - model_config = ConfigDict(extra="forbid") - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -@app.get("/book") -def get_books(query: BookQuery): - """get books - to get all books - """ - assert query.age == 3 - assert query.author == "joy" - return {"code": 0, "message": "ok"} - - -@app.post("/form") -def api_form(form: BookForm): - print(form) - return {"code": 0, "message": "ok"} - - -def test_query(client): - resp = client.get("/book?age=3&author=joy") - assert resp.status_code == 200 - - -@app.get("/header") -def get_book(header: BookHeader): - return header.model_dump(by_alias=True) - - -def test_form(client): - data = {"string": "a", "string_list": ["a", "b", "c"]} - r = client.post("/form", data=data, content_type="multipart/form-data") - assert r.status_code == 422 - - -def test_header(client): - headers = {"Hello1": "111", "hello2": "222", "api_key": "333", "api_type": "A", "x-hello": "444"} - resp = client.get("/header", headers=headers) - assert resp.status_code == 422 diff --git a/tests/test_number_constraints.py b/tests/test_number_constraints.py deleted file mode 100644 index edd355e7..00000000 --- a/tests/test_number_constraints.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -from pydantic import BaseModel, Field - -from flask_openapi import OpenAPI - -app = OpenAPI(__name__) -app.config["TESTING"] = True - - -class MyModel(BaseModel): - num_1: int = Field(..., ge=1, le=10) - num_2: int = Field(..., gt=1, lt=10) - - -@app.post("/book") -def create_book(body: MyModel): - print(body) # pragma: no cover - - -@pytest.fixture -def client(): - client = app.test_client() - return client - - -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json == app.api_doc - - model_props = resp.json["components"]["schemas"]["MyModel"]["properties"] - num_1_props = model_props["num_1"] - num_2_props = model_props["num_2"] - - assert num_1_props["minimum"] == 1 - assert num_1_props["maximum"] == 10 - assert num_2_props["exclusiveMinimum"] == 1 - assert num_2_props["exclusiveMaximum"] == 10 diff --git a/tests/test_openapi.py b/tests/test_openapi.py index ac16d3e0..b00f73f1 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1,512 +1,87 @@ from __future__ import annotations -from typing import Generic, Literal, TypeVar +import pytest +from pydantic import BaseModel -from pydantic import BaseModel, Field +from flask_openapi import APIBlueprint, OpenAPI, ValidationErrorModel +from tests.config import JWT -from flask_openapi import OpenAPI, Schema +servers = [{"url": "https://www.openapis.org/", "description": "openapi"}] +external_docs = {"url": "https://www.openapis.org/", "description": "Something great got better, get excited!"} +tags = [{"name": "book", "description": "book description"}] -T = TypeVar("T", bound=BaseModel) +class ErrorModel(BaseModel): + code: int + message: str -def test_responses_are_replicated_in_open_api(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - class BaseResponse(BaseModel): - """Base description""" +class NewValidationErrorModel(ValidationErrorModel): + error: ErrorModel | None = None - test: int - model_config = dict( - openapi_extra={ - "description": "Custom description", - "headers": {"location": {"description": "URL of the new resource", "schema": {"type": "string"}}}, - "content": {"text/plain": {"schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } - ) +app = OpenAPI( + __name__, + info={"title": "openapi", "version": "2.0"}, + servers=servers, + external_docs=external_docs, + validation_error_model=NewValidationErrorModel, +) - @test_app.get("/test", responses={"201": BaseResponse}) - def endpoint_test(): - return b"", 201 # pragma: no cover +api = APIBlueprint("/book", __name__, abp_tags=[{"name": "api name"}], url_prefix="/api") - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - # assert resp.json["paths"]["/test"]["get"]["responses"]["201"] == { - # "description": "Custom description", - # "headers": {"location": {"description": "URL of the new resource", "schema": {"type": "string"}}}, - # "content": { - # # This content is coming from responses - # "application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}, - # # While this one comes from responses - # "text/plain": {"schema": {"type": "string"}}, - # }, - # "links": {"dummy": {"description": "dummy link"}}, - # } +@pytest.fixture +def client(): + client = app.test_client() -def test_none_responses_are_replicated_in_open_api(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True + return client - @test_app.get( - "/test", - responses={ - "204": { - "description": "Custom description", - "headers": {"x-my-special-header": {"description": "Custom header", "schema": {"type": "string"}}}, - "content": {"text/plain": {"schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } - }, - ) - def endpoint_test(): - return b"", 204 # pragma: no cover - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["get"]["responses"]["204"] == { - "description": "Custom description", - "headers": {"x-my-special-header": {"description": "Custom header", "schema": {"type": "string"}}}, - "content": {"text/plain": {"schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } +@app.get( + "/book1", + doc_ui=False, +) +def get_book1(): ... -def test_responses_are_replicated_in_open_api2(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True +@app.post( + "/book2", + summary="Book2", + description="Book description", + external_docs=external_docs, + deprecated=False, + security=JWT, + servers=servers, + tags=tags, + responses={"422": ErrorModel}, +) +def get_book2(): ... - @test_app.get( - "/test", - responses={ - "201": { - "description": "Custom description", - "headers": {"location": {"description": "URL of the new resource", "schema": {"type": "string"}}}, - "content": {"text/plain": {"schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } - }, - ) - def endpoint_test(): - return b"", 201 # pragma: no cover - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["get"]["responses"]["201"] == { - "description": "Custom description", - "headers": {"location": {"description": "URL of the new resource", "schema": {"type": "string"}}}, - "content": {"text/plain": {"schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } +@api.get( + "/book1", + doc_ui=False, +) +def get_api_book1(): ... -def test_responses_without_content_are_replicated_in_open_api(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True +@api.get( + "/book2", + external_docs=external_docs, + deprecated=False, + security=JWT, + servers=servers, + tags=tags, + responses={"422": ErrorModel}, +) +def get_api_book2(): ... - @test_app.get( - "/test", - responses={ - "201": { - "description": "Custom description", - "headers": {"location": {"description": "URL of the new resource", "schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } - }, - ) - def endpoint_test(): - return b"", 201 # pragma: no cover - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["get"]["responses"]["201"] == { - "description": "Custom description", - "headers": {"location": {"description": "URL of the new resource", "schema": {"type": "string"}}}, - "links": {"dummy": {"description": "dummy link"}}, - } +app.register_api(api) -class BaseRequest(BaseModel): - """Base description""" - - test_int: int - test_str: str - - -class BaseRequestGeneric(BaseModel, Generic[T]): - detail: T - - model_config = dict( - openapi_extra={ - "examples": { - "Example 01": { - "summary": "An example", - "value": { - "test_int": -1, - "test_str": "negative", - }, - }, - "Example 02": {"externalValue": "https://example.org/examples/second-example.xml"}, - "Example 03": {"$ref": "#/components/examples/third-example"}, - } - } - ) - - -def test_body_examples_are_replicated_in_open_api(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(body: BaseRequestGeneric[BaseRequest]): - return body.model_dump(), 200 - - with test_app.test_client() as client: - client.post("/test", json={"detail": {"test_int": 1, "test_str": "s"}}) - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - # assert resp.json["paths"]["/test"]["post"]["requestBody"] == { - # "content": { - # "application/json": { - # "examples": { - # "Example 01": {"summary": "An example", "value": {"test_int": -1, "test_str": "negative"}}, - # "Example 02": {"externalValue": "https://example.org/examples/second-example.xml"}, - # "Example 03": {"$ref": "#/components/examples/third-example"}, - # }, - # "schema": {"$ref": "#/components/schemas/BaseRequestGeneric_BaseRequest_"}, - # } - # }, - # "required": True, - # } - assert resp.json["components"]["schemas"]["BaseRequestGeneric_BaseRequest_"] == { - "properties": {"detail": {"$ref": "#/components/schemas/BaseRequest"}}, - "required": ["detail"], - "title": "BaseRequestGeneric[BaseRequest]", - "type": "object", - } - - -def test_form_examples(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - model_config = dict( - openapi_extra={ - "examples": { - "Example 01": { - "summary": "An example", - "value": { - "test_int": -1, - "test_str": "negative", - }, - } - } - } - ) - BaseRequestGeneric[BaseRequest].model_config = model_config - - @test_app.post("/test") - def endpoint_test(form: BaseRequestGeneric[BaseRequest]): - return form.model_dump(), 200 # pragma: no cover - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - # assert resp.json["paths"]["/test"]["post"]["requestBody"] == { - # "content": { - # "multipart/form-data": { - # "schema": {"$ref": "#/components/schemas/BaseRequestGeneric_BaseRequest_"}, - # "examples": { - # "Example 01": {"summary": "An example", "value": {"test_int": -1, "test_str": "negative"}} - # }, - # } - # }, - # "required": True, - # } - assert resp.json["components"]["schemas"]["BaseRequestGeneric_BaseRequest_"] == { - "properties": {"detail": {"$ref": "#/components/schemas/BaseRequest"}}, - "required": ["detail"], - "title": "BaseRequestGeneric[BaseRequest]", - "type": "object", - } - - -class BaseRequestBody(BaseModel): - base: BaseRequest - - -def test_body_with_complex_object(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(body: BaseRequestBody): - return body.model_dump(), 200 # pragma: no cover - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert {"properties", "required", "title", "type"} == set( - resp.json["components"]["schemas"]["BaseRequestBody"].keys() - ) - - -class Detail(BaseModel): - num: int - - -class GenericResponse(BaseModel, Generic[T]): - detail: T - - -class ListGenericResponse(BaseModel, Generic[T]): - items: list[GenericResponse[T]] - - -def test_responses_with_generics(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.get("/test", responses={"201": ListGenericResponse[Detail]}) - def endpoint_test(): - return b"", 201 # pragma: no cover - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["get"]["responses"]["201"] == { - "description": "Created", - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/ListGenericResponse_Detail_"}}, - }, - } - - schemas = resp.json["components"]["schemas"] - detail = schemas["ListGenericResponse_Detail_"] - assert detail["title"] == "ListGenericResponse[Detail]" - assert detail["properties"]["items"]["items"]["$ref"] == "#/components/schemas/GenericResponse_Detail_" - assert schemas["GenericResponse_Detail_"]["title"] == "GenericResponse[Detail]" - - -class PathParam(BaseModel): - type_name: str = Field( - ..., description="id for path", max_length=300, json_schema_extra={"deprecated": False, "example": "42"} - ) - - -def test_path_parameter_object(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(path: PathParam): - return path.model_dump(), 200 # pragma: no cover - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["post"]["parameters"][0] == { - "deprecated": False, - "description": "id for path", - "example": "42", - "in": "path", - "name": "type_name", - "required": True, - "schema": { - "deprecated": False, - "description": "id for path", - "maxLength": 300, - "example": "42", - "title": "Type Name", - "type": "string", - }, - } - - -class QueryParam(BaseModel): - count: int = Field( - ..., description="count of param", le=1000.0, json_schema_extra={"deprecated": True, "example": 100} - ) - - -def test_query_parameter_object(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(query: QueryParam): - return query.model_dump(), 200 # pragma: no cover - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["post"]["parameters"][0] == { - "deprecated": True, - "description": "count of param", - "example": 100, - "in": "query", - "name": "count", - "required": True, - "schema": { - "deprecated": True, - "description": "count of param", - "maximum": 1000.0, - "example": 100, - "title": "Count", - "type": "integer", - }, - } - - -class HeaderParam(BaseModel): - app_name: str = Field(None, description="app name") - - -class CookieParam(BaseModel): - app_name: str = Field(None, description="app name", json_schema_extra={"example": "aaa"}) - - -def test_header_parameter_object(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(header: HeaderParam, cookie: CookieParam): - print(header, cookie) # pragma: no cover - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - assert resp.json["paths"]["/test"]["post"]["parameters"][0] == { - "description": "app name", - "in": "header", - "name": "app_name", - "required": False, - "schema": {"description": "app name", "title": "App Name", "type": "string", "default": None}, - } - assert resp.json["paths"]["/test"]["post"]["parameters"][1] == { - "description": "app name", - "in": "cookie", - "example": "aaa", - "name": "app_name", - "required": False, - "schema": { - "description": "app name", - "example": "aaa", - "title": "App Name", - "type": "string", - "default": None, - }, - } - - -class Model(BaseModel): - one: int | None = Field(default=None) - two: int | None = Field(default=2) - - -def test_default_none(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(body: Model): - print([]) # pragma: no cover - - works = Model.model_json_schema()["properties"] - assert works["one"]["default"] is None - assert works["two"]["default"] == 2 - breaks = test_app.api_doc["components"]["schemas"]["Model"]["properties"] - assert breaks["two"]["default"] == 2 - assert breaks["one"]["default"] is None - - -def test_parameters_none(request): - """Parameters key shouldn't exist.""" - test_app = OpenAPI(request.node.name) - - @test_app.post("/test") - def endpoint_test(): - print([]) # pragma: no cover - - data = test_app.api_doc["paths"]["/test"]["post"] - - assert "parameters" not in data - - -def test_deprecated_none(request): - """Parameters key shouldn't exist.""" - test_app = OpenAPI(request.node.name) - - @test_app.post("/test") - def endpoint_test(): - print([]) # pragma: no cover - - data = test_app.api_doc["paths"]["/test"]["post"] - - assert "deprecated" not in data - - -class TupleModel(BaseModel): - my_tuple: tuple[Literal["a", "b"], Literal["c", "d"]] - - -def test_prefix_items(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - @test_app.post("/test") - def endpoint_test(body: TupleModel): - print([]) # pragma: no cover - - schema = test_app.api_doc["paths"]["/test"]["post"]["requestBody"]["content"]["application/json"]["schema"] - assert schema == {"$ref": "#/components/schemas/TupleModel"} - components = test_app.api_doc["components"]["schemas"] - assert components["TupleModel"] == { - "properties": { - "my_tuple": { - "maxItems": 2, - "minItems": 2, - "prefixItems": [{"enum": ["a", "b"], "type": "string"}, {"enum": ["c", "d"], "type": "string"}], - "title": "My Tuple", - "type": "array", - } - }, - "required": ["my_tuple"], - "title": "TupleModel", - "type": "object", - } - - -def test_schema_bigint(request): - max_nr = 9223372036854775807 - obj = Schema(maximum=max_nr) - assert obj.model_dump()["maximum"] == max_nr - - -def test_convert_literal_with_single_value_to_const(request): - test_app = OpenAPI(request.node.name) - test_app.config["TESTING"] = True - - class MyResponse(BaseModel): - foobar: Literal["baz"] - - @test_app.post("/test", responses={200: MyResponse}) - def endpoint_test(): - print("do nothing") - - with test_app.test_client() as client: - resp = client.get("/openapi/openapi.json") - assert resp.status_code == 200 - print("###", resp.json) - assert resp.json["components"]["schemas"]["MyResponse"]["properties"]["foobar"] == { - "const": "baz", - "title": "Foobar", - "type": "string", - } +def test_openapi(client): + response = client.get("/openapi/openapi.json") + assert response.status_code == 200 + client.get("/openapi/openapi.json") diff --git a/tests/test_openapi_with_doc_prefix.py b/tests/test_openapi_with_doc_prefix.py new file mode 100644 index 00000000..d39cceec --- /dev/null +++ b/tests/test_openapi_with_doc_prefix.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import pytest + +from flask_openapi import OpenAPI + +doc_prefix = "/v1/openapi" +app = OpenAPI(__name__, doc_prefix=doc_prefix) +app.config["TESTING"] = True + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +def test_openapi(client): + resp = client.get(f"{doc_prefix}/openapi.json") + assert resp.status_code == 200 + assert resp.json == app.api_doc diff --git a/tests/test_path.py b/tests/test_path.py new file mode 100644 index 00000000..e46ac386 --- /dev/null +++ b/tests/test_path.py @@ -0,0 +1,49 @@ +from enum import Enum + +import pytest +from pydantic import BaseModel, Field + +from flask_openapi import Info, OpenAPI + +app = OpenAPI(__name__, info=Info(title="Enum demo", version="1.0.0")) + +app.config["TESTING"] = True + + +@pytest.fixture +def client(): + client = app.test_client() + return client + + +class Language(str, Enum): + cn = "Chinese" + en = "English" + + +class BookPath(BaseModel): + name: Language = Field( + ..., + description="Name", + deprecated=True, + json_schema_extra={ + "example": 1, + "examples": {"example1": {"value": 1}, "example2": {"value": 2}}, + }, + ) + + +@app.get("/book/") +def get_path(path: BookPath): + return path.model_dump(by_alias=True) + + +def test_path(client): + r = client.get("/openapi/openapi.json") + _json = r.json + assert r.status_code == 200 + assert _json["components"]["schemas"].get("Language") is not None + + r = client.get("/book/English") + assert r.status_code == 200 + assert r.json == {"name": "English"} diff --git a/tests/test_pydantic_calculated_fields.py b/tests/test_pydantic_calculated_fields.py index 6c7b0efa..5a675c75 100644 --- a/tests/test_pydantic_calculated_fields.py +++ b/tests/test_pydantic_calculated_fields.py @@ -33,7 +33,4 @@ def get_book(): def test_openapi(client): resp = client.get("/openapi/openapi.json") - import pprint - - pprint.pprint(resp.json) assert resp.json["components"]["schemas"]["User"]["properties"].get("display_name") is not None diff --git a/tests/test_pydantic_custom_root_types.py b/tests/test_pydantic_custom_root_types.py index e92336e2..4ea5f182 100644 --- a/tests/test_pydantic_custom_root_types.py +++ b/tests/test_pydantic_custom_root_types.py @@ -41,25 +41,21 @@ def client(): @app.post("/api/v1/sellouts", tags=[Tag(name="Sellout", description="Loren.")], responses={"200": SelloutList}) def post_sellout(body: SelloutList): - print(body) return body.model_dump_json() @app.post("/api/v2/sellouts", tags=[Tag(name="Sellout", description="Loren.")], responses={"200": SelloutDict}) def post_sellout2(body: SelloutDict): - print(body) return body.model_dump_json() @app.post("/api/v3/sellouts", tags=[Tag(name="Sellout", description="Loren.")]) def post_sellout3(body: SelloutDict2): - print(body) return body.model_dump_json() @app.post("/api/v4/sellouts", tags=[Tag(name="Sellout", description="Loren.")]) def post_sellout4(body: SelloutDict3): - print(body) return body.model_dump_json() diff --git a/tests/test_pydantic_validation_error.py b/tests/test_pydantic_validation_error.py index 98135ddc..2376c812 100644 --- a/tests/test_pydantic_validation_error.py +++ b/tests/test_pydantic_validation_error.py @@ -20,7 +20,7 @@ class LoginRequest(BaseModel): @app.post("/login") def login(body: LoginRequest): - return {"message": f"Login successful for {body.email}"} + return {"message": f"Login successful for {body.email}"} # pragma: no cover def test_pydantic_validation_error_schema(client): diff --git a/tests/test_query.py b/tests/test_query.py new file mode 100644 index 00000000..a6cddfa7 --- /dev/null +++ b/tests/test_query.py @@ -0,0 +1,85 @@ +import pytest +from pydantic import BaseModel, Field + +from flask_openapi import OpenAPI + +app = OpenAPI(__name__) +app.config["TESTING"] = True + + +@pytest.fixture +def client(): + client = app.test_client() + + return client + + +class AuthorModel(BaseModel): + name: str + age: int + + +class BookModel(BaseModel): + name: str = Field( + ..., + description="Name", + deprecated=True, + json_schema_extra={ + "example": 1, + "examples": {"example1": {"value": 1}, "example2": {"value": 2}}, + }, + ) + authors: list[AuthorModel] | None = None + files: list[str] + optional_list: list[str] | None = None + null: None = None + alias_name: str = Field(None, alias="alias") + alias_list: list[str] = Field(None, alias="alias_list") + + model_config = {"extra": "allow"} + + +class BookPopulateByName(BookModel): + model_config = {"populate_by_name": True} + + +@app.get("/book") +def get_book(query: BookModel): + return query.model_dump() + + +@app.get("/book/populate_by_name") +def get_book_populate_by_name(query: BookPopulateByName): + return query.model_dump() + + +def test_get(client): + params = {"name": "test", "files": ["file1", "file2"], "alias": "alias", "alias_list": ["a", "b"], "extra": "extra"} + response = client.get("/book", query_string=params) + assert response.status_code == 200 + assert response.json == { + "alias_list": ["a", "b"], + "alias_name": "alias", + "authors": None, + "extra": "extra", + "files": ["file1", "file2"], + "name": "test", + "null": None, + "optional_list": None, + } + + +def test_get_populate_by_name(client): + params = {"name": "test", "files": ["file1", "file2"], "alias": "alias", "alias_list": ["a", "b"], "extra": "extra"} + response = client.get("/book/populate_by_name", query_string=params) + assert response.status_code == 200 + assert response.json == { + "alias_list": ["a", "b"], + "alias_name": "alias", + "authors": None, + "extra": "extra", + "files": ["file1", "file2"], + "name": "test", + "null": None, + "optional_list": None, + } diff --git a/tests/test_request.py b/tests/test_request.py deleted file mode 100644 index 91bfdd9c..00000000 --- a/tests/test_request.py +++ /dev/null @@ -1,160 +0,0 @@ -from enum import Enum -from functools import wraps - -import pytest -from pydantic import BaseModel, Field - -from flask_openapi import FileStorage, OpenAPI - -app = OpenAPI(__name__) -app.config["TESTING"] = True - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -class TypeEnum(str, Enum): - A = "A" - B = "B" - - -class BookForm(BaseModel): - file: FileStorage - files: list[FileStorage] - string: str - string_list: list[str] - - -class BookQuery(BaseModel): - age: list[int] - book_type: TypeEnum | None = None - - -class BookQueryFilter(BaseModel): - age: list[int] - fields: list[str] | None = None - - -class BookBody(BaseModel): - age: int - - -class BookCookie(BaseModel): - token: str | None = None - token_type: TypeEnum | None = None - - -class BookHeader(BaseModel): - Hello1: str = Field("what's up", max_length=12, description="sds") - # required - hello2: str = Field(..., max_length=12, description="sds") - api_key: str = Field(..., description="API Key") - api_type: TypeEnum | None = None - x_hello: str = Field(..., max_length=12, description="Header with alias to support dash", alias="x-hello") - - -def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - return wrapper - - -@app.get("/query") -@decorator -def api_query(query: BookQuery): - print(query) - return {"code": 0, "message": "ok"} - - -@app.get("/filter-query") -@decorator -def api_filter_query(query: BookQueryFilter): - print(query) - return {"fields": query.fields, "message": "ok"} - - -@app.post("/form") -def api_form(form: BookForm): - print(form) - return {"code": 0, "message": "ok"} - - -@app.post("/body") -def api_error_json(body: BookBody): - print(body) # pragma: no cover - - -@app.get("/header") -def get_book(header: BookHeader): - return header.model_dump(by_alias=True) - - -@app.post("/cookie") -def api_cookie(cookie: BookCookie): - print(cookie) - return {"code": 0, "message": "ok"} - - -def test_query(client): - r = client.get("/query?age=1") - print(r.json) - assert r.status_code == 200 - - -def test_query_list(client): - r = client.get("/filter-query?age=1&fields=name&fields=age") - print(r.json) - assert r.status_code == 200 - assert r.json["fields"] == ["name", "age"] - - -def test_query_list_no_fields(client): - r = client.get("/filter-query?age=1") - print(r.json) - assert r.status_code == 200 - assert r.json["fields"] is None - - -def test_query_list_single_field(client): - r = client.get("/filter-query?age=1&fields=age") - print(r.json) - assert r.status_code == 200 - assert r.json["fields"] == ["age"] - - -def test_form(client): - from io import BytesIO - - data = { - "file": (BytesIO(b"post-data"), "filename"), - "files": [(BytesIO(b"post-data"), "filename"), (BytesIO(b"post-data"), "filename")], - "string": "a", - "string_list": ["a", "b", "c"], - } - r = client.post("/form", data=data, content_type="multipart/form-data") - assert r.status_code == 200 - - -def test_error_json(client): - r = client.post("/body", json="{age: 1}") - assert r.status_code == 422 - - -def test_cookie(client): - r = client.post("/cookie") - print(r.json) - assert r.status_code == 200 - - -def test_header(client): - headers = {"Hello1": "111", "hello2": "222", "api_key": "333", "api_type": "A", "x-hello": "444"} - resp = client.get("/header", headers=headers) - print(resp.json) - assert resp.status_code == 200 - assert resp.json == headers diff --git a/tests/test_response.py b/tests/test_response.py index 42ae980e..d22e740c 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,22 +1,13 @@ from http import HTTPStatus import pytest -from pydantic import BaseModel, Field +from pydantic import BaseModel -from flask_openapi import APIBlueprint, OpenAPI +from flask_openapi import APIBlueprint, OpenAPI, Response -app = OpenAPI(__name__) +app = OpenAPI(__name__, responses={200: Response(description="OK")}) app.config["TESTING"] = True -api = APIBlueprint("/api", __name__, url_prefix="/api") - - -class BookResponse(BaseModel): - code: int = Field(0, description="Status Code") - message: str = Field("ok", description="Exception Information") - - -class BookPath(BaseModel): - bid: int = Field(..., description="book id") +api = APIBlueprint("/api", __name__, url_prefix="/api", abp_responses={200: Response(description="API OK")}) @pytest.fixture @@ -26,31 +17,81 @@ def client(): return client -@app.get( - "/book/", +class AuthorModel(BaseModel): + name: str + age: int + + +class BookModel(BaseModel): + name: str + authors: list[AuthorModel] | None = None + + +@app.post("/book1") +def response1(body: BookModel): + return body.model_json_schema() + + +@app.post("/book2", responses={HTTPStatus.OK: BookModel}) +def response2(body: BookModel): + return body.model_json_schema() + + +@app.post( + "/book3", responses={ - HTTPStatus.OK: BookResponse, - "201": BookResponse, - 202: {"content": {"text/html": {"schema": {"type": "string"}}}}, - 204: None, - "422": {"description": "validation error"}, + "200": { + "description": "Custom OK", + "content": {"application/custom+json": {"schema": BookModel.model_json_schema()}}, + }, + "204": None, }, ) -def get_book(path: BookPath): - print(path) # pragma: no cover +def response3(body: BookModel): + return body.model_json_schema() + + +@api.post("/book4") +def response4(body: BookModel): + return body.model_json_schema() + +@api.post("/book5", responses={HTTPStatus.OK: BookModel}) +def response5(body: BookModel): + return body.model_json_schema() -@api.get("/book", responses={HTTPStatus.OK: BookResponse, "201": BookResponse, 204: None}) -def get_api_book(): - return {"code": 0, "message": "ok"} # pragma: no cover + +@api.post( + "/book6", + responses={ + "200": { + "description": "API OK", + "content": {"application/custom+json": {"schema": BookModel.model_json_schema()}}, + } + }, +) +def response6(body: BookModel): + return body.model_json_schema() app.register_api(api) -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - _json = resp.json - assert resp.status_code == 200 - assert _json["paths"]["/book/{bid}"]["get"]["responses"].keys() - ["200", "201", "202", "204"] == {"422"} - assert _json["paths"]["/api/book"]["get"]["responses"].keys() - ["200", "201", "202", "204"] == {"422"} +def test_response(client): + response = client.get("/openapi/openapi.json") + _json = response.json + assert _json["paths"]["/book1"]["post"]["responses"]["200"]["description"] == "OK" + assert _json["paths"]["/book2"]["post"]["responses"]["200"]["content"]["application/json"] is not None + assert _json["paths"]["/book3"]["post"]["responses"]["200"]["content"]["application/custom+json"] is not None + assert _json["paths"]["/api/book4"]["post"]["responses"]["200"]["description"] == "API OK" + assert _json["paths"]["/api/book5"]["post"]["responses"]["200"]["content"]["application/json"] is not None + assert _json["paths"]["/api/book6"]["post"]["responses"]["200"]["content"]["application/custom+json"] is not None + + +def test(client): + assert client.post("/book1", json={"name": "bob", "age": 3}).status_code == 200 + assert client.post("/book2", json={"name": "bob", "age": 3}).status_code == 200 + assert client.post("/book3", json={"name": "bob", "age": 3}).status_code == 200 + assert client.post("/api/book4", json={"name": "bob", "age": 3}).status_code == 200 + assert client.post("/api/book5", json={"name": "bob", "age": 3}).status_code == 200 + assert client.post("/api/book6", json={"name": "bob", "age": 3}).status_code == 200 diff --git a/tests/test_restapi.py b/tests/test_restapi.py index 3313824a..1f507360 100644 --- a/tests/test_restapi.py +++ b/tests/test_restapi.py @@ -1,222 +1,79 @@ from __future__ import annotations -import json -from http import HTTPStatus - import pytest -from flask import Response -from pydantic import BaseModel, Field, RootModel - -from flask_openapi import ExternalDocumentation, Info, OpenAPI, Tag - -info = Info(title="book API", version="1.0.0") - -jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} -security_schemes = {"jwt": jwt} - - -class NotFoundResponse(BaseModel): - code: int = Field(-1, description="Status Code") - message: str = Field("Resource not found!", description="Exception Information") - +from pydantic import BaseModel -def get_operation_id_for_path_callback(*, name: str, path: str, method: str) -> str: - print(name, path, method) - return name +from flask_openapi import OpenAPI - -app = OpenAPI( - __name__, - info=info, - security_schemes=security_schemes, - responses={"404": NotFoundResponse}, - operation_id_callback=get_operation_id_for_path_callback, -) +app = OpenAPI(__name__) app.config["TESTING"] = True -security = [{"jwt": []}] -book_tag = Tag(name="book", description="Book") - - -class BookQuery(BaseModel): - age: int | None = Field(None, description="Age") - - -class BookBody(BaseModel): - age: int | None = Field(..., ge=2, le=4, description="Age") - author: str = Field(None, min_length=2, max_length=4, description="Author") - - -class BookPath(BaseModel): - bid: int = Field(..., description="book id") +@pytest.fixture +def client(): + client = app.test_client() -class BookBodyWithID(BaseModel): - bid: int = Field(..., description="book id") - age: int | None = Field(None, ge=2, le=4, description="Age") - author: str = Field(None, min_length=2, max_length=4, description="Author") + return client -class BaseResponse(BaseModel): - code: int = Field(0, description="Status Code") - message: str = Field("ok", description="Exception Information") +class IdModel(BaseModel): + id: int -class BookListResponseV1(BaseResponse): - data: list[BookBodyWithID] = Field(..., description="All the books") +class BookModel(BaseModel): + name: str + age: int -class BookListResponseV2(BaseModel): - books: list[BookBodyWithID] = Field(...) +@app.get("/book") +def get_book(query: BookModel): + return query.model_dump() -class BookListResponseV3(RootModel): - root: list[BookBodyWithID] +@app.post("/book") +def create_book(body: BookModel): + return body.model_dump() -class BookResponse(BaseModel): - code: int = Field(0, description="Status Code") - message: str = Field("ok", description="Exception Information") - data: BookBodyWithID +@app.put("/book/") +def update_book(path: IdModel, body: BookModel): + return {"id": path.id, "name": body.name, "age": body.age} -@pytest.fixture -def client(): - client = app.test_client() - - return client +@app.patch("/book/") +def patch_book(path: IdModel, body: BookModel): + return {"id": path.id, "name": body.name, "age": body.age} -@app.get( - "/book/", - tags=[book_tag], - operation_id="get_book_id", - external_docs=ExternalDocumentation( - url="https://www.openapis.org/", description="Something great got better, get excited!" - ), - responses={"200": BookResponse}, - security=security, -) -def get_book(path: BookPath): - """Get a book - to get some book by id, like: - http://localhost:5000/book/3 - """ - if path.bid == 4: - return NotFoundResponse().model_dump(), 404 - - -@app.get("/book", tags=[book_tag], responses={"200": BookListResponseV1}) -def get_books(query: BookBody): - """get books - to get all books - """ - assert query.age == 3 - assert query.author == "joy" - return { - "code": 0, - "message": "ok", - "data": [{"bid": 1, "age": query.age, "author": "b1"}, {"bid": 2, "age": query.age, "author": "b2"}], - } - - -@app.get("/book_v2", tags=[book_tag], responses={"200": BookListResponseV2}) -def get_books_v2(query: BookBody): - """get books - to get all books (v2) - """ - assert query.age == 3 - assert query.author == "joy" - return {"books": [{"bid": 1, "age": query.age, "author": "b1"}, {"bid": 2, "age": query.age, "author": "b2"}]} - - -@app.get("/book_v3", tags=[book_tag], responses={"200": BookListResponseV3}) -def get_books_v3(query: BookBody): - """get books - to get all books (v3) - """ - assert query.age == 3 - assert query.author == "joy" - - books = [{"bid": 1, "age": query.age, "author": "b1"}, {"bid": 2, "age": query.age, "author": "b2"}] - # A `list` have to be converted to json-format `str` returned as a `Response` object, - # because flask doesn't support returning a `list` as a response - return Response(json.dumps(books), status=200, headers={"Content-Type": "application/json"}) - - -@app.post("/book", tags=[book_tag], responses={"200": BaseResponse}) -def create_book(body: BookBody): - assert body.age == 3 - return {"code": 0, "message": "ok"}, HTTPStatus.OK - - -@app.put("/book/", tags=[book_tag]) -def update_book(path: BookPath, body: BookBody): - assert path.bid == 1 - assert body.age == 3 - return {"code": 0, "message": "ok"} - - -@app.patch("/book/", tags=[book_tag], doc_ui=False) -def update_book1(path: BookPath, body: BookBody): - assert path.bid == 1 - assert body.age == 3 - return {"code": 0, "message": "ok"} - - -@app.delete("/book/", tags=[book_tag], responses={"200": BaseResponse}) -def delete_book(path: BookPath): - assert path.bid == 1 - return {"code": 0, "message": "ok"} - - -@app.delete("/book_no_response/", tags=[book_tag], responses={"204": None}) -def delete_book_no_response(path: BookPath): - assert path.bid == 1 - return b"", 204 - - -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - print(resp.json) - assert resp.status_code == 200 - assert resp.json == app.api_doc +@app.delete("/book/") +def delete_book(path: IdModel): + return {"id": path.id} def test_get(client): - resp = client.get("/book?age=3&author=joy") - assert resp.status_code == 200 - resp_v2 = client.get("/book_v2?age=3&author=joy") - assert resp_v2.status_code == 200 - resp_v3 = client.get("/book_v3?age=3&author=joy") - assert resp_v3.status_code == 200 - - -def test_get_by_id_4(client): - resp = client.get("/book/4?age=3&author=joy") - assert resp.status_code == 404 + response = client.get("/book?name=s&age=3") + assert response.status_code == 200 def test_post(client): - resp = client.post("/book", json={"age": 3}) - assert resp.status_code == 200 + data = {"name": "test", "age": 1} + response = client.post("/book", json=data) + assert response.status_code == 200 + assert response.json == data def test_put(client): - resp = client.put("/book/1", json={"age": 3}) - assert resp.status_code == 200 + data = {"name": "test", "age": 1} + response = client.put("/book/1", json=data) + assert response.status_code == 200 def test_patch(client): - resp = client.patch("/book/1", json={"age": 3}) - assert resp.status_code == 200 + data = {"name": "test", "age": 1} + response = client.patch("/book/1", json=data) + assert response.status_code == 200 def test_delete(client): - resp = client.delete("/book/1") - assert resp.status_code == 200 - - -def test_delete_no_response(client): - resp = client.delete("/book_no_response/1") - assert resp.status_code == 204 + response = client.delete("/book/1") + assert response.status_code == 200 diff --git a/tests/test_restapi_with_doc_prefix.py b/tests/test_restapi_with_doc_prefix.py deleted file mode 100644 index dfab0afd..00000000 --- a/tests/test_restapi_with_doc_prefix.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import pytest -from pydantic import BaseModel, Field - -from flask_openapi import Info, OpenAPI - -info = Info(title="book API", version="1.0.0") - -jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} -security_schemes = {"jwt": jwt} - - -class NotFoundResponse(BaseModel): - code: int = Field(-1, description="Status Code") - message: str = Field("Resource not found!", description="Exception Information") - - -doc_prefix = "/v1/openapi" -app = OpenAPI( - __name__, info=info, doc_prefix=doc_prefix, security_schemes=security_schemes, responses={"404": NotFoundResponse} -) -app.config["TESTING"] = True - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -def test_openapi(client): - resp = client.get(f"{doc_prefix}/openapi.json") - assert resp.status_code == 200 - assert resp.json == app.api_doc diff --git a/tests/test_security.py b/tests/test_security.py deleted file mode 100644 index 7159bac8..00000000 --- a/tests/test_security.py +++ /dev/null @@ -1,39 +0,0 @@ -import pytest - -from flask_openapi import OpenAPI - -# Basic Authentication Sample -basic = {"type": "http", "scheme": "basic"} -# JWT Bearer Sample -jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} -# API Key Sample -api_key = {"type": "apiKey", "name": "api_key", "in": "header"} -# Implicit OAuth2 Sample -oauth2 = { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://example.com/api/oauth/dialog", - "scopes": {"write:pets": "modify pets in your account", "read:pets": "read your pets"}, - } - }, -} -security_schemes = {"jwt": jwt, "api_key": api_key, "oauth2": oauth2, "basic": basic} -security = [{"jwt": []}] - -app = OpenAPI(__name__, security_schemes=security_schemes) -app.config["TESTING"] = True - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - print(resp.json) - assert resp.status_code == 200 - assert resp.json == app.api_doc diff --git a/tests/test_server.py b/tests/test_server.py deleted file mode 100644 index 438fe4ac..00000000 --- a/tests/test_server.py +++ /dev/null @@ -1,28 +0,0 @@ -from pydantic import ValidationError - -from flask_openapi import Server, ServerVariable - - -def test_server_variable(): - Server(url="http://127.0.0.1:5000", variables=None) - try: - variables = {"one": ServerVariable(default="one", enum=[])} - Server(url="http://127.0.0.1:5000", variables=variables) - error = 0 - except ValidationError: - error = 1 - assert error == 1 - try: - variables = {"one": ServerVariable(default="one")} - Server(url="http://127.0.0.1:5000", variables=variables) - error = 0 - except ValidationError: - error = 1 - assert error == 0 - try: - variables = {"one": ServerVariable(default="one", enum=["one", "two"])} - Server(url="http://127.0.0.1:5000", variables=variables) - error = 0 - except ValidationError: - error = 1 - assert error == 0 diff --git a/tests/test_str_body.py b/tests/test_str_body.py deleted file mode 100644 index 0ed4762d..00000000 --- a/tests/test_str_body.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -from pydantic import BaseModel - -from flask_openapi import OpenAPI - -app = OpenAPI(__name__) -app.config["TESTING"] = True - - -class MyModel(BaseModel): - text: str - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -@app.post("/path/") -def create_book1(body: MyModel): - return body.text - - -def test_post(client): - my_model = MyModel(text="1") - resp = client.post("/path/", json=my_model.model_dump_json()) - assert resp.status_code == 200 diff --git a/tests/test_summary.py b/tests/test_summary.py deleted file mode 100644 index eb06c538..00000000 --- a/tests/test_summary.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest - -from flask_openapi import Info, OpenAPI - -info = Info(title="book API", version="1.0.0") - -app = OpenAPI(__name__, info=info) -app.config["TESTING"] = True - - -@pytest.fixture -def client(): - client = app.test_client() - - return client - - -@app.get("/book", summary="new summary", description="new description") -def get_book(): - """Get a book - to get some book by id, like: - http://localhost:5000/book/3 - """ - return {"code": 0, "message": "ok"} # pragma: no cover - - -@app.get("/book2", description="new description") -def get_book2(): - """Get a book - to get some book by id, like: - http://localhost:5000/book/3 - """ - return {"code": 0, "message": "ok"} # pragma: no cover - - -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - _json = resp.json - assert resp.status_code == 200 - assert _json["paths"]["/book"]["get"]["summary"] == "new summary" - assert _json["paths"]["/book"]["get"]["description"] == "new description" - assert _json["paths"]["/book2"]["get"]["summary"] == "Get a book" - assert _json["paths"]["/book2"]["get"]["description"] == "new description" diff --git a/tests/test_tags.py b/tests/test_tags.py deleted file mode 100644 index b9dc3c21..00000000 --- a/tests/test_tags.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest - -from flask_openapi import APIBlueprint, Info, OpenAPI, Tag - -info = Info(title="book API", version="1.0.0") - -app = OpenAPI(__name__, info=info) -app.config["TESTING"] = True - - -@pytest.fixture -def client(): - client = app.test_client() - return client - - -api1 = APIBlueprint("book1", __name__) - - -@api1.get("/book", tags=[Tag(name="book")]) -def get_book(): ... # pragma: no cover - - -api2 = APIBlueprint("book2", __name__) - - -@api2.get("/book2", tags=[Tag(name="book")]) -def get_book2(): ... # pragma: no cover - - -app.register_api(api1) -app.register_api(api2) - - -def test_openapi(client): - resp = client.get("/openapi/openapi.json") - _json = resp.json - assert resp.status_code == 200 - tags = _json["tags"] - news_tags = [] - for tag in tags: - if tag not in news_tags: - news_tags.append(tag) - assert news_tags == tags diff --git a/tests/test_url_prefix.py b/tests/test_url_prefix.py index e1f4dafb..1ac54d7a 100644 --- a/tests/test_url_prefix.py +++ b/tests/test_url_prefix.py @@ -20,13 +20,11 @@ def client(): @api1.get("/book") -def create_book1(): - return "ok" +def create_book1(): ... @api2.get("/book") -def create_book2(): - return "ok" +def create_book2(): ... app.register_api(api1, url_prefix="/api1") @@ -39,17 +37,13 @@ def create_book2(): @api_view1.route("/book") class BookAPIView: @api_view1.doc(summary="get book") - def get(self): - return "ok" + def get(self): ... @api_view2.route("/book") class BookAPIView2: @api_view2.doc(summary="get book") - def get( - self, - ): - return "ok" + def get(self): ... app.register_api_view(api_view1, url_prefix="/api3") diff --git a/tests/test_validate_request.py b/tests/test_validate_request.py index 1eab9b2f..18f157f4 100644 --- a/tests/test_validate_request.py +++ b/tests/test_validate_request.py @@ -1,10 +1,11 @@ +import inspect from functools import wraps import pytest from flask import request from pydantic import BaseModel, Field -from flask_openapi import APIView, Info, OpenAPI, Tag +from flask_openapi import APIView, OpenAPI, Tag from flask_openapi.request import validate_request @@ -20,14 +21,27 @@ class BookBody(BaseModel): def login_required(): def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - if not request.headers.get("Authorization"): - return {"error": "Unauthorized"}, 401 - kwargs["client_id"] = "client1234565" - return func(*args, **kwargs) + is_coroutine_function = inspect.iscoroutinefunction(func) + if is_coroutine_function: - return wrapper + @wraps(func) + async def wrapper(*args, **kwargs): + if not request.headers.get("Authorization"): + return {"error": "Unauthorized"}, 401 # pragma: no cover + kwargs["client_id"] = "client1234565" + return await func(*args, **kwargs) + + return wrapper + else: + + @wraps(func) + def wrapper(*args, **kwargs): + if not request.headers.get("Authorization"): + return {"error": "Unauthorized"}, 401 + kwargs["client_id"] = "client1234565" + return func(*args, **kwargs) + + return wrapper return decorator @@ -37,15 +51,7 @@ def app(): app = OpenAPI(__name__) app.config["TESTING"] = True - info = Info(title="book API", version="1.0.0") - jwt = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} - security_schemes = {"jwt": jwt} - - app = OpenAPI(__name__, info=info, security_schemes=security_schemes) - app.config["TESTING"] = True - security = [{"jwt": []}] - - api_view = APIView(url_prefix="/v1/books", view_tags=[Tag(name="book")], view_security=security) + api_view = APIView(url_prefix="/v1/books", view_tags=[Tag(name="book")]) @api_view.route("") class BookListAPIView: @@ -53,13 +59,14 @@ class BookListAPIView: @login_required() @validate_request() def get(self, client_id: str): - return {"books": ["book1", "book2"], "client_id": client_id} + return {"books": ["book1", "book2"], "client_id": client_id} # pragma: no cover @api_view.doc(summary="create book") @login_required() @validate_request() - def post(self, body: BookBody, client_id): + async def post(self, body: BookBody, client_id): """description for a created book""" + print(client_id) return body.model_dump_json() @api_view.route("/") diff --git a/tests/test_validate_responses.py b/tests/test_validate_responses.py index ed912978..5fe69438 100644 --- a/tests/test_validate_responses.py +++ b/tests/test_validate_responses.py @@ -1,6 +1,9 @@ from __future__ import annotations +from http import HTTPStatus + import pytest +from flask import make_response from pydantic import BaseModel, ValidationError from flask_openapi import APIView, OpenAPI @@ -14,7 +17,9 @@ class BaseRequest(BaseModel): test_str: str -class GoodResponse(BaseRequest): ... +class GoodResponse(BaseRequest): + test_int: int + test_str: str class BadResponse(BaseModel): @@ -46,9 +51,9 @@ def test_app_level_validate_response(request): test_app = OpenAPI(request.node.name, validate_response=True) test_app.config["TESTING"] = True - @test_app.post("/test", responses={201: BadResponse}) + @test_app.post("/test", responses={200: BadResponse}) def endpoint_test(body: BaseRequest): - return body.model_dump(), 201 + return body.model_dump_json() with test_app.test_client() as client: with pytest.raises(ValidationError): @@ -62,9 +67,9 @@ def test_app_api_level_validate_response(request): test_app = OpenAPI(request.node.name) test_app.config["TESTING"] = True - @test_app.post("/test", responses={201: BadResponse}, validate_response=True) + @test_app.post("/test", responses={HTTPStatus.CREATED: BadResponse}, validate_response=True) def endpoint_test(body: BaseRequest): - return body.model_dump(), 201 + return body.model_dump(), HTTPStatus.CREATED with test_app.test_client() as client: with pytest.raises(ValidationError): @@ -192,3 +197,40 @@ def post(self, body: BaseRequest): with test_app.test_client() as client: with pytest.raises(ValidationError): _ = client.post("/test", json={"test_int": 1, "test_str": "s"}) + + +def test_validate_response_with_make_response(request): + test_app = OpenAPI(request.node.name, validate_response=True) + test_app.config["TESTING"] = True + + @test_app.post("/test1", responses={200: GoodResponse}) + def endpoint_test1(): + r = make_response({"test_int": 1, "test_str": "string"}, 200) + r.headers["Content-Type"] = "application/json" + return r + + @test_app.post("/test2", responses={200: GoodResponse}) + def endpoint_test2(): + r = make_response({"test_int": 1, "test_str": "string"}, 200) + r.headers["Content-Type"] = "application/csv" + return r + + with test_app.test_client() as client: + resp = client.post("/test1") + assert resp.status_code == 200 + + resp = client.post("/test2") + assert resp.status_code == 200 + + +def test_validate_response_with_none(request): + test_app = OpenAPI(request.node.name, validate_response=True) + test_app.config["TESTING"] = True + + @test_app.post("/test", responses={204: None}) + def endpoint_test1(): + return "", 204 + + with test_app.test_client() as client: + resp = client.post("/test") + assert resp.status_code == 204 diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index e0f855e1..c217e6d7 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -73,5 +73,4 @@ def test_openapi(client): def test_api_query(client): resp = client.get("/query?age=abc") - print(resp.json) assert resp.status_code == 400 diff --git a/uv.lock b/uv.lock index 68a8b56d..365e30b1 100644 --- a/uv.lock +++ b/uv.lock @@ -31,16 +31,16 @@ wheels = [ [[package]] name = "backrefs" -version = "6.1" +version = "6.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] [[package]] @@ -179,7 +179,7 @@ wheels = [ [[package]] name = "flask" -version = "3.1.2" +version = "3.1.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "blinker" }, @@ -189,9 +189,9 @@ dependencies = [ { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] @@ -355,26 +355,26 @@ wheels = [ [[package]] name = "flask-openapi-scalar" -version = "1.43.8" +version = "1.44.25" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "flask-openapi" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/56/faf5c9073e82617cc043c7dde8e9eab60638563a1054be4eb7ebf70a5ce2/flask_openapi_scalar-1.43.8.tar.gz", hash = "sha256:bd7c25a9f4dbe8f897a2e4915bdcfe83c07a142f794f92bd5ffcf000d811ee59", size = 880033, upload-time = "2026-01-19T08:56:14.856Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/41/939e01a75a45f0d446337bdc05a7886f6155fea8cea5d7596bf09fe96b31/flask_openapi_scalar-1.44.25.tar.gz", hash = "sha256:c9f12706476a44495f9dc8195f04f516e5af74dc79fa5990535d0b77de1e8eb3", size = 956636, upload-time = "2026-02-20T02:51:37.401Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/37/45cd2e307e8680ea96fcacd3b1ef36e67afa0fc99236039d019532a0c790/flask_openapi_scalar-1.43.8-py3-none-any.whl", hash = "sha256:43c9cb28b96a98d81f4308bebe2bd678c10ef8591ee7d40eddb78669691af8ab", size = 892540, upload-time = "2026-01-19T08:56:10.474Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/a0/9bfbc2d5784fa164f495c3abb892e7426fc152aa33bfdc110aca309eb8c6/flask_openapi_scalar-1.44.25-py3-none-any.whl", hash = "sha256:f0dce13aefcaf644d2e2d152e55424efea09f0b79f19d701984dcfd0d8a9b5b0", size = 959969, upload-time = "2026-02-20T02:51:36.108Z" }, ] [[package]] name = "flask-openapi-swagger" -version = "5.31.0" +version = "5.31.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "flask-openapi" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/b0/a113b4651bea35aca075557459537993ad7e73f3b76296b667fe987ea781/flask_openapi_swagger-5.31.0.tar.gz", hash = "sha256:cbf764ae0ea9896766c7d361cec85a70294db7e5401de3fcedc3a8b881f790a9", size = 527529, upload-time = "2026-01-19T08:52:37.3Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/9d/0db79cb44960c412011b0897c41eb1b06ae107ef99abf460f28962dbfbed/flask_openapi_swagger-5.31.2.tar.gz", hash = "sha256:64b7f37dbe26680faae6d5b9cb8c598c9d459c3b8a0596bbc8c5c9558c8abd5d", size = 516945, upload-time = "2026-02-21T02:38:40.211Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/63/29860443da63c20599832615b0b489ed192cbf04a9307373ba2dae1cee91/flask_openapi_swagger-5.31.0-py3-none-any.whl", hash = "sha256:f74332519edd5337c7415976d187d24ec1d832c9c03df573a385b0dab6c92daf", size = 535748, upload-time = "2026-01-19T08:52:33.424Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/d9/cfcd56e263087f4fd8ca3a5ab114c8b875a50960ebd9e04f893a0424f0ce/flask_openapi_swagger-5.31.2-py3-none-any.whl", hash = "sha256:8247810cf610d41b6d91665ec2cff0d7157155a8c540884d9b77501053b4c081", size = 520447, upload-time = "2026-02-21T02:38:38.479Z" }, ] [[package]] @@ -390,15 +390,11 @@ wheels = [ ] [[package]] -name = "griffe" -version = "1.15.0" +name = "griffelib" +version = "2.0.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] [[package]] @@ -463,11 +459,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10.1" +version = "3.10.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -598,16 +594,16 @@ wheels = [ [[package]] name = "mkdocs-autorefs" -version = "1.4.3" +version = "1.4.4" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, ] [[package]] @@ -638,7 +634,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.1" +version = "9.7.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "babel" }, @@ -653,9 +649,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/57/5d3c8c9e2ff9d66dc8f63aa052eb0bac5041fecff7761d8689fe65c39c13/mkdocs_material-9.7.2.tar.gz", hash = "sha256:6776256552290b9b7a7aa002780e25b1e04bc9c3a8516b6b153e82e16b8384bd", size = 4097818, upload-time = "2026-02-18T15:53:07.763Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/19/d194e75e82282b1d688f0720e21b5ac250ed64ddea333a228aaf83105f2e/mkdocs_material-9.7.2-py3-none-any.whl", hash = "sha256:9bf6f53452d4a4d527eac3cef3f92b7b6fc4931c55d57766a7d87890d47e1b92", size = 9305052, upload-time = "2026-02-18T15:53:05.221Z" }, ] [[package]] @@ -669,19 +665,19 @@ wheels = [ [[package]] name = "mkdocs-static-i18n" -version = "1.3.0" +version = "1.3.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "mkdocs" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/2b/59652a2550465fde25ae6a009cb6d74d0f7e724d272fc952685807b29ca1/mkdocs_static_i18n-1.3.0.tar.gz", hash = "sha256:65731e1e4ec6d719693e24fee9340f5516460b2b7244d2a89bed4ce3cfa6a173", size = 1370450, upload-time = "2025-01-24T09:03:24.389Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/f9/51e2ffda9c7210bc35a24f3717b08c052cd4b728dfa87f901c00d8005259/mkdocs_static_i18n-1.3.1.tar.gz", hash = "sha256:a6125ea7db6cc1a900d76a967f262535af09831160a93c56d7f0d522a79b5faf", size = 1371325, upload-time = "2026-02-20T10:42:41.835Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/f7/ef222a7a2f96ecf79c7c00bfc9dde3b22cd2cc1bd2b7472c7b204fc64225/mkdocs_static_i18n-1.3.0-py3-none-any.whl", hash = "sha256:7905d52fff71d2c108b6c344fd223e848ca7e39ddf319b70864dfa47dba85d6b", size = 21660, upload-time = "2025-01-24T09:03:22.461Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/0b/43ff4afb6b438d47718b1959a22075ed95d8460d8c47381878b37a40de63/mkdocs_static_i18n-1.3.1-py3-none-any.whl", hash = "sha256:4036e24795a150c9c4d4b001ed24a43aec01335f76188dbe5a5d8fb4a27eba65", size = 21853, upload-time = "2026-02-20T10:42:40.551Z" }, ] [[package]] name = "mkdocstrings" -version = "1.0.2" +version = "1.0.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "jinja2" }, @@ -691,9 +687,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/4d/1ca8a9432579184599714aaeb36591414cc3d3bfd9d494f6db540c995ae4/mkdocstrings-1.0.2.tar.gz", hash = "sha256:48edd0ccbcb9e30a3121684e165261a9d6af4d63385fc4f39a54a49ac3b32ea8", size = 101048, upload-time = "2026-01-24T15:57:25.735Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/32/407a9a5fdd7d8ecb4af8d830b9bcdf47ea68f916869b3f44bac31f081250/mkdocstrings-1.0.2-py3-none-any.whl", hash = "sha256:41897815a8026c3634fe5d51472c3a569f92ded0ad8c7a640550873eea3b6817", size = 35443, upload-time = "2026-01-24T15:57:23.933Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, ] [package.optional-dependencies] @@ -703,16 +699,16 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "2.0.1" +version = "2.0.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] [[package]] @@ -744,11 +740,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -883,15 +879,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.20.1" +version = "10.21" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, ] [[package]] @@ -1024,27 +1020,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +version = "0.15.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] [[package]] @@ -1111,26 +1107,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.15" +version = "0.0.18" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/15/9682700d8d60fdca7afa4febc83a2354b29cdcd56e66e19c92b521db3b39/ty-0.0.18.tar.gz", hash = "sha256:04ab7c3db5dcbcdac6ce62e48940d3a0124f377c05499d3f3e004e264ae94b83", size = 5214774, upload-time = "2026-02-20T21:51:31.173Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/d8/920460d4c22ea68fcdeb0b2fb53ea2aeb9c6d7875bde9278d84f2ac767b6/ty-0.0.18-py3-none-linux_armv6l.whl", hash = "sha256:4e5e91b0a79857316ef893c5068afc4b9872f9d257627d9bc8ac4d2715750d88", size = 10280825, upload-time = "2026-02-20T21:51:25.03Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/56/62587de582d3d20d78fcdddd0594a73822ac5a399a12ef512085eb7a4de6/ty-0.0.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee0e578b3f8416e2d5416da9553b78fd33857868aa1384cb7fefeceee5ff102d", size = 10118324, upload-time = "2026-02-20T21:51:22.27Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/2d/dbdace8d432a0755a7417f659bfd5b8a4261938ecbdfd7b42f4c454f5aa9/ty-0.0.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f7a0487d36b939546a91d141f7fc3dbea32fab4982f618d5b04dc9d5b6da21e", size = 9605861, upload-time = "2026-02-20T21:51:16.066Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/d9/de11c0280f778d5fc571393aada7fe9b8bc1dd6a738f2e2c45702b8b3150/ty-0.0.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e2fa8d45f57ca487a470e4bf66319c09b561150e98ae2a6b1a97ef04c1a4eb", size = 10092701, upload-time = "2026-02-20T21:51:26.862Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/94/068d4d591d791041732171e7b63c37a54494b2e7d28e88d2167eaa9ad875/ty-0.0.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d75652e9e937f7044b1aca16091193e7ef11dac1c7ec952b7fb8292b7ba1f5f2", size = 10109203, upload-time = "2026-02-20T21:51:11.59Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/e4/526a4aa56dc0ca2569aaa16880a1ab105c3b416dd70e87e25a05688999f3/ty-0.0.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:563c868edceb8f6ddd5e91113c17d3676b028f0ed380bdb3829b06d9beb90e58", size = 10614200, upload-time = "2026-02-20T21:51:20.298Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/3d/b68ab20a34122a395880922587fbfc3adf090d22e0fb546d4d20fe8c2621/ty-0.0.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502e2a1f948bec563a0454fc25b074bf5cf041744adba8794d024277e151d3b0", size = 11153232, upload-time = "2026-02-20T21:51:14.121Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/ea/678243c042343fcda7e6af36036c18676c355878dcdcd517639586d2cf9e/ty-0.0.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc881dea97021a3aa29134a476937fd8054775c4177d01b94db27fcfb7aab65b", size = 10832934, upload-time = "2026-02-20T21:51:32.92Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/bd/7f8d647cef8b7b346c0163230a37e903c7461c7248574840b977045c77df/ty-0.0.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:421fcc3bc64cab56f48edb863c7c1c43649ec4d78ff71a1acb5366ad723b6021", size = 10700888, upload-time = "2026-02-20T21:51:09.673Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/06/cb3620dc48c5d335ba7876edfef636b2f4498eff4a262ff90033b9e88408/ty-0.0.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0fe5038a7136a0e638a2fb1ad06e3d3c4045314c6ba165c9c303b9aeb4623d6c", size = 10078965, upload-time = "2026-02-20T21:51:07.678Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/27/c77a5a84533fa3b685d592de7b4b108eb1f38851c40fac4e79cc56ec7350/ty-0.0.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d123600a52372677613a719bbb780adeb9b68f47fb5f25acb09171de390e0035", size = 10134659, upload-time = "2026-02-20T21:51:18.311Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/6e/60af6b88c73469e628ba5253a296da6984e0aa746206f3034c31f1a04ed1/ty-0.0.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb4bc11d32a1bf96a829bf6b9696545a30a196ac77bbc07cc8d3dfee35e03723", size = 10297494, upload-time = "2026-02-20T21:51:39.631Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/90/612dc0b68224c723faed6adac2bd3f930a750685db76dfe17e6b9e534a83/ty-0.0.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dda2efbf374ba4cd704053d04e32f2f784e85c2ddc2400006b0f96f5f7e4b667", size = 10791944, upload-time = "2026-02-20T21:51:37.13Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/da/f4ada0fd08a9e4138fe3fd2bcd3797753593f423f19b1634a814b9b2a401/ty-0.0.18-py3-none-win32.whl", hash = "sha256:c5768607c94977dacddc2f459ace6a11a408a0f57888dd59abb62d28d4fee4f7", size = 9677964, upload-time = "2026-02-20T21:51:42.039Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/fa/090ed9746e5c59fc26d8f5f96dc8441825171f1f47752f1778dad690b08b/ty-0.0.18-py3-none-win_amd64.whl", hash = "sha256:b78d0fa1103d36fc2fce92f2092adace52a74654ab7884d54cdaec8eb5016a4d", size = 10636576, upload-time = "2026-02-20T21:51:29.159Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" }, ] [[package]] @@ -1201,14 +1197,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.5" +version = "3.1.6" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, ] [[package]]