Skip to content

Commit bdc98ff

Browse files
committed
migrate to UV
1 parent d224aad commit bdc98ff

27 files changed

+1299
-273
lines changed

.gitignore

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -82,34 +82,7 @@ target/
8282
profile_default/
8383
ipython_config.py
8484

85-
# pyenv
86-
# For a library or package, you might want to ignore these files since the code is
87-
# intended to run in multiple environments; otherwise, check them in:
88-
# .python-version
89-
90-
# pipenv
91-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94-
# install all needed dependencies.
95-
#Pipfile.lock
96-
97-
# poetry
98-
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99-
# This is especially recommended for binary packages to ensure reproducibility, and is more
100-
# commonly ignored for libraries.
101-
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102-
poetry.lock
103-
104-
# pdm
105-
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106-
#pdm.lock
107-
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108-
# in version control.
109-
# https://pdm.fming.dev/#use-with-ide
11085
.pdm.toml
111-
poetry.lock
112-
src/poetry.lock
11386

11487
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
11588
__pypackages__/
@@ -154,13 +127,6 @@ dmypy.json
154127
# Cython debug symbols
155128
cython_debug/
156129

157-
# PyCharm
158-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160-
# and can be added to the global gitignore or merged into this file. For a more nuclear
161-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162-
#.idea/
163-
164130
# Macos
165131
.DS_Store
166132

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ repos:
6464
- id: unit_test
6565
name: Unit test
6666
language: system
67-
entry: poetry run pytest
67+
entry: uv run pytest
6868
pass_filenames: false
6969
always_run: true
7070
types: [python]

CONTRIBUTING.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,27 @@ Start by forking and cloning the FastAPI-boilerplate repository:
1010

1111
1. **Fork the Repository**: Begin by forking the project repository. You can do this by visiting https://github.com/igormagalhaesr/FastAPI-boilerplate and clicking the "Fork" button.
1212
1. **Create a Feature Branch**: Once you've forked the repo, create a branch for your feature by running `git checkout -b feature/fooBar`.
13-
1. **Testing Changes**: Ensure that your changes do not break existing functionality by running tests. In the root folder, execute poetry run `python -m pytest` to run the tests.
13+
1. **Testing Changes**: Ensure that your changes do not break existing functionality by running tests. In the root folder, execute `uv run pytest` to run the tests.
1414

15-
### Using Poetry for Dependency Management
16-
FastAPI-boilerplate uses Poetry for managing dependencies. If you don't have Poetry installed, follow the instructions on the [official Poetry website](https://python-poetry.org/docs/).
15+
### Using uv for Dependency Management
16+
FastAPI-boilerplate uses uv for managing dependencies. If you don't have uv installed, follow the instructions on the [official uv website](https://docs.astral.sh/uv/).
1717

18-
Once Poetry is installed, navigate to the cloned repository and install the dependencies:
18+
Once uv is installed, navigate to the cloned repository and install the dependencies:
1919
```sh
2020
cd FastAPI-boilerplate
21-
poetry install
21+
uv sync
2222
```
2323

2424
### Activating the Virtual Environment
25-
Poetry creates a virtual environment for your project. Activate it using:
25+
uv creates a virtual environment for your project. Activate it using:
2626

2727
```sh
28-
poetry shell
28+
source .venv/bin/activate
29+
```
30+
31+
Alternatively, you can run commands directly with `uv run` without activating the environment:
32+
```sh
33+
uv run python your_script.py
2934
```
3035

3136
## Making Contributions
@@ -37,7 +42,7 @@ poetry shell
3742
### Testing with Pytest
3843
FastAPI-boilerplate uses pytest for testing. Run tests using:
3944
```sh
40-
poetry run pytest
45+
uv run pytest
4146
```
4247

4348
### Linting
@@ -59,7 +64,7 @@ Ensure your code passes linting before submitting.
5964
It helps in identifying simple issues before submission to code review. By running automated checks, pre-commit can ensure code quality and consistency.
6065

6166
1. **Install Pre-commit**:
62-
- **Installation**: Install pre-commit in your development environment. Use the command `pip install pre-commit`.
67+
- **Installation**: Install pre-commit in your development environment. Use the command `uv add --dev pre-commit` or `pip install pre-commit`.
6368
- **Setting Up Hooks**: After installing pre-commit, set up the hooks with `pre-commit install`. This command will install hooks into your .git/ directory which will automatically check your commits for issues.
6469
1. **Committing Your Changes**:
6570
After making your changes, use `git commit -am 'Add some fooBar'` to commit them. Pre-commit will run automatically on your files when you commit, ensuring that they meet the required standards.

Dockerfile

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
1-
# --------- requirements ---------
1+
# --------- Builder Stage ---------
2+
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS builder
23

3-
FROM python:3.11 as requirements-stage
4+
# Set environment variables for uv
5+
ENV UV_COMPILE_BYTECODE=1
6+
ENV UV_LINK_MODE=copy
47

5-
WORKDIR /tmp
8+
WORKDIR /app
69

7-
RUN pip install poetry poetry-plugin-export
10+
# Install dependencies first (for better layer caching)
11+
RUN --mount=type=cache,target=/root/.cache/uv \
12+
--mount=type=bind,source=uv.lock,target=uv.lock \
13+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
14+
uv sync --locked --no-install-project
815

9-
COPY ./pyproject.toml ./poetry.lock* /tmp/
16+
# Copy the project source code
17+
COPY . /app
1018

11-
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
19+
# Install the project in non-editable mode
20+
RUN --mount=type=cache,target=/root/.cache/uv \
21+
uv sync --locked --no-editable
1222

23+
# --------- Final Stage ---------
24+
FROM python:3.11-slim-bookworm
1325

14-
# --------- final image build ---------
15-
FROM python:3.11
26+
# Create a non-root user for security
27+
RUN groupadd --gid 1000 app \
28+
&& useradd --uid 1000 --gid app --shell /bin/bash --create-home app
1629

17-
WORKDIR /code
30+
# Copy the virtual environment from the builder stage
31+
COPY --from=builder --chown=app:app /app/.venv /app/.venv
1832

19-
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt
33+
# Ensure the virtual environment is in the PATH
34+
ENV PATH="/app/.venv/bin:$PATH"
2035

21-
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
36+
# Switch to the non-root user
37+
USER app
2238

23-
COPY ./src/app /code/app
39+
# Set the working directory
40+
WORKDIR /code
2441

2542
# -------- replace with comment to run with gunicorn --------
2643
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
27-
# CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker". "-b", "0.0.0.0:8000"]
44+
# CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]

docker-compose.yml

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,6 @@ services:
9393
# volumes:
9494
# - ./src:/code/src
9595

96-
#-------- uncomment to run tests --------
97-
# pytest:
98-
# build:
99-
# context: .
100-
# dockerfile: Dockerfile
101-
# env_file:
102-
# - ./src/.env
103-
# depends_on:
104-
# - db
105-
# - redis
106-
# command: python -m pytest ./tests
107-
# volumes:
108-
# - .:/code
109-
11096
#-------- uncomment to create first tier --------
11197
# create_tier:
11298
# build:

src/app/api/dependencies.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Annotated, Any
1+
from typing import Annotated, Any, cast
22

33
from fastapi import Depends, HTTPException, Request
44
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,8 +12,8 @@
1212
from ..crud.crud_rate_limit import crud_rate_limits
1313
from ..crud.crud_tier import crud_tiers
1414
from ..crud.crud_users import crud_users
15-
from ..models.user import User
16-
from ..schemas.rate_limit import sanitize_path
15+
from ..schemas.rate_limit import RateLimitRead, sanitize_path
16+
from ..schemas.tier import TierRead
1717

1818
logger = logging.getLogger(__name__)
1919

@@ -29,12 +29,12 @@ async def get_current_user(
2929
raise UnauthorizedException("User not authenticated.")
3030

3131
if "@" in token_data.username_or_email:
32-
user: dict | None = await crud_users.get(db=db, email=token_data.username_or_email, is_deleted=False)
32+
user = await crud_users.get(db=db, email=token_data.username_or_email, is_deleted=False)
3333
else:
3434
user = await crud_users.get(db=db, username=token_data.username_or_email, is_deleted=False)
3535

3636
if user:
37-
return user
37+
return cast(dict[str, Any], user)
3838

3939
raise UnauthorizedException("User not authenticated.")
4040

@@ -73,30 +73,32 @@ async def get_current_superuser(current_user: Annotated[dict, Depends(get_curren
7373

7474

7575
async def rate_limiter_dependency(
76-
request: Request, db: Annotated[AsyncSession, Depends(async_get_db)], user: User | None = Depends(get_optional_user)
76+
request: Request, db: Annotated[AsyncSession, Depends(async_get_db)], user: dict | None = Depends(get_optional_user)
7777
) -> None:
7878
if hasattr(request.app.state, "initialization_complete"):
7979
await request.app.state.initialization_complete.wait()
8080

8181
path = sanitize_path(request.url.path)
8282
if user:
8383
user_id = user["id"]
84-
tier = await crud_tiers.get(db, id=user["tier_id"])
84+
tier = await crud_tiers.get(db, id=user["tier_id"], schema_to_select=TierRead)
8585
if tier:
86-
rate_limit = await crud_rate_limits.get(db=db, tier_id=tier["id"], path=path)
86+
tier = cast(TierRead, tier)
87+
rate_limit = await crud_rate_limits.get(db=db, tier_id=tier.id, path=path, schema_to_select=RateLimitRead)
8788
if rate_limit:
88-
limit, period = rate_limit["limit"], rate_limit["period"]
89+
rate_limit = cast(RateLimitRead, rate_limit)
90+
limit, period = rate_limit.limit, rate_limit.period
8991
else:
9092
logger.warning(
91-
f"User {user_id} with tier '{tier['name']}' has no specific rate limit for path '{path}'. \
93+
f"User {user_id} with tier '{tier.name}' has no specific rate limit for path '{path}'. \
9294
Applying default rate limit."
9395
)
9496
limit, period = DEFAULT_LIMIT, DEFAULT_PERIOD
9597
else:
9698
logger.warning(f"User {user_id} has no assigned tier. Applying default rate limit.")
9799
limit, period = DEFAULT_LIMIT, DEFAULT_PERIOD
98100
else:
99-
user_id = request.client.host
101+
user_id = request.client.host if request.client else "unknown"
100102
limit, period = DEFAULT_LIMIT, DEFAULT_PERIOD
101103

102104
is_limited = await rate_limiter.is_rate_limited(db=db, user_id=user_id, path=path, limit=limit, period=period)

src/app/api/v1/login.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ...core.schemas import Token
1212
from ...core.security import (
1313
ACCESS_TOKEN_EXPIRE_MINUTES,
14+
TokenType,
1415
authenticate_user,
1516
create_access_token,
1617
create_refresh_token,
@@ -37,7 +38,7 @@ async def login_for_access_token(
3738
max_age = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
3839

3940
response.set_cookie(
40-
key="refresh_token", value=refresh_token, httponly=True, secure=True, samesite="Lax", max_age=max_age
41+
key="refresh_token", value=refresh_token, httponly=True, secure=True, samesite="lax", max_age=max_age
4142
)
4243

4344
return {"access_token": access_token, "token_type": "bearer"}
@@ -49,7 +50,7 @@ async def refresh_access_token(request: Request, db: AsyncSession = Depends(asyn
4950
if not refresh_token:
5051
raise UnauthorizedException("Refresh token missing.")
5152

52-
user_data = await verify_token(refresh_token, db)
53+
user_data = await verify_token(refresh_token, TokenType.REFRESH, db)
5354
if not user_data:
5455
raise UnauthorizedException("Invalid refresh token.")
5556

src/app/api/v1/logout.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from fastapi import APIRouter, Depends, Response, Cookie
1+
from typing import Optional
2+
3+
from fastapi import APIRouter, Cookie, Depends, Response
24
from jose import JWTError
35
from sqlalchemy.ext.asyncio import AsyncSession
4-
from typing import Optional
56

67
from ...core.db.database import async_get_db
78
from ...core.exceptions.http_exceptions import UnauthorizedException
@@ -15,17 +16,13 @@ async def logout(
1516
response: Response,
1617
access_token: str = Depends(oauth2_scheme),
1718
refresh_token: Optional[str] = Cookie(None, alias="refresh_token"),
18-
db: AsyncSession = Depends(async_get_db)
19+
db: AsyncSession = Depends(async_get_db),
1920
) -> dict[str, str]:
2021
try:
2122
if not refresh_token:
2223
raise UnauthorizedException("Refresh token not found")
23-
24-
await blacklist_tokens(
25-
access_token=access_token,
26-
refresh_token=refresh_token,
27-
db=db
28-
)
24+
25+
await blacklist_tokens(access_token=access_token, refresh_token=refresh_token, db=db)
2926
response.delete_cookie(key="refresh_token")
3027

3128
return {"message": "Logged out successfully"}

0 commit comments

Comments
 (0)