Skip to content

Commit

Permalink
🐛 Fix issue with @endpoint decorator & Setup uv (#182)
Browse files Browse the repository at this point in the history
🐛 Fix issue with `@endpoint` decorator & Setup `uv`
  • Loading branch information
yezz123 committed Apr 20, 2024
2 parents 9cbf5b8 + 680b9b1 commit c7a8be0
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 109 deletions.
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/action@v3.0.1

- 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

0 comments on commit c7a8be0

Please sign in to comment.