Skip to content

Commit

Permalink
Add pre-commit, set up ruff, configure pre-commit hooks for ruff and …
Browse files Browse the repository at this point in the history
…mypy
  • Loading branch information
lucabenvenuto committed Jul 30, 2024
1 parent 2fffb16 commit 53276f6
Show file tree
Hide file tree
Showing 19 changed files with 97 additions and 77 deletions.
Binary file added .docs/images/pre-commit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions .flake8

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ var/

.env
logs/*

# Virtual envs
.venv/
3 changes: 0 additions & 3 deletions .isort.cfg

This file was deleted.

27 changes: 27 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.5
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format

# Using mypy's hook isn't working properly. Keep using format.sh
- repo: local
hooks:
- id: mypy
name: mypy type checking
language: script
entry: ./scripts/exec.sh format
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
- PostgreSQL database

## Project setup
You only need to install [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/).
To start the containers, just run `docker-compose up` (or `docker-compose up -d` if you want to run the containers in background); or `docker-compose create` and `docker-compose start` if you don't want to see the logs.
You only need to install [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/).
To start the containers, just run `docker-compose up` (or `docker-compose up -d` if you want to run the containers in background); or `docker-compose create` and `docker-compose start` if you don't want to see the logs.
Once the containers are running, you can go to `http://localhost:8000/docs` to see the automatic interactive API documentation.

## Migrations
Expand All @@ -19,6 +19,27 @@ We use Alembic as database migration tool. To run its commands you can open an i
- `./exec.sh makemigrations` -> compares the actual status of the DB against the table metadata, and generates the migrations based on the comparison

## Code tools

### pre-commit
Install `pre-commit` via `pip`:

pip install pre-commit

Setup the `pre-commit` hooks, specified in `.pre-commit-config.yaml`:

pre-commit install

Ensure everything was set up correctly by running the hooks:

pre-commit run --all-files

![Screenshot](.docs/images/pre-commit.png)

#### Adding hooks

You can add new `pre-commit` hooks by editing `.pre-commit-config.yaml`. Whenever new hooks are added, you must run `pre-commit install` to ensure new hooks are run on commit.


Linters, formatters, etc.

- **Pycln**: Formatter for finding and removing unused import statements.
Expand Down Expand Up @@ -49,5 +70,3 @@ The template includes an admin interface via [SQLAdmin](https://github.com/amina
*One note: You should be careful when adding relationships to the list or detail pages (specially large many-to-many / one-to-many relationships), because it's not very optimal in terms of DB querys in those cases (all the related objects would be loaded in memory).*

![Screenshot](.docs/images/admin.png)


7 changes: 0 additions & 7 deletions mypy.ini

This file was deleted.

26 changes: 26 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[tool.ruff]
line-length = 130
force-exclude = true # Ensure exclusions are respected by the pre-commit hook
extend-exclude = [
"src/alembic/versions",
"__pycache__"
]

[tool.ruff.lint]
extend-select = [ # Defaults: [ "E4", "E7", "E9", "F" ] (https://docs.astral.sh/ruff/rules/#error-e)
"E501", # line-too-long
"I001", # unsorted-imports
"I002", # missing-required-import
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]

[tool.mypy]
plugins = [
"pydantic.mypy",
"sqlalchemy.ext.mypy.plugin"
]
exclude = "src/alembic/"
disallow_untyped_defs = true
ignore_missing_imports = true
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pyxdg==0.28
PyYAML==6.0
requests==2.31.0
rsa==4.9
ruff==0.5.5
six==1.16.0
sniffio==1.3.0
sqladmin==0.10.3
Expand Down
10 changes: 0 additions & 10 deletions scripts/format.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
#!/bin/bash

printf "Runing pycln...\n"
pycln src --exclude __init__.py --all
printf "\nRunning isort...\n"
isort src
printf "\nRunning flake8...\n"
flake8

printf "\nRunning mypy...\n"
mypy src

printf "\nRunning black...\n"
black src --exclude alembic
4 changes: 1 addition & 3 deletions src/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ async def logout(self, request: Request) -> bool:
return True

async def authenticate(self, request: Request) -> RedirectResponse | None:
failed_auth_response = RedirectResponse(
request.url_for("admin:login"), status_code=302
)
failed_auth_response = RedirectResponse(request.url_for("admin:login"), status_code=302)
manager = AuthManager()
token = request.session.get(AdminAuth.cookie_name)
if not token:
Expand Down
6 changes: 2 additions & 4 deletions src/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool

from alembic import context
from src import models # noqa F401
from src.core.config import settings
from src.core.database import SQLBase
Expand Down Expand Up @@ -59,9 +59,7 @@ def run_migrations_online():
)

with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata, compare_type=True
)
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)

with context.begin_transaction():
context.run_migrations()
Expand Down
2 changes: 1 addition & 1 deletion src/alembic/versions/2023-07-12-5d07fd610995_.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""empty message
Revision ID: 5d07fd610995
Revises:
Revises:
Create Date: 2023-07-12 18:37:52.159059
"""
Expand Down
4 changes: 1 addition & 3 deletions src/api/v1/routers/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@


@router.get("", response_model=Page[Item])
def get_items(
user: User = Depends(get_user), session: Session = Depends(db_session)
) -> Any:
def get_items(user: User = Depends(get_user), session: Session = Depends(db_session)) -> Any:
return paginate(session, user.get_items())


Expand Down
4 changes: 1 addition & 3 deletions src/controllers/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

class ItemController:
@staticmethod
def create(
item_data: schemas.ItemCreate, owner_id: UUID, session: Session
) -> models.Item:
def create(item_data: schemas.ItemCreate, owner_id: UUID, session: Session) -> models.Item:
item_data = schemas.Item(owner_id=owner_id, **item_data.model_dump())
item = models.Item.objects(session).create(item_data.model_dump())
return item
4 changes: 1 addition & 3 deletions src/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ def create(user_data: UserCreate, session: Session) -> User:

@staticmethod
def login(user_data: UserCreate, session: Session) -> User:
login_exception = HTTPException(
status_code=401, detail="Invalid email or password"
)
login_exception = HTTPException(status_code=401, detail="Invalid email or password")
user = User.objects(session).get(User.email == user_data.email)
if not user:
raise login_exception
Expand Down
12 changes: 3 additions & 9 deletions src/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ def get(self, *where_clause: Any) -> _Model | None:
def get_or_404(self, *where_clause: Any) -> _Model:
obj = self.get(*where_clause)
if obj is None:
raise HTTPException(
status_code=404, detail=f"{self.cls.__name__} not found"
)
raise HTTPException(status_code=404, detail=f"{self.cls.__name__} not found")
return obj

def get_all(self, *where_clause: Any) -> Sequence[_Model]:
Expand All @@ -96,14 +94,10 @@ def create(self, data: Dict[str, Any]) -> _Model:

@declarative_mixin
class TableIdMixin:
id: Mapped[uuid.UUID] = mapped_column(
primary_key=True, server_default=random_uuid()
)
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, server_default=random_uuid())


@declarative_mixin
class DatedTableMixin(TableIdMixin):
created_at: Mapped[datetime] = mapped_column(server_default=utcnow())
updated_at: Mapped[datetime] = mapped_column(
server_default=utcnow(), onupdate=datetime.utcnow()
)
updated_at: Mapped[datetime] = mapped_column(server_default=utcnow(), onupdate=datetime.utcnow())
16 changes: 4 additions & 12 deletions src/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,10 @@ class AuthManager:
accept_header = settings.accept_token

@classmethod
def create_access_token(
cls, user: User, expires_delta: timedelta | None = None
) -> Tuple[str, datetime]:
expires = datetime.utcnow() + (
expires_delta or timedelta(minutes=settings.access_token_expire_minutes)
)
def create_access_token(cls, user: User, expires_delta: timedelta | None = None) -> Tuple[str, datetime]:
expires = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes))
claims = {"exp": expires, "user_id": str(user.id)}
token = jwt.encode(
claims=claims, key=settings.jwt_signing_key, algorithm=cls.algorithm
)
token = jwt.encode(claims=claims, key=settings.jwt_signing_key, algorithm=cls.algorithm)
return token, expires

@classmethod
Expand All @@ -65,9 +59,7 @@ def process_login(cls, user: User, response: Response) -> Token | None:

def get_user_from_token(self, token: str, session: Session) -> User:
try:
payload = jwt.decode(
token=token, key=settings.jwt_signing_key, algorithms=self.algorithm
)
payload = jwt.decode(token=token, key=settings.jwt_signing_key, algorithms=self.algorithm)
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise self.credentials_exception
Expand Down
12 changes: 3 additions & 9 deletions src/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ def check_me_response(self, response: Response) -> None:

@reset_database
def test_signup(self) -> None:
expected_expire = datetime.utcnow() + timedelta(
minutes=settings.access_token_expire_minutes
)
expected_expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
response = client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD)
assert response.status_code == 201
data = response.json()
Expand All @@ -74,9 +72,7 @@ def test_signup_dup_emails(self) -> None:
@reset_database
def test_login(self) -> None:
client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD)
expected_expire = datetime.utcnow() + timedelta(
minutes=settings.access_token_expire_minutes
)
expected_expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
response = client.post(self.LOGIN_URL, json=self.TEST_PAYLOAD)
assert response.status_code == 200
data = response.json()
Expand Down Expand Up @@ -126,9 +122,7 @@ def test_me_unauthenticated(self) -> None:
def test_me_bad_access_token(self) -> None:
client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD)
client.cookies.clear()
response = client.get(
self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN}
)
response = client.get(self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN})
assert response.status_code == 401

@reset_database
Expand Down

0 comments on commit 53276f6

Please sign in to comment.