diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c80211b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,74 @@ +name: Build + +on: + workflow_call: + workflow_dispatch: + +env: + PYTHONUNBUFFERED: "1" + FORCE_COLOR: "1" + +jobs: + test: + uses: ./.github/workflows/test.yml + permissions: + contents: read + + build: + name: Build distribution + runs-on: ubuntu-latest + needs: + - test + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install Hatch + run: pipx install hatch + + - name: Set package version from tag + run: hatch version "$(git describe --tags --always)" + + - name: Build package + run: hatch build + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + sign-release: + name: Sign Python distribution with Sigstore + needs: + - build + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Sign distributions with Sigstore + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + + - name: Store signed distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + overwrite: true diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml deleted file mode 100644 index 23a6f88..0000000 --- a/.github/workflows/code_quality.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Code Quality - -on: - push: - branches: - - "*" - tags: - - "*" - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install hatch - - - name: Build the package - run: | - hatch build - - - name: Run linter - run: | - hatch run ruff check . diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 6afdfbe..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Publish on PyPI - -on: - release: - types: - - published - -jobs: - build-n-publish: - name: Build and publish to PyPI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m build - --sdist - --wheel - --outdir dist/ - . - - name: Publish distribution to PyPI - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 0000000..742ffc6 --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,64 @@ +name: Prepare release + +on: + push: + tags: + - "*" + workflow_call: + workflow_dispatch: + +env: + PYTHONUNBUFFERED: "1" + FORCE_COLOR: "1" + +jobs: + build: + uses: ./.github/workflows/build.yml + permissions: + contents: read + id-token: write + + create-release: + needs: + - build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate release notes + id: release-notes + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + args: --latest --strip all + env: + OUTPUT: RELEASE_NOTES.md + GITHUB_REPO: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Create draft release + id: create-draft-release + uses: softprops/action-gh-release@v2 + with: + files: | + ./dist/* + draft: true + body_path: RELEASE_NOTES.md + + - name: Summary + run: | + echo "# Release summary" >> "$GITHUB_STEP_SUMMARY" + echo "Url: ${{ steps.create-draft-release.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" + echo "You can now publish the release on GitHub" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9641cfc --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,41 @@ +name: Publish + +on: + release: + types: + - published + workflow_call: + workflow_dispatch: + +env: + PYTHONUNBUFFERED: "1" + FORCE_COLOR: "1" + +jobs: + publish-to-pypi: + name: Publish Python distribution to PyPI + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/ariadne-lambda + permissions: + id-token: write + contents: read + + steps: + - name: Download all distributions from release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release download + '${{ github.ref_name }}' + -p '*.whl' + -p '*.tar.gz' + --dir dist/ + --repo '${{ github.repository }}' + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml deleted file mode 100644 index 042cb81..0000000 --- a/.github/workflows/run_tests.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Run Tests - -on: - push: - branches: - - "*" - tags: - - "*" - -jobs: - build-and-test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install hatch - - - name: Build the package - run: | - hatch build - - - name: Run tests - run: | - hatch run test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..49bcdad --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + schedule: + - cron: "0 7 * * 1,3" + workflow_call: + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHONUNBUFFERED: "1" + FORCE_COLOR: "1" + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Hatch + uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc + + - name: Run static analysis + run: hatch run lint + + - name: Run tests + run: hatch test -c -py ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..be2742d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# CHANGELOG + +All notable unreleased changes to this project will be documented in this file. + +For released versions, see the [Releases](https://github.com/mirumee/ariadne-lambda/releases) page. + +## Unreleased + +### ๐Ÿ› ๏ธ Build System +- Modernize packaging metadata and CI/release workflows + + diff --git a/ariadne_lambda/__about__.py b/ariadne_lambda/__about__.py new file mode 100644 index 0000000..c618245 --- /dev/null +++ b/ariadne_lambda/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.1.dev0" # This is overwritten by Hatch in CI/CD, don't change it. diff --git a/ariadne_lambda/base.py b/ariadne_lambda/base.py index a265768..e267a17 100644 --- a/ariadne_lambda/base.py +++ b/ariadne_lambda/base.py @@ -8,7 +8,7 @@ class GraphQLLambdaHandler(GraphQLHandler): @abstractmethod - async def handle(self, event: dict, context: LambdaContext): + async def handle(self, event: dict, context: LambdaContext): # ty: ignore[invalid-method-override] """An entrypoint for the AWS Lambda connection handler. This method is called by Ariadne AWS Lambda GraphQL application. Subclasses diff --git a/ariadne_lambda/http_handler.py b/ariadne_lambda/http_handler.py index bb4c453..4510bd2 100644 --- a/ariadne_lambda/http_handler.py +++ b/ariadne_lambda/http_handler.py @@ -1,6 +1,7 @@ import json +from collections.abc import Awaitable, Callable from inspect import isawaitable -from typing import Any +from typing import Any, cast from ariadne.constants import ( DATA_TYPE_JSON, @@ -27,7 +28,8 @@ class GraphQLAWSAPIHTTPGatewayHandler(GraphQLLambdaHandler): """Handler for AWS Lambda functions triggered by HTTP requests via API Gateway. Designed to process both Query and Mutation operations in a GraphQL schema. - Ideal for serverless architectures, providing a bridge between AWS Lambda and GraphQL. + Ideal for serverless architectures, providing a bridge between + AWS Lambda and GraphQL. """ def __init__( @@ -52,12 +54,16 @@ async def handle(self, event: dict, context: LambdaContext): return (await self.handle_request(request)).render() async def handle_request(self, request: Request) -> Response: - """Determines the request type (GET or POST) and routes to the corresponding GraphQL - processor. + """Determines the request type (GET or POST) and routes to + the corresponding GraphQL processor. Supports executing queries directly from GET requests, or handling introspection and GraphQL explorers.""" if request.method == "GET": - if self.execute_get_queries and request.params and request.params.get("query"): + if ( + self.execute_get_queries + and request.params + and request.params.get("query") + ): return await self.graphql_http_server(request) if self.introspection and self.explorer: # only render explorer when introspection is enabled @@ -80,9 +86,10 @@ async def render_explorer(self, request: Request, explorer: Explorer) -> Respons or `None`. If explorer returns `None`, `405` method not allowed response is returned instead. """ - explorer_html = explorer.html(request) - if isawaitable(explorer_html): - explorer_html = await explorer_html + explorer_html_result = explorer.html(request) + if isawaitable(explorer_html_result): + explorer_html_result = await explorer_html_result + explorer_html = cast(str | None, explorer_html_result) if explorer_html: return Response(body=explorer_html, headers={"Content-Type": "text/html"}) @@ -91,7 +98,8 @@ async def render_explorer(self, request: Request, explorer: Explorer) -> Respons async def graphql_http_server(self, request: Request) -> Response: """Executes GraphQL queries or mutations based on the POST request's body. - Parses the request, executes the GraphQL query, and formats the response as JSON. + Parses the request, executes the GraphQL query, + and formats the response as JSON. """ try: data = await self.extract_data_from_request(request) @@ -114,10 +122,12 @@ async def extract_data_from_request(self, request: Request): AWS Lambda's HTTP response format. Args: - request: A `Request` object containing the parsed HTTP request from API Gateway. + request: A `Request` object containing the parsed HTTP request + from API Gateway. Returns: - A `Response` object containing the JSON-formatted result of the GraphQL operation. + A `Response` object containing the JSON-formatted result + of the GraphQL operation. """ content_type = request.headers.get("content-type", "") content_type = content_type.split(";")[0] @@ -145,7 +155,8 @@ async def extract_data_from_json_request(self, request: Request) -> dict: Parses the JSON body of an HTTP request to extract the GraphQL query data. Args: - request: A `Request` object containing the parsed HTTP request from API Gateway. + request: A `Request` object containing the parsed HTTP request + from API Gateway. Returns: A dictionary containing the extracted GraphQL query data. @@ -159,14 +170,18 @@ async def extract_data_from_json_request(self, request: Request) -> dict: raise HttpBadRequestError("Request body is not a valid JSON") from ex async def extract_data_from_multipart_request(self, request: Request): - raise NotImplementedError("Multipart requests are not yet supported in AWS Lambda") + raise NotImplementedError( + "Multipart requests are not yet supported in AWS Lambda" + ) def extract_data_from_get_request(self, request: Request) -> dict: """ - Extracts the GraphQL query data from the query string parameters of a GET request. + Extracts the GraphQL query data from + the query string parameters of a GET request. Args: - request: A `Request` object containing the parsed HTTP request from API Gateway. + request: A `Request` object containing the parsed HTTP request + from API Gateway. Returns: A dictionary containing the GraphQL query, operation name, and variables. @@ -186,7 +201,9 @@ def extract_data_from_get_request(self, request: Request) -> dict: try: clean_variables = json.loads(variables) except (TypeError, ValueError) as ex: - raise HttpBadRequestError("Variables query arg is not a valid JSON") from ex + raise HttpBadRequestError( + "Variables query arg is not a valid JSON" + ) from ex return { "query": query, @@ -263,12 +280,20 @@ async def get_extensions_for_request( Returns: A list of extensions to be used during the execution of the GraphQL query. """ - if callable(self.extensions): - extensions = self.extensions(request, context) - if isawaitable(extensions): - extensions = await extensions # type: ignore + extensions = self.extensions + if not callable(extensions): return extensions - return self.extensions + + extensions_factory = cast( + Callable[ + [Any, ContextValue | None], ExtensionList | Awaitable[ExtensionList] + ], + extensions, + ) + resolved_extensions = extensions_factory(request, context) + if isawaitable(resolved_extensions): + resolved_extensions = await resolved_extensions + return cast(ExtensionList, resolved_extensions) async def create_json_response( self, @@ -277,7 +302,8 @@ async def create_json_response( success: bool, ) -> Response: """ - Formats the GraphQL execution result into a JSON response suitable for AWS Lambda. + Formats the GraphQL execution result into a JSON response + suitable for AWS Lambda. Args: request: The original request object. diff --git a/ariadne_lambda/schema.py b/ariadne_lambda/schema.py index b7d9724..3fe7d7b 100644 --- a/ariadne_lambda/schema.py +++ b/ariadne_lambda/schema.py @@ -1,4 +1,4 @@ -from typing import Any, Literal +from typing import Any, Literal, cast from pydantic import BaseModel @@ -23,30 +23,38 @@ def route_key(self): def create_from_event(cls, event: dict[str, Any]) -> "Request": # this is needed for API Gateway V1 when header keys comes capitalized # but on API Gateway V2 it comes as lowered - lowered_key_headers = {key.lower(): value for key, value in event["headers"].items()} - request_data = { - "event": event, - "body": "", - "is_base64_encoded": event["isBase64Encoded"], - "headers": lowered_key_headers, - "params": event.get("queryStringParameters", {}), - } + headers = cast(dict[str, str], event.get("headers") or {}) + lowered_key_headers = {key.lower(): value for key, value in headers.items()} + + query_params = cast(dict[str, str] | None, event.get("queryStringParameters")) + params = query_params or {} if http_context := event["requestContext"].get("http"): # Api Gateway V2 - request_data["path"] = http_context["path"] - request_data["method"] = http_context["method"].upper() - + path = cast(str, http_context["path"]) + raw_method = cast(str, http_context["method"]) else: # API Gateway V1 # Application Load Balancer - request_data["path"] = event["path"] - request_data["method"] = event["httpMethod"].upper() + path = cast(str, event["path"]) + raw_method = cast(str, event["httpMethod"]) + + method = cast( + Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"], + raw_method.upper(), + ) - if body := event.get("body"): - request_data["body"] = body + body = cast(str | None, event.get("body")) or "" - return cls(**request_data) + return cls( + event=event, + path=path, + method=method, + body=body, + is_base64_encoded=bool(event.get("isBase64Encoded", False)), + headers=lowered_key_headers, + params=params, + ) class Response: @@ -54,7 +62,9 @@ class Response: body: str headers: dict - def __init__(self, status_code: int = 200, body: str = "", headers: dict | None = None): + def __init__( + self, status_code: int = 200, body: str = "", headers: dict | None = None + ): self.status_code = status_code self.body = body if not headers: diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..273ded2 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,77 @@ +# GitHub integration is activated in CI via GITHUB_REPO env var. + +[changelog] +header = """# CHANGELOG + +All notable unreleased changes to this project will be documented in this file. + +For released versions, see the [Releases](https://github.com/mirumee/ariadne-lambda/releases) page. + +""" +body = """ +{% if version -%} +## {{ version }} ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else -%} +## Unreleased +{% endif -%} +{% set breaking_commits = commits | filter(attribute="breaking", value=true) -%} +{% if breaking_commits %} +### โš ๏ธ Breaking Changes +{% for commit in breaking_commits -%} +- **{{ commit.message | upper_first | trim }}**{% if commit.body %} + + {{ commit.body | trim | indent(prefix=" ") }} +{% endif %} +{% endfor %} +{%- endif %} +{%- for group, commits in commits | group_by(attribute="group") -%} +{%- set non_breaking = commits | filter(attribute="breaking", value=false) -%} +{%- if non_breaking and group != "_misc" %} +### {{ group }} +{% for commit in non_breaking -%} +- {{ commit.message | upper_first | trim }}{% if commit.remote.username %} (by @{{ commit.remote.username }}{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}](https://github.com/mirumee/ariadne-lambda/pull/{{ commit.remote.pr_number }}){% endif %}){% endif %} +{% endfor %} +{%- endif -%} +{% endfor %} +{% if version %} +## What's Changed +{% for commit in commits -%} +* {{ commit.message | upper_first | trim }}{% if commit.remote.username %} by @{{ commit.remote.username }}{% endif %}{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}](https://github.com/mirumee/ariadne-lambda/pull/{{ commit.remote.pr_number }}){% endif %} +{% endfor %} +{%- if github.contributors is defined -%} +{%- set new_contributors = github.contributors | filter(attribute="is_first_time", value=true) -%} +{%- if new_contributors | length != 0 %} +### New Contributors +{% for contributor in new_contributors -%} +* @{{ contributor.username }} made their first contribution{% if contributor.pr_number %} in [#{{ contributor.pr_number }}](https://github.com/mirumee/ariadne-lambda/pull/{{ contributor.pr_number }}){% endif %} +{% endfor %} +{%- endif -%} +{%- endif %} +**Full Changelog**: https://github.com/mirumee/ariadne-lambda/compare/{{ previous.version }}...{{ version }} +{% endif -%} +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +protect_breaking_commits = false +filter_commits = false +sort_commits = "oldest" +tag_pattern = "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$" +skip_tags = "\\.(rc|dev)[0-9]*$" +ignore_tags = "\\.(b|beta|alpha)[0-9]*$" + +commit_parsers = [ + { message = "^feat", group = "โœจ New Features" }, + { message = "^fix", group = "๐Ÿ› Bug Fixes" }, + { message = "^perf", group = "โšก Performance" }, + { message = "^refactor", group = "โ™ป๏ธ Refactoring" }, + { message = "^docs?", group = "๐Ÿ“š Documentation" }, + { message = "^build", group = "๐Ÿ› ๏ธ Build System" }, + { message = "^ci", group = "๐Ÿ‘ท CI/CD" }, + { message = "^test", group = "๐Ÿงช Testing" }, + { message = ".*", group = "_misc" }, +] diff --git a/pyproject.toml b/pyproject.toml index 4885563..56dbb0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,15 +4,13 @@ build-backend = "hatchling.build" [project] name = "ariadne-lambda" -version = "0.3.1" -description = 'This package extends the Ariadne library by adding a GraphQL HTTP handler designed for use in AWS Lambda environments.' +dynamic = ["version"] +description = "This package extends the Ariadne library by adding a GraphQL HTTP handler designed for use in AWS Lambda environments." +authors = [{ name = "Mirumee Software", email = "ariadne@mirumee.com" }] +requires-python = ">= 3.10" readme = "README.md" -requires-python = ">=3.10" -license = { file = "LICENSE" } -keywords = [] -authors = [ - { name = "Mirumee Software", email = "it@mirumee.com" }, -] +license = "BSD-3-Clause" +license-files = ["LICENSE"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -24,89 +22,152 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "ariadne>=0.23.0,<0.24.0", - "aws-lambda-powertools>=2.35.1,<3.0.0", - "jmespath", + "ariadne>=0.23.0,<0.30.0", + "aws-lambda-powertools>=3.0.0,<4.0.0", "pydantic>=2.4.0,<3.0.0", ] +[project.optional-dependencies] +dev = ["ipdb"] +test = [ + "pytest>=9.0.0,<10.0.0", + "pytest-asyncio>=1.0.0,<2.0.0", + "pytest-cov[toml]>=7.0.0,<8.0.0", +] +types = ["ty>=0.0.20,<0.1.0"] + [project.urls] -Documentation = "https://github.com/mirumee/ariadne-lambda#readme" -Issues = "https://github.com/mirumee/ariadne-lambda/issues" -Source = "https://github.com/mirumee/ariadne-lambda" +"Homepage" = "https://ariadnegraphql.org/" +"Repository" = "https://github.com/mirumee/ariadne-lambda" +"Bug Tracker" = "https://github.com/mirumee/ariadne-lambda/issues" +"Community" = "https://github.com/mirumee/ariadne/discussions" + +[tool.hatch.build] +include = ["ariadne_lambda/**/*.py"] +exclude = ["tests"] + +[tool.hatch.version] +path = "ariadne_lambda/__about__.py" [tool.hatch.envs.default] -dependencies = [ - "coverage[toml]>=6.5", - "pytest", - "pytest-asyncio", - "ruff", -] +python = "3.10" +features = ["dev", "types", "test"] +path = ".venv" [tool.hatch.envs.default.scripts] -test = "pytest {args:tests}" -test-cov = "coverage run -m pytest {args:tests}" -cov-report = [ - "- coverage combine", - "coverage report", -] -cov = [ - "test-cov", - "cov-report", +lint = ["hatch fmt --check", "hatch run types:check"] +check = [ + "hatch fmt", + "hatch test -a -p", + "hatch test --cover", + "hatch run types:check", ] +changelog-preview = "git cliff --unreleased --strip all" +changelog-update = "git cliff --unreleased -o CHANGELOG.md" +release-notes = "git cliff --latest --strip all" + +[tool.hatch.envs.types.scripts] +check = ["ty check"] + +[tool.hatch.envs.hatch-static-analysis] +config-path = "none" +dependencies = ["ruff==0.15.0"] -[[tool.hatch.envs.all.matrix]] +[tool.hatch.envs.hatch-test] +features = ["test"] +extra-args = [] + +[[tool.hatch.envs.hatch-test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] -[tool.hatch.envs.types] -dependencies = [ - "mypy>=1.0.0", -] -[tool.hatch.envs.types.scripts] -check = "mypy --install-types --non-interactive {args:ariadne_lambda tests}" +[tool.ty.environment] +python-version = "3.10" -[tool.coverage.run] -source_pkgs = ["ariadne_lambda", "tests"] -branch = true -parallel = true +[tool.ty.src] +include = ["ariadne_lambda", "tests"] +[tool.ty.rules] +unused-type-ignore-comment = "ignore" +redundant-cast = "ignore" -[tool.coverage.paths] -ariadne_lambda = ["ariadne_lambda"] -tests = ["tests", "*/ariadne-lambda/tests"] +[[tool.ty.overrides]] +include = ["tests/**"] + +[tool.ty.overrides.rules] +missing-argument = "ignore" +no-matching-overload = "ignore" +unresolved-attribute = "ignore" +invalid-argument-type = "ignore" +invalid-assignment = "ignore" +unknown-argument = "ignore" +not-iterable = "ignore" +possibly-missing-attribute = "ignore" + +[[tool.ty.overrides]] +include = [ + "tests/main/clients/*/expected_client/**", + "tests/main/graphql_schemas/*/expected_schema.py", +] + +[tool.ty.overrides.analysis] +allowed-unresolved-imports = ["**"] + +[tool.coverage.run] +source = ["ariadne_lambda", "tests"] [tool.coverage.report] -exclude_lines = [ - "no cov", +exclude_also = [ + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", "if __name__ == .__main__.:", "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", ] +omit = ["*/__about__.py", "*/__main__.py", "*/cli/__init__.py"] +# TODO: Enable this when we have more tests +# fail_under = 90 [tool.ruff] -line-length = 99 +line-length = 88 target-version = "py310" -# rules: https://beta.ruff.rs/docs/rules -# F - pyflakes -# E - pycodestyle -# G - flake8-logging-format -# I - isort -# N - pep8-naming -# Q - flake8-quotes -# UP - pyupgrade -# C90 - mccabe (complexity) -# T20 - flake8-print -# TID - flake8-tidy-imports +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 99 +exclude = ["tests/main/clients/*/expected_client/*"] [tool.ruff.lint] -select = ["E", "F", "G", "I", "N", "Q", "UP", "C90", "T20", "TID"] +select = ["E", "F", "G", "I", "N", "Q", "UP", "B", "C90", "SIM", "T20", "TID"] +ignore = ["TID252", "UP006", "UP007", "UP045"] +task-tags = ["NOTE", "TODO", "FIXME", "HACK", "XXX"] + +[tool.ruff.lint.per-file-ignores] +"tests/main/graphql_schemas/*/expected_schema.py" = ["E501"] +"tests/main/clients/*/expected_client/*" = ["E501", "N801", "F401"] + +[tool.ruff.lint.pycodestyle] +ignore-overlong-task-comments = true [tool.ruff.lint.mccabe] -max-complexity = 10 +max-complexity = 11 + +[tool.ruff.lint.isort] +known-first-party = ["ariadne_lambda"] + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tests/test_http_handler.py b/tests/test_http_handler.py index fc1fa8a..234dcd1 100644 --- a/tests/test_http_handler.py +++ b/tests/test_http_handler.py @@ -20,7 +20,9 @@ def handler(): @pytest.mark.asyncio async def test_handle(handler, api_gateway_v1_event_payload, lambda_context): # Given - mocked_handler_request = AsyncMock(return_value=Response(body="response", status_code=200)) + mocked_handler_request = AsyncMock( + return_value=Response(body="response", status_code=200) + ) handler.handle_request = mocked_handler_request # When @@ -37,7 +39,9 @@ async def test_handle_request_post_graphql(handler, api_gateway_v1_event_payload # Given request = Request.create_from_event(api_gateway_v1_event_payload) request.method = "POST" - mocked_graphql_http_server = AsyncMock(return_value=Response(body="response", status_code=200)) + mocked_graphql_http_server = AsyncMock( + return_value=Response(body="response", status_code=200) + ) handler.graphql_http_server = mocked_graphql_http_server # When @@ -46,7 +50,7 @@ async def test_handle_request_post_graphql(handler, api_gateway_v1_event_payload # Then mocked_graphql_http_server.assert_called_once() assert response.status_code == 200 - assert "response" == response.body + assert response.body == "response" @pytest.mark.asyncio @@ -54,7 +58,9 @@ async def test_handle_request_get_explorer(handler, api_gateway_v1_event_payload # Given handler.introspection = True handler.explorer = True - mocked_render_explorer = AsyncMock(return_value=Response(body="response", status_code=200)) + mocked_render_explorer = AsyncMock( + return_value=Response(body="response", status_code=200) + ) handler.render_explorer = mocked_render_explorer request = Request.create_from_event(api_gateway_v1_event_payload) @@ -64,7 +70,7 @@ async def test_handle_request_get_explorer(handler, api_gateway_v1_event_payload # Then mocked_render_explorer.assert_called_once() assert response.status_code == 200 - assert "response" == response.body + assert response.body == "response" @pytest.mark.asyncio @@ -87,10 +93,14 @@ async def test_handle_request_method_not_allowed(handler, api_gateway_v1_event_p @pytest.mark.asyncio -async def test_handle_request_get_graphql_http_server(handler, api_gateway_v1_event_payload): +async def test_handle_request_get_graphql_http_server( + handler, api_gateway_v1_event_payload +): # Given handler.execute_get_queries = True - mocked_graphql_http_server = AsyncMock(return_value=Response(body="response", status_code=200)) + mocked_graphql_http_server = AsyncMock( + return_value=Response(body="response", status_code=200) + ) handler.graphql_http_server = mocked_graphql_http_server request = Request.create_from_event(api_gateway_v1_event_payload) request.params = {"query": "hello"} @@ -101,7 +111,7 @@ async def test_handle_request_get_graphql_http_server(handler, api_gateway_v1_ev # Then mocked_graphql_http_server.assert_called_once() assert response.status_code == 200 - assert "response" == response.body + assert response.body == "response" @pytest.mark.asyncio diff --git a/tests/test_schema.py b/tests/test_schema.py index bf242cd..d61df72 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -3,7 +3,8 @@ def test_api_v1_event(api_gateway_v1_event_payload): lowered_keys_headers = { - key.lower(): value for key, value in api_gateway_v1_event_payload["headers"].items() + key.lower(): value + for key, value in api_gateway_v1_event_payload["headers"].items() } request = Request.create_from_event(api_gateway_v1_event_payload) assert request.method == api_gateway_v1_event_payload["httpMethod"] @@ -16,8 +17,13 @@ def test_api_v1_event(api_gateway_v1_event_payload): def test_api_v2_event(api_gateway_v2_event_payload): request = Request.create_from_event(api_gateway_v2_event_payload) - assert request.method == api_gateway_v2_event_payload["requestContext"]["http"]["method"] - assert request.path == api_gateway_v2_event_payload["requestContext"]["http"]["path"] + assert ( + request.method + == api_gateway_v2_event_payload["requestContext"]["http"]["method"] + ) + assert ( + request.path == api_gateway_v2_event_payload["requestContext"]["http"]["path"] + ) assert request.body == "" assert request.is_base64_encoded is False assert request.headers == api_gateway_v2_event_payload["headers"] @@ -31,7 +37,8 @@ def test_api_v2_event_lambda_url(api_gateway_v2_lambda_url_event_payload): == api_gateway_v2_lambda_url_event_payload["requestContext"]["http"]["method"] ) assert ( - request.path == api_gateway_v2_lambda_url_event_payload["requestContext"]["http"]["path"] + request.path + == api_gateway_v2_lambda_url_event_payload["requestContext"]["http"]["path"] ) assert request.body == "" assert request.is_base64_encoded is False @@ -41,7 +48,9 @@ def test_api_v2_event_lambda_url(api_gateway_v2_lambda_url_event_payload): def test_response_initialization(): # When - response = Response(status_code=200, body="OK", headers={"Content-Type": "application/json"}) + response = Response( + status_code=200, body="OK", headers={"Content-Type": "application/json"} + ) # Then assert response.status_code == 200 @@ -61,7 +70,9 @@ def test_response_default_values(): def test_response_iter(): # When - response = Response(status_code=404, body="Not Found", headers={"X-Custom-Header": "value"}) + response = Response( + status_code=404, body="Not Found", headers={"X-Custom-Header": "value"} + ) response_dict = dict(response) # Then @@ -74,7 +85,9 @@ def test_response_iter(): def test_response_render(): # When - response = Response(status_code=500, body="Error", headers={"Content-Type": "text/plain"}) + response = Response( + status_code=500, body="Error", headers={"Content-Type": "text/plain"} + ) response_rendered = response.render() # Then