diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e05f22..0135f3a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,24 +30,23 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] runs-on: windows-latest if: github.event_name == 'pull_request' || github.event_name == 'push' steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 with: fetch-depth: 9 submodules: false - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install cython==3.0.2 pip install -r requirements.txt pip install flake8 @@ -77,24 +76,24 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 with: fetch-depth: 9 submodules: false - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip install -r requirements.txt - pip install black==24.2.0 isort==5.13.2 flake8==7.0.0 + pip install black==24.8.0 isort==5.13.2 flake8==7.1.1 - name: Compile Cython extensions run: | @@ -104,10 +103,10 @@ jobs: run: | pytest --doctest-modules --junitxml=junit/pytest-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=$PROJECT_NAME --cov-report=xml - - name: Run tests with Pydantic v2 + - name: Run tests with Pydantic v1 run: | - echo "[*] The previous tests used Pydantic v1, now running with v2" - pip install -U pydantic==2.4.2 + echo "[*] The previous tests used Pydantic v2, now running with v1" + pip install -U "pydantic<2" pytest - name: Run linters @@ -167,22 +166,22 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 with: fetch-depth: 9 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install build dependencies run: | - pip install cython==3.0.2 + pip install cython==3.0.11 pip install --upgrade build - name: Compile Cython extensions @@ -229,7 +228,7 @@ jobs: path: dist - name: Use Python 3.11 - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: '3.11' diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b638c..5fa0c12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.8] - 2025-01-?? + +- Fix type error in `blacksheep/server/compression.py` `is_handled_encoding`; + contributed by @bymoye and @ChenyangGao. +- Fix issue where the host is not the proxy address when there is a proxy by + @ChenyangGao. + ## [2.0.7] - 2024-02-17 :tulip: - Fixes bug [#38](https://github.com/Neoteroi/BlackSheep-Docs/issues/38), diff --git a/blacksheep/server/asgi.py b/blacksheep/server/asgi.py index 5b6bac4..df16105 100644 --- a/blacksheep/server/asgi.py +++ b/blacksheep/server/asgi.py @@ -20,11 +20,19 @@ def get_request_url_from_scope( try: path = scope["path"] protocol = scope["scheme"] - host, port = scope["server"] + for key, val in scope["headers"]: + if key.lower() in (b"host", b"x-forwarded-host", b"x-original-host"): + host = val.decode("latin-1") + port = 0 + break + else: + host, port = scope["server"] except KeyError as key_error: raise ValueError(f"Invalid scope: {key_error}") - if protocol == "http" and port == 80: + if not port: + port_part = "" + elif protocol == "http" and port == 80: port_part = "" elif protocol == "https" and port == 443: port_part = "" diff --git a/blacksheep/server/compression.py b/blacksheep/server/compression.py index 24a5dba..7959b3c 100644 --- a/blacksheep/server/compression.py +++ b/blacksheep/server/compression.py @@ -75,7 +75,7 @@ def _is_handled_type(content_type) -> bool: return any(_type in content_type for _type in self.handled_types) def is_handled_encoding() -> bool: - return b"gzip" in (request.get_first_header(b"accept-encoding") or "") + return b"gzip" in (request.get_first_header(b"accept-encoding") or b"") def is_handled_response_content() -> bool: if response is None or response.content is None: diff --git a/blacksheep/server/openapi/v3.py b/blacksheep/server/openapi/v3.py index 434828f..3e6407b 100644 --- a/blacksheep/server/openapi/v3.py +++ b/blacksheep/server/openapi/v3.py @@ -684,6 +684,9 @@ def _try_get_schema_for_simple_type(self, object_type: Type) -> Optional[Schema] if object_type is str: return Schema(type=ValueType.STRING) + if object_type is bytes: + return Schema(type=ValueType.STRING, format=ValueFormat.BINARY) + if object_type is int: return Schema(type=ValueType.INTEGER, format=ValueFormat.INT64) diff --git a/pyproject.toml b/pyproject.toml index 0006c52..af16474 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Environment :: Web Environment", "Operating System :: OS Independent", "Framework :: AsyncIO", @@ -25,13 +26,13 @@ keywords = ["blacksheep", "web framework", "asyncio"] dependencies = [ "httptools>=0.5", "certifi>=2022.9.24", - "charset-normalizer~=3.1.0", + "charset-normalizer~=3.4.0", "guardpost>=1.0.2", "rodi~=2.0.2", "essentials>=1.1.4,<2.0", "essentials-openapi>=1.0.6,<1.1", - "python-dateutil~=2.8.2", - "itsdangerous~=2.1.2", + "python-dateutil~=2.9.0", + "itsdangerous~=2.2.0", ] [tool.setuptools] @@ -42,7 +43,7 @@ version = { attr = "blacksheep.__version__" } [project.optional-dependencies] jinja = ["Jinja2~=3.1.2"] -full = ["cryptography>=38.0.1,<41.1.0", "PyJWT~=2.6.0", "websockets~=10.3"] +full = ["cryptography>=44.0.0,<45.0.0", "PyJWT~=2.9.0", "websockets~=13.1"] [project.urls] "Homepage" = "https://github.com/Neoteroi/BlackSheep" diff --git a/requirements.txt b/requirements.txt index e1e520c..b0040fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,59 +1,60 @@ -annotated-types==0.6.0 -asgiref==3.7.2 -attrs==23.1.0 -blinker==1.6.3 -certifi==2023.7.22 -cffi==1.16.0 -charset-normalizer==3.3.1 +annotated-types==0.7.0 +asgiref==3.8.1 +attrs==24.2.0 +blinker==1.8.2 +certifi==2024.12.14 +cffi==1.17.1 +charset-normalizer==3.4.0 click==8.1.7 -coverage==7.3.2 -cryptography==41.0.6 -Cython==3.0.4; platform_system != "Windows" +coverage==7.6.1 +cryptography==44.0.0 +Cython==3.0.11 essentials==1.1.5 -essentials-openapi==1.0.8 -Flask==3.0.0 -gevent==23.9.1 -greenlet==3.0.0 +essentials-openapi==1.0.9 +Flask==3.0.3 +gevent==24.2.1 +greenlet==3.1.1 guardpost==1.0.2 h11==0.14.0 h2==4.1.0 hpack==4.0.0 -httptools==0.6.1 +httptools==0.6.4 hypercorn==0.14.4 hyperframe==6.0.1 -idna==3.4 +idna==3.10 iniconfig==2.0.0 -itsdangerous==2.1.2 -Jinja2==3.1.3 -MarkupSafe==2.1.3 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 mccabe==0.7.0 -packaging==23.2 -pluggy==1.3.0 +packaging==24.2 +pluggy==1.5.0 priority==2.0.0 py==1.11.0 -pycparser==2.21 -pydantic==2.4.2 -pydantic_core==2.10.1 -PyJWT==2.8.0 -pyparsing==3.1.1 -pytest==7.4.2 +pycparser==2.22 +pydantic==2.10.3 +pydantic-core==2.27.1 +PyJWT==2.9.0 +pyparsing==3.1.4 +pytest==8.3.4 pytest-asyncio==0.21.1 -pytest-cov==4.1.0 -python-dateutil==2.8.2 -PyYAML==6.0.1 -regex==2023.10.3 -requests==2.31.0 -rodi==2.0.3 -setuptools==68.2.2 -six==1.16.0 +pytest-cov==5.0.0 +python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +regex==2024.11.6 +requests==2.32.3 +rodi==2.0.6 +setuptools==75.3.0 +six==1.17.0 toml==0.10.2 -tomli==2.0.1 -typing_extensions==4.8.0 -urllib3==2.0.7 -uvicorn==0.23.2 -wcwidth==0.2.8 -websockets==12.0 -Werkzeug==3.0.1 +tomli==2.2.1 +typing-extensions==4.12.2 +urllib3==2.2.3 +uvicorn==0.33.0 +wcwidth==0.2.13 +websockets==13.1 +Werkzeug==3.0.6 wsproto==1.2.0 zope.event==5.0 -zope.interface==6.1 +zope.interface==7.2 +build==1.2.2.post1 diff --git a/tests/test_openapi_v3.py b/tests/test_openapi_v3.py index cbddde4..9d11252 100644 --- a/tests/test_openapi_v3.py +++ b/tests/test_openapi_v3.py @@ -129,6 +129,11 @@ class CatDetails(Cat): friends: List[int] +@dataclass +class CreateCatImages: + images: List[str] + + @dataclass class Combo(Generic[T, U]): item_one: T @@ -248,6 +253,9 @@ def create_cat(input: CreateCatInput) -> Cat: ... @delete("/api/cats/{cat_id}") def delete_cat(cat_id: int) -> None: ... + @post("/api/cats/{cat_id}/images") + def upload_images(cat_id: int, images: FromForm[CreateCatImages]) -> None: ... + return app @@ -1049,9 +1057,68 @@ def home() -> PydPaginatedSetOfCat: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if sys.version_info >= (3, 9): - assert ( - yaml.strip() - == """ + if PYDANTIC_VERSION == 1: + assert ( + yaml.strip() + == """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PydPaginatedSetOfCat' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + - childs + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + childs: + type: array + nullable: false + items: + nullable: false + PydPaginatedSetOfCat: + type: object + required: + - items + - total + properties: + items: + type: array + nullable: false + items: + $ref: '#/components/schemas/PydCat' + total: + type: integer + format: int64 + nullable: false +tags: [] +""".strip() + ) + else: + assert ( + yaml.strip() + == """ openapi: 3.0.3 info: title: Example @@ -1107,7 +1174,7 @@ def home() -> PydPaginatedSetOfCat: ... nullable: false tags: [] """.strip() - ) + ) else: assert ( yaml.strip() @@ -1177,9 +1244,89 @@ def home() -> PydTypeWithChildModels: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if sys.version_info >= (3, 9): - assert ( - yaml.strip() - == """ + if PYDANTIC_VERSION == 1: + assert ( + yaml.strip() + == """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PydTypeWithChildModels' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + - childs + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + childs: + type: array + nullable: false + items: + nullable: false + PydPaginatedSetOfCat: + type: object + required: + - items + - total + properties: + items: + type: array + nullable: false + items: + $ref: '#/components/schemas/PydCat' + total: + type: integer + format: int64 + nullable: false + PydExampleWithSpecificTypes: + type: object + required: + - url + properties: + url: + type: string + format: uri + maxLength: 2083 + minLength: 1 + nullable: false + PydTypeWithChildModels: + type: object + required: + - child + - friend + properties: + child: + $ref: '#/components/schemas/PydPaginatedSetOfCat' + friend: + $ref: '#/components/schemas/PydExampleWithSpecificTypes' +tags: [] + """.strip() + ) + else: + assert ( + yaml.strip() + == """ openapi: 3.0.3 info: title: Example @@ -1256,7 +1403,7 @@ def home() -> PydTypeWithChildModels: ... $ref: '#/components/schemas/PydExampleWithSpecificTypes' tags: [] """.strip() - ) + ) else: assert ( yaml.strip() @@ -1347,9 +1494,68 @@ def home() -> PaginatedSet[PydCat]: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if sys.version_info >= (3, 9): - assert ( - yaml.strip() - == """ + if PYDANTIC_VERSION == 1: + assert ( + yaml.strip() + == """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSetOfPydCat' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + - childs + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + childs: + type: array + nullable: false + items: + nullable: false + PaginatedSetOfPydCat: + type: object + required: + - items + - total + properties: + items: + type: array + nullable: false + items: + $ref: '#/components/schemas/PydCat' + total: + type: integer + format: int64 + nullable: false +tags: [] + """.strip() + ) + else: + assert ( + yaml.strip() + == """ openapi: 3.0.3 info: title: Example @@ -1405,7 +1611,7 @@ def home() -> PaginatedSet[PydCat]: ... nullable: false tags: [] """.strip() - ) + ) else: assert ( yaml.strip() @@ -1593,6 +1799,30 @@ async def test_cats_api(docs: OpenAPIHandler, serializer: Serializer): nullable: false description: '' required: true + /api/cats/{cat_id}/images: + post: + responses: + '204': + description: Success response + operationId: upload_images + parameters: + - name: cat_id + in: path + schema: + type: integer + format: int64 + nullable: false + description: '' + required: true + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateCatImages' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CreateCatImages' + required: true components: schemas: Cat: @@ -1672,6 +1902,17 @@ async def test_cats_api(docs: OpenAPIHandler, serializer: Serializer): type: integer format: int64 nullable: false + CreateCatImages: + type: object + required: + - images + properties: + images: + type: array + nullable: false + items: + type: string + nullable: false tags: [] """.strip() ) @@ -1757,6 +1998,30 @@ async def test_cats_api_capital_operations_ids( nullable: false description: '' required: true + /api/cats/{cat_id}/images: + post: + responses: + '204': + description: Success response + operationId: Upload images + parameters: + - name: cat_id + in: path + schema: + type: integer + format: int64 + nullable: false + description: '' + required: true + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateCatImages' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CreateCatImages' + required: true components: schemas: Cat: @@ -1836,6 +2101,17 @@ async def test_cats_api_capital_operations_ids( type: integer format: int64 nullable: false + CreateCatImages: + type: object + required: + - images + properties: + images: + type: array + nullable: false + items: + type: string + nullable: false tags: [] """.strip() ) @@ -1939,8 +2215,6 @@ def home() -> PydResponse[PydCat]: ... type: array nullable: false items: - type: string - format: uuid nullable: false Error: type: object @@ -2221,8 +2495,8 @@ def home() -> PydConstrained: ... big_float: type: number format: float - maximum: 1024 - minimum: 1000 + maximum: 1024.0 + minimum: 1000.0 nullable: false unit_interval: type: number diff --git a/tests/test_requests.py b/tests/test_requests.py index 538a0c9..1f13600 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -305,17 +305,32 @@ def test_request_pyi(): "scope,trailing_slash,expected_value", [ [ - {"scheme": "https", "path": "/", "server": ("www.neoteroi.dev", 443)}, + { + "scheme": "https", + "path": "/", + "server": ("www.neoteroi.dev", 443), + "headers": [], + }, False, "https://www.neoteroi.dev/", ], [ - {"scheme": "https", "path": "/admin", "server": ("www.neoteroi.dev", 443)}, + { + "scheme": "https", + "path": "/admin", + "server": ("www.neoteroi.dev", 443), + "headers": [], + }, False, "https://www.neoteroi.dev/admin", ], [ - {"scheme": "https", "path": "/admin", "server": ("www.neoteroi.dev", 443)}, + { + "scheme": "https", + "path": "/admin", + "server": ("www.neoteroi.dev", 443), + "headers": [], + }, True, "https://www.neoteroi.dev/admin/", ], @@ -324,17 +339,28 @@ def test_request_pyi(): "scheme": "https", "path": "/admin", "server": ("www.neoteroi.dev", 44777), + "headers": [], }, True, "https://www.neoteroi.dev:44777/admin/", ], [ - {"scheme": "http", "path": "/admin", "server": ("www.neoteroi.dev", 44777)}, + { + "scheme": "http", + "path": "/admin", + "server": ("www.neoteroi.dev", 44777), + "headers": [], + }, True, "http://www.neoteroi.dev:44777/admin/", ], [ - {"scheme": "http", "path": "/admin", "server": ("www.neoteroi.dev", 80)}, + { + "scheme": "http", + "path": "/admin", + "server": ("www.neoteroi.dev", 80), + "headers": [], + }, True, "http://www.neoteroi.dev/admin/", ], @@ -344,6 +370,7 @@ def test_request_pyi(): "path": "/admin", "server": ("www.neoteroi.dev", 80), "query_string": b"foo=Hello%20World%20%C3%B8", + "headers": [], }, False, "http://www.neoteroi.dev/admin?foo=Hello%20World%20%C3%B8",