From c2083de204f5cc2a9e1648e358a6d9b4514854ae Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Sun, 8 Jan 2023 18:33:37 +0400 Subject: [PATCH] :bookmark: Version 2.0.0 of FastAPI Class (#58) * :bookmark: Version 2.0.0 of FastAPI Class * :bug: Fix Directory name issue * :pushpin: Drop python 3.7 * :bug: unsupported `DefaultPlaceholder` * :bug: fix * :memo: Add Documentation --- .coveragerc | 8 -- .github/CODEOWNERS | 1 + .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/config.yml | 4 - .github/dependabot.yml | 21 +++- .github/workflows/codeql.yml | 41 -------- .github/workflows/lint.yaml | 29 ++++++ .github/workflows/publish.yml | 28 +++-- .github/workflows/test.yml | 40 +++----- .pre-commit-config.yaml | 86 ++++++++-------- LICENSE | 2 +- README.md | 163 ++++++++++++++++++++++-------- Scripts/clean.sh | 18 ++++ Scripts/format.sh | 6 ++ Scripts/lint.sh | 6 ++ Scripts/test.sh | 9 ++ Scripts/test_html.sh | 9 ++ codecov.yml | 8 ++ fastapi_class/__init__.py | 6 +- fastapi_class/args.py | 43 +------- fastapi_class/decorators.py | 2 +- fastapi_class/routable.py | 2 +- mypy.ini | 11 ++ pyproject.toml | 79 +++++++++++---- pytest.ini | 8 ++ setup.cfg | 9 ++ tests/conftest.py | 10 -- tests/test_version.py | 7 ++ 28 files changed, 397 insertions(+), 261 deletions(-) delete mode 100644 .coveragerc create mode 100644 .github/CODEOWNERS create mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/lint.yaml create mode 100644 Scripts/clean.sh create mode 100644 Scripts/format.sh create mode 100644 Scripts/lint.sh create mode 100644 Scripts/test.sh create mode 100644 Scripts/test_html.sh create mode 100644 codecov.yml create mode 100644 mypy.ini create mode 100644 pytest.ini create mode 100644 setup.cfg delete mode 100644 tests/conftest.py create mode 100644 tests/test_version.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 2724a37..0000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] -branch = True - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c5c187a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Yezz123 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7b4400a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +github: [yezz123] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 487085a..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,4 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Security Contact - about: Please report security vulnerabilities to yasserth19@pm.me \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 188317f..cf9a267 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,21 @@ version: 2 + updates: - - package-ecosystem: "pip" # See documentation for possible values - directory: "/" # Location of package manifests + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: "daily" \ No newline at end of file + interval: "daily" + reviewers: + - yezz123 + commit-message: + prefix: ⬆ + # Python + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + reviewers: + - yezz123 + commit-message: + prefix: ⬆ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 7e65781..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: "33 13 * * 4" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ python ] - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..3baeb67 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,29 @@ +name: Lint and Format + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize] + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + fail-fast: false + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: pip install -e .[lint] + - name: Lint + run: bash Scripts/format.sh + - name: check Static Analysis + run: bash Scripts/lint.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6a8076d..de33dda 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish to PyPI +name: Publish on: release: @@ -13,28 +13,26 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: "3.9" - - uses: actions/cache@v2 + python-version: "3.10" + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-publish - - name: Install Flit + - name: Install build dependencies if: steps.cache.outputs.cache-hit != 'true' - run: pip install flit - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: flit install --symlink + run: pip install build + - name: Build distribution + run: python -m build - name: Publish - env: - FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }} - FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: flit publish + uses: pypa/gh-action-pypi-publish@v1.6.4 + with: + password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" \ No newline at end of file + run: echo "$GITHUB_CONTEXT" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df6a7d1..7d595ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Build +name: Test Suite on: push: @@ -8,44 +8,28 @@ on: types: [opened, synchronize] jobs: - test: + tests: runs-on: ubuntu-latest - services: - redis: - image: redis - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + timeout-minutes: 30 strategy: matrix: - python-version: [3.8, 3.9, 3.10.0] - fail-fast: false + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test - - name: Install Flit - if: steps.cache.outputs.cache-hit != 'true' - run: pip install flit + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v02 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - run: flit install --symlink - - name: Lint - if: ${{ matrix.python-version != '3.9' }} && ${{ matrix.python-version != '3.8' }} - run: pre-commit run --all-files - - name: Test - run: python -m pytest --cov=fastapi_class + run: pip install -e .[test] + - name: Test with pytest + run: bash Scripts/test.sh - name: Upload coverage - uses: codecov/codecov-action@v1 \ No newline at end of file + uses: codecov/codecov-action@v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68c854b..96f097c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,44 +1,44 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 - hooks: - - id: check-merge-conflict - - id: check-added-large-files - - id: check-ast - - id: check-symlinks - - id: trailing-whitespace - - id: check-json - - id: debug-statements - - id: pretty-format-json - args: ["--autofix", "--allow-missing-credentials"] - - repo: https://github.com/PyCQA/isort - rev: 5.10.1 - hooks: - - id: isort - args: ["--profile", "black"] - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: [flake8-print] - files: '\.py$' - exclude: docs/ - args: - - --select=F403,F406,F821,T003 - - repo: https://github.com/humitos/mirrors-autoflake - rev: v1.3 - hooks: - - id: autoflake - files: '\.py$' - exclude: '^\..*' - args: ["--in-place"] - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - args: ["--target-version", "py38"] - - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 - hooks: - - id: pyupgrade - args: [--py37-plus] \ No newline at end of file +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/asottile/pyupgrade + rev: v3.2.2 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.138 + hooks: + - id: ruff + args: + - --fix +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] +- repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/LICENSE b/LICENSE index ed6c1ac..3514fb4 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 338e480..bfba88e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,23 @@ -# Fastapi-Class 🦜 - ![Class](https://user-images.githubusercontent.com/52716203/137606695-f110f129-08b1-45f3-a445-962c1f28378c.png)

Classes and Decorators to use FastAPI with Class based routing

-[![codecov](https://codecov.io/gh/yezz123/fastapi-class/branch/main/graph/badge.svg?token=1W73kO30IL)](https://codecov.io/gh/yezz123/fastapi-class) -[![Testing](https://github.com/yezz123/fastapi-class/actions/workflows/test.yml/badge.svg)](https://github.com/yezz123/fastapi-class/actions/workflows/test.yml) -[![PyPI version](https://badge.fury.io/py/fastapi-class.svg)](https://badge.fury.io/py/fastapi-class) -[![Downloads](https://pepy.tech/badge/fastapi-class)](https://pepy.tech/project/fastapi-class) -[![Language](https://img.shields.io/badge/Language-Python-green?style)](https://github.com/yezz123) -[![framework](https://img.shields.io/badge/Framework-FastAPI-blue?style)](https://fastapi.tiangolo.com/) -[![Star Badge](https://img.shields.io/static/v1?label=%F0%9F%8C%9F&message=If%20Useful&style=style=flatcolor=BC4E99)](https://github.com/yezz123/fastapi-class) -[![Pypi](https://img.shields.io/pypi/pyversions/fastapi-class.svg?color=%2334D058)](https://pypi.org/project/fastapi-class) +

+ + Test + + + + + + Package version + + + Supported Python versions + +

--- @@ -23,61 +27,83 @@ --- -Classes and Decorators to use FastAPI with `class based routing`. In particular this allows you to -construct an **instance** of a class and have methods of that instance be route handlers for FastAPI & Python 3.8. +This package provides classes and decorators to use FastAPI with class based routing in Python 3.8. This allows you to construct an instance of a class and have methods of that instance be route handlers for FastAPI. + +**Note**: This package does not support async routes with Python versions less than 3.8 due to bugs in [`inspect.iscoroutinefunction`](https://stackoverflow.com/a/52422903/1431244). Specifically, with older versions of Python `iscoroutinefunction` incorrectly returns false so async routes are not awaited. As a result, this package only supports Python versions >= 3.8. + +To get started, install the package using pip: -- Older Versions of Python: - - Unfortunately this does not work with `async` routes with Python versions less than 3.8 [due to bugs in `inspect.iscoroutinefunction`](https://stackoverflow.com/a/52422903/1431244). Specifically with older versions of Python `iscoroutinefunction` incorrectly returns false so `async` routes aren't `await`'d. We therefore only support Python versions >= 3.8. +```sh +pip install fastapi-class +``` + +### Example + +let's imagine that this code is part of a system that manages a list of users. The `Dao` class represents a Data Access Object, which is responsible for storing and retrieving user data from a database. -## Example 🐢 +The `UserRoutes` class is responsible for defining the routes (i.e., the URL paths) that users can access to perform various actions on the user data. + +Here's how the code could be used in a real world scenario: ```py -from ping import pong -# Some fictional ping pong class -from fastapi_class import Routable, get, delete +import argparse + +from dao import Dao +from fastapi import FastAPI + +from fastapi_class.decorators import delete, get +from fastapi_class.routable import Routable + def parse_arg() -> argparse.Namespace: - """parse command line arguments.""" - ... + """parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Example of FastAPI class based routing." + ) + parser.add_argument("--url", type=str, help="URL to connect to.") + parser.add_argument("--user", type=str, help="User to connect with.") + parser.add_argument("--password", type=str, help="Password to connect with.") + return parser.parse_args() class UserRoutes(Routable): - """Inherits from Routable.""" + """Inherits from Routable.""" - # Note injection here by simply passing values to the constructor. - # Other injection frameworks also work. - # supported as there's nothing special about this __init__ method. - def __init__(self, pong: pong) -> None: - """Constructor. The pong is injected here.""" - self.__pong = pong + # Note injection here by simply passing values to the constructor. Other injection frameworks also + # supported as there's nothing special about this __init__ method. + def __init__(self, dao: Dao) -> None: + """Constructor. The Dao is injected here.""" + super().__init__() + self.__dao = Dao - @get('/user/{name}') - def get_user_by_name(name: str) -> User: - # Use our injected pong instance. - return self.__pong.get_user_by_name(name) + @get("/user/{name}") + def get_user_by_name(self, name: str) -> str: + # Use our injected DAO instance. + return self.__dao.get_user_by_name(name) - @delete('/user/{name}') - def delete_user(name: str) -> None: - self.__pong.delete(name) + @delete("/user/{name}") + def delete_user(self, name: str) -> None: + self.__dao.delete(name) def main(): - args = parse_args() - # Configure the pong per command line arguments - pong = pong(args.url, args.user, args.password) + args = parse_arg() + # Configure the DAO per command line arguments + dao = Dao(args.url, args.user, args.password) # Simple intuitive injection - user_routes = UserRoutes(pong) - + user_routes = UserRoutes(dao) app = FastAPI() - # router member inherited from Routable and configured per the annotations. + # router member inherited from cr.Routable and configured per the annotations. app.include_router(user_routes.router) ``` -## Why 🐣 +## Explanation FastAPI generally has one define routes like: ```py +from fastapi import FastAPI + app = FastAPI() @app.get('/echo/{x}') @@ -85,9 +111,11 @@ def echo(x: int) -> int: return x ``` -__Note:__ that `app` is a global. Furthermore, [FastAPI's suggested way of doing dependency injection](https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/) is handy for things like pulling values out of header in the HTTP request. However, they don't work well for more standard dependency injection scenarios where we'd like to do something like inject a Data Access Object or database connection. For that, FastAPI suggests [their parameterized dependencies](https://fastapi.tiangolo.com/advanced/advanced-dependencies/) which might look something like: +**Note**: that `app` is a global. Furthermore, [FastAPI's suggested way of doing dependency injection](https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/) is handy for things like pulling values out of header in the HTTP request. However, they don't work well for more standard dependency injection scenarios where we'd like to do something like inject a Data Access Object or database connection. For that, FastAPI suggests [their parameterized dependencies](https://fastapi.tiangolo.com/advanced/advanced-dependencies/) which might look something like: ```py +from fastapi import FastAPI + app = FastAPI() class ValueToInject: @@ -105,6 +133,55 @@ def add(x: int, y: Depends(to_add)) -> int: return x + y ``` -## License 🍻 +## Development 🚧 + +### Setup environment 📦 + +You should create a virtual environment and activate it: + +```bash +python -m venv venv/ +``` + +```bash +source venv/bin/activate +``` + +And then install the development dependencies: + +```bash +# Install dependencies +pip install -e .[test,lint] +``` + +### Run tests 🌝 + +You can run all the tests with: + +```bash +bash scripts/test.sh +``` + +> Note: You can also generate a coverage report with: + +```bash +bash scripts/test_html.sh +``` + +### Format the code 🍂 + +Execute the following command to apply `pre-commit` formatting: + +```bash +bash scripts/format.sh +``` + +Execute the following command to apply `mypy` type checking: + +```bash +bash scripts/lint.sh +``` + +## License This project is licensed under the terms of the MIT license. diff --git a/Scripts/clean.sh b/Scripts/clean.sh new file mode 100644 index 0000000..b15b254 --- /dev/null +++ b/Scripts/clean.sh @@ -0,0 +1,18 @@ +#!/bin/sh -e + +rm -f `find . -type f -name '*.py[co]' ` +rm -f `find . -type f -name '*~' ` +rm -f `find . -type f -name '.*~' ` +rm -f `find . -type f -name .coverage` +rm -f `find . -type f -name ".coverage.*"` +rm -rf `find . -name __pycache__` +rm -rf `find . -type d -name '*.egg-info' ` +rm -rf `find . -type d -name 'pip-wheel-metadata' ` +rm -rf `find . -type d -name .pytest_cache` +rm -rf `find . -type d -name .ruff_cache` +rm -rf `find . -type d -name .cache` +rm -rf `find . -type d -name .mypy_cache` +rm -rf `find . -type d -name htmlcov` +rm -rf `find . -type d -name "*.egg-info"` +rm -rf `find . -type d -name build` +rm -rf `find . -type d -name dist` diff --git a/Scripts/format.sh b/Scripts/format.sh new file mode 100644 index 0000000..f423597 --- /dev/null +++ b/Scripts/format.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e +set -x + +pre-commit run --all-files --verbose --show-diff-on-failure diff --git a/Scripts/lint.sh b/Scripts/lint.sh new file mode 100644 index 0000000..f2e1e26 --- /dev/null +++ b/Scripts/lint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e +set -x + +mypy --show-error-codes fastapi_class tests diff --git a/Scripts/test.sh b/Scripts/test.sh new file mode 100644 index 0000000..b30ed17 --- /dev/null +++ b/Scripts/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e +set -x + +echo "ENV=${ENV}" + +export PYTHONPATH=. +pytest --cov=fastapi_class --cov=tests --cov-report=term-missing --cov-fail-under=80 diff --git a/Scripts/test_html.sh b/Scripts/test_html.sh new file mode 100644 index 0000000..cf56a63 --- /dev/null +++ b/Scripts/test_html.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e +set -x + +echo "ENV=${ENV}" + +export PYTHONPATH=. +pytest --cov=fastapi_class --cov=tests --cov-report=html diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..eaa8e06 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ + +coverage: + status: + project: + default: + # basic + target: auto + threshold: 100% diff --git a/fastapi_class/__init__.py b/fastapi_class/__init__.py index 19ca353..17d8a72 100644 --- a/fastapi_class/__init__.py +++ b/fastapi_class/__init__.py @@ -1,8 +1,6 @@ -""" - FastAPI_Class : contains classes and decorators to use FastAPI with "class based routing". -""" +"""contains classes and decorators to use FastAPI with "class based routing".""" -__version__ = "1.2.0" +__version__ = "2.0.0" from fastapi_class import args, decorators, routable diff --git a/fastapi_class/args.py b/fastapi_class/args.py index 6f20bce..f083a3e 100644 --- a/fastapi_class/args.py +++ b/fastapi_class/args.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union from fastapi import Response, params @@ -10,41 +10,11 @@ SetIntStr = Set[Union[int, str]] DictIntStrAny = Dict[Union[int, str], Any] -""" - args: This file contains the definition of the arguments that are passed to the functions. -""" - @dataclass class RouteArgs: """ This class contains the arguments that are passed to the functions. - - Attributes: - path: str - response_model: Optional[Type[Any]] = None - status_code: Optional[int] = None - tags: Optional[List[str]] = None - dependencies: Optional[Sequence[params.Depends]] = None - summary: Optional[str] = None - description: Optional[str] = None - response_description: str = "Successful Response" - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None - deprecated: Optional[bool] = None - methods: Optional[Union[Set[str], List[str]]] = None - operation_id: Optional[str] = None - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None - response_model_by_alias: bool = True - response_model_exclude_unset: bool = False - response_model_exclude_defaults: bool = False - response_model_exclude_none: bool = False - include_in_schema: bool = True - response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse) - name: Optional[str] = None - route_class_override: Optional[Type[APIRoute]] = None - callbacks: Optional[List[Route]] = None - openapi_extra: Optional[Dict[str, Any]] = None """ path: str @@ -66,24 +36,21 @@ class RouteArgs: response_model_exclude_defaults: bool = False response_model_exclude_none: bool = False include_in_schema: bool = True - response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse) + response_class: Union[Type[Response], DefaultPlaceholder] = field( + default_factory=lambda: Default(JSONResponse) + ) name: Optional[str] = None route_class_override: Optional[Type[APIRoute]] = None callbacks: Optional[List[Route]] = None openapi_extra: Optional[Dict[str, Any]] = None class Config: - """ - Config for `RouteArgs`. - """ - arbitrary_types_allowed = True @dataclass class EndpointDefinition: - """RouteArgs plus the endpoint. - + """ Endpoint is separate as it has to be bound to self before it can be used. """ diff --git a/fastapi_class/decorators.py b/fastapi_class/decorators.py index 837585e..49ede9d 100644 --- a/fastapi_class/decorators.py +++ b/fastapi_class/decorators.py @@ -31,7 +31,7 @@ def marker(method: AnyCallable) -> AnyCallable: if not args.description: description = inspect.cleandoc(method.__doc__ or "") args.description = description or " " - setattr(method, "_endpoint", EndpointDefinition(endpoint=method, args=args)) + method._endpoint = EndpointDefinition(endpoint=method, args=args) # type: ignore return method return marker diff --git a/fastapi_class/routable.py b/fastapi_class/routable.py index 252abcd..112a93b 100644 --- a/fastapi_class/routable.py +++ b/fastapi_class/routable.py @@ -38,7 +38,7 @@ class Routable(metaclass=RoutableMeta): _endpoints: List[EndpointDefinition] = [] - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore self.router = APIRouter(*args, **kwargs) for endpoint in self._endpoints: self.router.add_api_route( diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..79cdd11 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,11 @@ +[mypy] +plugins=pydantic.mypy + +follow_imports = silent +strict_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +disallow_any_generics = True +check_untyped_defs = True +ignore_missing_imports = True +disallow_untyped_defs = True diff --git a/pyproject.toml b/pyproject.toml index 261717b..35ccab5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,17 @@ [build-system] -requires = ["flit"] -build-backend = "flit.buildapi" - -[tool.flit.metadata] -module = "fastapi_class" -dist-name = "fastapi_class" -author = "Yasser Tahiri" -author-email = "yasserth19@gmail.com" -home-page = "https://github.com/yezz123/fastapi-class" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "fastapi_class" +description = "Classes and Decorators to use FastAPI with Class based routing" +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +authors = [ + { name = "Yasser Tahiri", email = "hello@yezz.me" }, +] + classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", @@ -18,27 +22,60 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Topic :: Internet :: WWW/HTTP :: Session", "Typing :: Typed", ] -description-file = "README.md" -requires-python = ">=3.8" -requires = [ - "fastapi==0.75.1", - "pydantic==1.9.0" + +dependencies = [ + "fastapi >=0.65.2,<0.87.0", + "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] -[tool.flit.metadata.requires-extra] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/yezz123/fastapi-class" +Funding = 'https://github.com/sponsors/yezz123' + +[project.optional-dependencies] lint = [ - "pre-commit==2.16.0", + "pre-commit==2.21.0", + "mypy==0.991", ] test = [ - "pytest==7.1.1", "requests==2.27.1", - "codecov", - "pytest-cov" + "pytest==7.2.0", + "pytest-asyncio == 0.20.3", + "codecov==2.1.12", + "pytest-cov==4.0.0", ] -[tool.flit.metadata.urls] -Documentation = "https://github.com/yezz123/fastapi-class/blob/main/README.md" \ No newline at end of file +[tool.hatch.version] +path = "fastapi_class/__init__.py" + +[tool.isort] +profile = "black" +known_third_party = ["pydantic", "typing_extensions"] + +[tool.ruff] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.isort] +known-third-party = ["pydantic", "typing_extensions"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..416776e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +testpaths = + tests/ +log_cli = 1 +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format=%Y-%m-%d %H:%M:%S +asyncio_mode=auto diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2077b9a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[coverage:report] +precision = 2 +exclude_lines = + pragma: no cover + raise NotImplementedError + raise NotImplemented + @overload + if TYPE_CHECKING: + if __name__ == "__main__": diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 31caaa5..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import asyncio - -import pytest - - -@pytest.fixture -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - loop.close() diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..2de5125 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,7 @@ +import fastapi_class + +__version__ = "2.0.0" + + +def test_version() -> None: + assert fastapi_class.__version__ == __version__