Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix issue with @endpoint decorator & Setup uv #182

Merged
merged 11 commits into from
Apr 20, 2024
35 changes: 26 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: pip install -e .[lint]
- uses: pre-commit/[email protected]

- name: setup uv
uses: yezz123/setup-uv@v4
with:
extra_args: --all-files --verbose
- name: Run mypy
uv-venv: ".venv"

- name: Install Dependencies
run: uv pip install -r requirements/pyproject.txt && uv pip install -r requirements/linting.txt

- name: Run Pre-commit
run: bash scripts/format.sh

- name: Run Mypy
run: bash scripts/lint.sh

tests:
Expand All @@ -51,17 +58,27 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: setup UV
uses: yezz123/setup-uv@v4
with:
uv-venv: ".venv"

- name: Install Dependencies
run: pip install -e .[test]
run: uv pip install -r requirements/pyproject.txt && uv pip install -r requirements/testing.txt

- name: Freeze Dependencies
run: pip freeze
run: uv pip freeze

- name: Test with pytest
- name: Test with pytest - ${{ matrix.os }} - py${{ matrix.python-version }}
run: bash scripts/test.sh
env:
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-with-deps

- name: Upload coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml

# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
check:
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-toml
Expand All @@ -10,7 +10,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.2.0
rev: v0.4.1
hooks:
- id: ruff
args:
Expand Down
48 changes: 25 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,39 @@ A common question people have as they become more comfortable with FastAPI is ho
- Example:

```python
from fastapi import FastAPI, APIRouter, Query
from fastapi import FastAPI, Query
from pydantic import BaseModel
from fastapi_class import View

app = FastAPI()
router = APIRouter()

class ItemModel(BaseModel):
id: int
name: str
description: str = None

@View(router)
@View(app)
class ItemView:
def post(self, item: ItemModel):
async def post(self, item: ItemModel):
return item

def get(self, item_id: int = Query(..., gt=0)):
async def get(self, item_id: int = Query(..., gt=0)):
return {"item_id": item_id}

app.include_router(router)
```

### Response model 📦

`Exception` in list need to be either function that return `fastapi.HTTPException` itself. In case of a function it is required to have all of it's arguments to be `optional`.

```py
from fastapi import FastAPI, APIRouter, HTTPException, status
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel

from fastapi_class import View

app = FastAPI()
router = APIRouter()

NOT_AUTHORIZED = HTTPException(401, "Not authorized.")
NOT_ALLOWED = HTTPException(405, "Method not allowed.")
Expand All @@ -85,7 +82,7 @@ NOT_FOUND = lambda item_id="item_id": HTTPException(404, f"Item with {item_id} n
class ItemResponse(BaseModel):
field: str | None = None

@View(router)
@View(app)
class MyView:
exceptions = {
"__all__": [NOT_AUTHORIZED],
Expand All @@ -100,29 +97,26 @@ class MyView:
"delete": PlainTextResponse
}

def get(self):
async def get(self):
...

def put(self):
async def put(self):
...

def delete(self):
async def delete(self):
...

app.include_router(router)
```

### Customized Endpoints

```py
from fastapi import FastAPI, APIRouter, HTTPException
from fastapi import FastAPI, HTTPException
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel

from fastapi_class import View, endpoint

app = FastAPI()
router = APIRouter()

NOT_AUTHORIZED = HTTPException(401, "Not authorized.")
NOT_ALLOWED = HTTPException(405, "Method not allowed.")
Expand All @@ -132,7 +126,7 @@ EXCEPTION = HTTPException(400, "Example.")
class UserResponse(BaseModel):
field: str | None = None

@View(router)
@View(app)
class MyView:
exceptions = {
"__all__": [NOT_AUTHORIZED],
Expand All @@ -149,17 +143,17 @@ class MyView:
"delete": PlainTextResponse
}

def get(self):
async def get(self):
...

def put(self):
async def put(self):
...

def delete(self):
async def delete(self):
...

@endpoint(("PUT",), path="edit")
def edit(self):
@endpoint(("PUT"), path="edit")
async def edit(self):
...
```

Expand All @@ -182,9 +176,17 @@ source venv/bin/activate

And then install the development dependencies:

__Note:__ You should have `uv` installed, if not you can install it with:

```bash
pip install uv
```

Then you can install the dependencies with:

```bash
# Install dependencies
pip install -e .[test,lint]
uv pip install -r requirements/all.txt
```

### Run tests 🌝
Expand Down
5 changes: 2 additions & 3 deletions fastapi_class/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,13 @@ class ItemView:
async def get(self, query: str = Query(), limit: int = 50, offset: int = 0):
pass

def post(self, user: ItemModel):
async def post(self, user: ItemModel):
pass
```

"""


__version__ = "3.5.0"
__version__ = "3.6.0"

from fastapi_class.exception import FormattedMessageException
from fastapi_class.openapi import ExceptionModel, _exceptions_to_responses
Expand Down
7 changes: 1 addition & 6 deletions fastapi_class/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@ def _exceptions_to_responses(
"""
Convert exceptions to responses.

:param exceptions: exceptions
:return: responses

:raise TypeError: if exception is not an instance of HTTPException or a factory function

:example:
### example
>>> from fastapi import HTTPException, status
>>> from fastapi_class import _exceptions_to_responses
>>> _exceptions_to_responses([HTTPException(status.HTTP_400_BAD_REQUEST, detail="Bad request")])
Expand Down
52 changes: 28 additions & 24 deletions fastapi_class/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@


class Method(str, Enum):
"""
HTTP methods.
"""

GET = "get"
POST = "post"
PATCH = "patch"
Expand All @@ -20,6 +24,10 @@ class Method(str, Enum):

@dataclass(frozen=True, init=True, repr=True)
class Metadata:
"""
Metadata class, used to store endpoint metadata.
"""

methods: Iterable[str | Method]
name: str | None = None
path: str | None = None
Expand All @@ -29,6 +37,9 @@ class Metadata:
__default_method_suffix: ClassVar[str] = "_or_default"

def __getattr__(self, __name: str) -> Any | Callable[[Any], Any]:
"""
Dynamically return the value of the attribute.
"""
if __name.endswith(Metadata.__default_method_suffix):
prefix = __name.replace(Metadata.__default_method_suffix, "")
if hasattr(self, prefix):
Expand All @@ -47,30 +58,16 @@ def endpoint(
response_class: type[Response] | None = None,
):
"""
Endpoint decorator.

:param methods: methods
:param name: name
:param path: path
:param status_code: status code
:param response_model: response model
:param response_class: response class

:raise AssertionError: if response model or response class is not a subclass of BaseModel or Response respectively
:raise AssertionError: if methods is not an iterable of strings or Method enums

:example:
>>> from fastapi import FastAPI
>>> from fastapi_class import endpoint
>>> app = FastAPI()
>>> @endpoint()
... def get():
... return {"message": "Hello, world!"}
>>> app.include_router(get)

Results:

`GET /get`
Endpoint decorator for FastAPI.

### Example:
>>> from fastapi import FastAPI
>>> from fastapi_class import endpoint
>>> app = FastAPI()
>>> @endpoint()
... async def get():
... return {"message": "Hello, world!"}
>>> app.include_router(get)
"""
assert all(
issubclass(_type, expected_type)
Expand All @@ -85,8 +82,15 @@ def endpoint(
), "Methods must be an string, iterable of strings or Method enums."

def _decorator(function: Callable):
"""
Decorate the function.
"""

@wraps(function)
async def _wrapper(*args, **kwargs):
"""
Wrapper for the function.
"""
return await function(*args, **kwargs)

parsed_method = set()
Expand Down
35 changes: 12 additions & 23 deletions fastapi_class/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from fastapi_class.routers import Metadata, Method

COMMON_KEYWORD = "common"
RESPONSE_MODEL_ATTRIBUTE_NAME = "RESPONSE_MODEL"
RESPONSE_CLASS_ATTRIBUTE_NAME = "RESPONSE_CLASS"
ENDPOINT_METADATA_ATTRIBUTE_NAME = "ENDPOINT_METADATA"
RESPONSE_MODEL_ATTRIBUTE_NAME = "response_model"
RESPONSE_CLASS_ATTRIBUTE_NAME = "response_class"
ENDPOINT_METADATA_ATTRIBUTE_NAME = "__endpoint_metadata"
EXCEPTIONS_ATTRIBUTE_NAME = "EXCEPTIONS"


Expand All @@ -29,28 +29,17 @@ def View(
name_parser: Callable[[object, str], str] = _view_class_name_default_parser,
):
"""
Class-based view decorator.
Class-based view decorator for FastAPI.

:param router: router
:param path: path
:param default_status_code: default status code
:param name_parser: name parser
### Example:
>>> from fastapi import FastAPI
>>> from fastapi_class import View

:raise AssertionError: if router is not an instance of FastAPI or APIRouter

:example:
>>> from fastapi import FastAPI
>>> from fastapi_class import View
>>> app = FastAPI()
>>> @View(app)
... class MyView:
... def get(self):
... return {"message": "Hello, world!"}
>>> app.include_router(MyView.router)

Results:

`GET /my-view`
>>> app = FastAPI()
>>> @View(app)
... class MyView:
... async def get(self):
... return {"message": "Hello, world!"}
"""

def _decorator(cls) -> None:
Expand Down
Loading
Loading