Skip to content

Commit c7a8be0

Browse files
authored
🐛 Fix issue with @endpoint decorator & Setup uv (#182)
🐛 Fix issue with `@endpoint` decorator & Setup `uv`
2 parents 9cbf5b8 + 680b9b1 commit c7a8be0

17 files changed

+235
-109
lines changed

.github/workflows/ci.yml

+26-9
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ jobs:
2020
uses: actions/setup-python@v5
2121
with:
2222
python-version: ${{ matrix.python-version }}
23-
- name: Install Dependencies
24-
run: pip install -e .[lint]
25-
- uses: pre-commit/[email protected]
23+
24+
- name: setup uv
25+
uses: yezz123/setup-uv@v4
2626
with:
27-
extra_args: --all-files --verbose
28-
- name: Run mypy
27+
uv-venv: ".venv"
28+
29+
- name: Install Dependencies
30+
run: uv pip install -r requirements/pyproject.txt && uv pip install -r requirements/linting.txt
31+
32+
- name: Run Pre-commit
33+
run: bash scripts/format.sh
34+
35+
- name: Run Mypy
2936
run: bash scripts/lint.sh
3037

3138
tests:
@@ -51,17 +58,27 @@ jobs:
5158
with:
5259
python-version: ${{ matrix.python-version }}
5360

61+
- name: setup UV
62+
uses: yezz123/setup-uv@v4
63+
with:
64+
uv-venv: ".venv"
65+
5466
- name: Install Dependencies
55-
run: pip install -e .[test]
67+
run: uv pip install -r requirements/pyproject.txt && uv pip install -r requirements/testing.txt
5668

5769
- name: Freeze Dependencies
58-
run: pip freeze
70+
run: uv pip freeze
5971

60-
- name: Test with pytest
72+
- name: Test with pytest - ${{ matrix.os }} - py${{ matrix.python-version }}
6173
run: bash scripts/test.sh
74+
env:
75+
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-with-deps
6276

63-
- name: Upload coverage
77+
- name: Upload coverage to Codecov
6478
uses: codecov/codecov-action@v4
79+
with:
80+
token: ${{ secrets.CODECOV_TOKEN }}
81+
file: ./coverage.xml
6582

6683
# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
6784
check:

.pre-commit-config.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.5.0
3+
rev: v4.6.0
44
hooks:
55
- id: check-added-large-files
66
- id: check-toml
@@ -10,7 +10,7 @@ repos:
1010
- id: end-of-file-fixer
1111
- id: trailing-whitespace
1212
- repo: https://github.com/charliermarsh/ruff-pre-commit
13-
rev: v0.2.0
13+
rev: v0.4.1
1414
hooks:
1515
- id: ruff
1616
args:

README.md

+25-23
Original file line numberDiff line numberDiff line change
@@ -41,42 +41,39 @@ A common question people have as they become more comfortable with FastAPI is ho
4141
- Example:
4242

4343
```python
44-
from fastapi import FastAPI, APIRouter, Query
44+
from fastapi import FastAPI, Query
4545
from pydantic import BaseModel
4646
from fastapi_class import View
4747

4848
app = FastAPI()
49-
router = APIRouter()
5049

5150
class ItemModel(BaseModel):
5251
id: int
5352
name: str
5453
description: str = None
5554

56-
@View(router)
55+
@View(app)
5756
class ItemView:
58-
def post(self, item: ItemModel):
57+
async def post(self, item: ItemModel):
5958
return item
6059

61-
def get(self, item_id: int = Query(..., gt=0)):
60+
async def get(self, item_id: int = Query(..., gt=0)):
6261
return {"item_id": item_id}
6362

64-
app.include_router(router)
6563
```
6664

6765
### Response model 📦
6866

6967
`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`.
7068

7169
```py
72-
from fastapi import FastAPI, APIRouter, HTTPException, status
70+
from fastapi import FastAPI, HTTPException, status
7371
from fastapi.responses import PlainTextResponse
7472
from pydantic import BaseModel
7573

7674
from fastapi_class import View
7775

7876
app = FastAPI()
79-
router = APIRouter()
8077

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

88-
@View(router)
85+
@View(app)
8986
class MyView:
9087
exceptions = {
9188
"__all__": [NOT_AUTHORIZED],
@@ -100,29 +97,26 @@ class MyView:
10097
"delete": PlainTextResponse
10198
}
10299

103-
def get(self):
100+
async def get(self):
104101
...
105102

106-
def put(self):
103+
async def put(self):
107104
...
108105

109-
def delete(self):
106+
async def delete(self):
110107
...
111-
112-
app.include_router(router)
113108
```
114109

115110
### Customized Endpoints
116111

117112
```py
118-
from fastapi import FastAPI, APIRouter, HTTPException
113+
from fastapi import FastAPI, HTTPException
119114
from fastapi.responses import PlainTextResponse
120115
from pydantic import BaseModel
121116

122117
from fastapi_class import View, endpoint
123118

124119
app = FastAPI()
125-
router = APIRouter()
126120

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

135-
@View(router)
129+
@View(app)
136130
class MyView:
137131
exceptions = {
138132
"__all__": [NOT_AUTHORIZED],
@@ -149,17 +143,17 @@ class MyView:
149143
"delete": PlainTextResponse
150144
}
151145

152-
def get(self):
146+
async def get(self):
153147
...
154148

155-
def put(self):
149+
async def put(self):
156150
...
157151

158-
def delete(self):
152+
async def delete(self):
159153
...
160154

161-
@endpoint(("PUT",), path="edit")
162-
def edit(self):
155+
@endpoint(("PUT"), path="edit")
156+
async def edit(self):
163157
...
164158
```
165159

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

183177
And then install the development dependencies:
184178

179+
__Note:__ You should have `uv` installed, if not you can install it with:
180+
181+
```bash
182+
pip install uv
183+
```
184+
185+
Then you can install the dependencies with:
186+
185187
```bash
186188
# Install dependencies
187-
pip install -e .[test,lint]
189+
uv pip install -r requirements/all.txt
188190
```
189191

190192
### Run tests 🌝

fastapi_class/__init__.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,13 @@ class ItemView:
3030
async def get(self, query: str = Query(), limit: int = 50, offset: int = 0):
3131
pass
3232
33-
def post(self, user: ItemModel):
33+
async def post(self, user: ItemModel):
3434
pass
3535
```
3636
3737
"""
3838

39-
40-
__version__ = "3.5.0"
39+
__version__ = "3.6.0"
4140

4241
from fastapi_class.exception import FormattedMessageException
4342
from fastapi_class.openapi import ExceptionModel, _exceptions_to_responses

fastapi_class/openapi.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@ def _exceptions_to_responses(
2121
"""
2222
Convert exceptions to responses.
2323
24-
:param exceptions: exceptions
25-
:return: responses
26-
27-
:raise TypeError: if exception is not an instance of HTTPException or a factory function
28-
29-
:example:
24+
### example
3025
>>> from fastapi import HTTPException, status
3126
>>> from fastapi_class import _exceptions_to_responses
3227
>>> _exceptions_to_responses([HTTPException(status.HTTP_400_BAD_REQUEST, detail="Bad request")])

fastapi_class/routers.py

+28-24
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212

1313
class Method(str, Enum):
14+
"""
15+
HTTP methods.
16+
"""
17+
1418
GET = "get"
1519
POST = "post"
1620
PATCH = "patch"
@@ -20,6 +24,10 @@ class Method(str, Enum):
2024

2125
@dataclass(frozen=True, init=True, repr=True)
2226
class Metadata:
27+
"""
28+
Metadata class, used to store endpoint metadata.
29+
"""
30+
2331
methods: Iterable[str | Method]
2432
name: str | None = None
2533
path: str | None = None
@@ -29,6 +37,9 @@ class Metadata:
2937
__default_method_suffix: ClassVar[str] = "_or_default"
3038

3139
def __getattr__(self, __name: str) -> Any | Callable[[Any], Any]:
40+
"""
41+
Dynamically return the value of the attribute.
42+
"""
3243
if __name.endswith(Metadata.__default_method_suffix):
3344
prefix = __name.replace(Metadata.__default_method_suffix, "")
3445
if hasattr(self, prefix):
@@ -47,30 +58,16 @@ def endpoint(
4758
response_class: type[Response] | None = None,
4859
):
4960
"""
50-
Endpoint decorator.
51-
52-
:param methods: methods
53-
:param name: name
54-
:param path: path
55-
:param status_code: status code
56-
:param response_model: response model
57-
:param response_class: response class
58-
59-
:raise AssertionError: if response model or response class is not a subclass of BaseModel or Response respectively
60-
:raise AssertionError: if methods is not an iterable of strings or Method enums
61-
62-
:example:
63-
>>> from fastapi import FastAPI
64-
>>> from fastapi_class import endpoint
65-
>>> app = FastAPI()
66-
>>> @endpoint()
67-
... def get():
68-
... return {"message": "Hello, world!"}
69-
>>> app.include_router(get)
70-
71-
Results:
72-
73-
`GET /get`
61+
Endpoint decorator for FastAPI.
62+
63+
### Example:
64+
>>> from fastapi import FastAPI
65+
>>> from fastapi_class import endpoint
66+
>>> app = FastAPI()
67+
>>> @endpoint()
68+
... async def get():
69+
... return {"message": "Hello, world!"}
70+
>>> app.include_router(get)
7471
"""
7572
assert all(
7673
issubclass(_type, expected_type)
@@ -85,8 +82,15 @@ def endpoint(
8582
), "Methods must be an string, iterable of strings or Method enums."
8683

8784
def _decorator(function: Callable):
85+
"""
86+
Decorate the function.
87+
"""
88+
8889
@wraps(function)
8990
async def _wrapper(*args, **kwargs):
91+
"""
92+
Wrapper for the function.
93+
"""
9094
return await function(*args, **kwargs)
9195

9296
parsed_method = set()

fastapi_class/views.py

+12-23
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from fastapi_class.routers import Metadata, Method
1111

1212
COMMON_KEYWORD = "common"
13-
RESPONSE_MODEL_ATTRIBUTE_NAME = "RESPONSE_MODEL"
14-
RESPONSE_CLASS_ATTRIBUTE_NAME = "RESPONSE_CLASS"
15-
ENDPOINT_METADATA_ATTRIBUTE_NAME = "ENDPOINT_METADATA"
13+
RESPONSE_MODEL_ATTRIBUTE_NAME = "response_model"
14+
RESPONSE_CLASS_ATTRIBUTE_NAME = "response_class"
15+
ENDPOINT_METADATA_ATTRIBUTE_NAME = "__endpoint_metadata"
1616
EXCEPTIONS_ATTRIBUTE_NAME = "EXCEPTIONS"
1717

1818

@@ -29,28 +29,17 @@ def View(
2929
name_parser: Callable[[object, str], str] = _view_class_name_default_parser,
3030
):
3131
"""
32-
Class-based view decorator.
32+
Class-based view decorator for FastAPI.
3333
34-
:param router: router
35-
:param path: path
36-
:param default_status_code: default status code
37-
:param name_parser: name parser
34+
### Example:
35+
>>> from fastapi import FastAPI
36+
>>> from fastapi_class import View
3837
39-
:raise AssertionError: if router is not an instance of FastAPI or APIRouter
40-
41-
:example:
42-
>>> from fastapi import FastAPI
43-
>>> from fastapi_class import View
44-
>>> app = FastAPI()
45-
>>> @View(app)
46-
... class MyView:
47-
... def get(self):
48-
... return {"message": "Hello, world!"}
49-
>>> app.include_router(MyView.router)
50-
51-
Results:
52-
53-
`GET /my-view`
38+
>>> app = FastAPI()
39+
>>> @View(app)
40+
... class MyView:
41+
... async def get(self):
42+
... return {"message": "Hello, world!"}
5443
"""
5544

5645
def _decorator(cls) -> None:

0 commit comments

Comments
 (0)