Skip to content

Commit e5fc165

Browse files
committed
chore: added more setup configurations and linters
1 parent 8cd0fc5 commit e5fc165

File tree

17 files changed

+247
-66
lines changed

17 files changed

+247
-66
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,8 @@ repos:
1616
- id: mixed-line-ending
1717
- id: requirements-txt-fixer
1818
- repo: https://github.com/astral-sh/ruff-pre-commit
19-
rev: v0.5.2
19+
rev: v0.5.4
2020
hooks:
2121
- id: ruff
2222
args: [--fix]
2323
- id: ruff-format
24-
- repo: https://github.com/pre-commit/mirrors-mypy
25-
rev: v1.10.1
26-
hooks:
27-
- id: mypy
28-
additional_dependencies: [alembic==1.13.2]

Makefile

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,73 @@
1-
.DEFAULT_GOAL := help
2-
ROOT_DIR := ./
1+
# Makefile for setting up the project
32

4-
hello:
3+
.DEFAULT_GOAL=help
4+
5+
# Define commands to be explicitly invoked
6+
.PHONY: all venv install check clean help run migrate hello
7+
8+
# Define the name of the virtual environment directory
9+
VENV_DIR = .venv
10+
11+
# Define the python command for creating virtual environments
12+
PYTHON = python3
13+
14+
# Define the pip executable within the virtual environment
15+
PIP = $(VENV_DIR)/bin/pip
16+
17+
# Define the tox executable within the virtual environment
18+
TOX = $(VENV_DIR)/bin/tox
19+
20+
# Define the pre-commit executable within the virtual environment
21+
PRE_COMMIT = $(VENV_DIR)/bin/pre-commit
22+
23+
# Define the uvicorn executable within the virtual environment
24+
UVICORN = $(VENV_DIR)/bin/uvicorn
25+
26+
# Define the alembic executable within the virtual environment
27+
ALEMBIC = $(VENV_DIR)/bin/alembic
28+
29+
hello: ## Hello, World!
530
@echo "Hello, World!"
631

7-
help: ## Show this help
8-
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
32+
# Setup the development environment
33+
all: venv install check
34+
35+
venv: ## Create a virtual environment
36+
$(PYTHON) -m venv $(VENV_DIR)
37+
@echo "Virtual environment created."
38+
39+
install: ## Install development packages
40+
$(PIP) install --upgrade pip
41+
$(PIP) install -r requirements.txt
42+
$(PRE_COMMIT) install
43+
cp .env.example .env
44+
@echo "Development packages has been setup."
45+
46+
check: ## Run all checks using tox and pre-commit
47+
$(TOX)
48+
$(PRE_COMMIT) run --all-files
49+
@echo "All checks passed"
950

1051
clean: ## Clean up the project of unneeded files
1152
@echo "Cleaning up the project of unneeded files..."
12-
@rm -rf .tox .mypy_cache .ruff_cache *.egg-info dist .cache htmlcov coverage.xml .coverage
13-
@find . -name '*.pyc' -delete
14-
@find . -name 'db.sqlite3' -delete
15-
@find . -type d -name '__pycache__' -exec rm -r {} \+
53+
@rm -rf $(VENV_DIR)
54+
@rm -rf .cache
55+
@rm -rf htmlcov coverage.xml .coverage
56+
@rm -rf .tox
57+
@rm -rf .mypy_cache
58+
@rm -rf .ruff_cache
59+
@rm -rf *.egg-info
60+
@rm -rf dist
61+
@find . -name "*.pyc" -delete
62+
@find . -type d -name "__pycache__" -exec rm -r {} +
1663
@echo "Clean up successfully completed."
1764

65+
help: ## Show this help
66+
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
67+
1868
run: ## Run the development server
19-
@uvicorn main:app --reload
69+
$(UVICORN) main:app --reload
2070

2171
migrate: ## Run the database migration
22-
@alembic revision --autogenerate
23-
@alembic upgrade head
72+
$(ALEMBIC) revision --autogenerate
73+
$(ALEMBIC) upgrade head

api/db/database.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env python3
22
"""The database module"""
33

4-
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
54
from sqlalchemy import create_engine
6-
from api.utils.settings import settings, BASE_DIR
5+
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
76

7+
from api.utils.settings import BASE_DIR, settings
88

99
DB_HOST = settings.DB_HOST
1010
DB_PORT = settings.DB_PORT
@@ -14,7 +14,7 @@
1414
DB_TYPE = settings.DB_TYPE
1515

1616

17-
def get_db_engine(test_mode: bool = False):
17+
def get_db_engine(test_mode: bool = False): # type: ignore
1818
DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
1919

2020
if DB_TYPE == "sqlite" or test_mode:

api/utils/settings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from pydantic_settings import BaseSettings
2-
from decouple import config
31
from pathlib import Path
42

3+
from decouple import config
4+
from pydantic_settings import BaseSettings
5+
56
# Use this to build paths inside the project
67
BASE_DIR = Path(__file__).resolve().parent
78

api/v1/models/blog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""The Blog Post Model."""
22

3-
from sqlalchemy import Column, String, Text, Boolean, Integer, DateTime
3+
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text
44
from sqlalchemy.sql import func
55

66
from api.v1.models.base import Base

api/v1/routes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010

1111
from fastapi import APIRouter
12+
1213
from api.v1.routes.blog import blog
1314

1415
api_version_one = APIRouter(prefix="/api/v1")

api/v1/routes/blog.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
from typing import Any
1+
import logging
2+
23
from fastapi import APIRouter, Depends, HTTPException, Query, status
3-
from sqlalchemy.orm import Session
4-
from sqlalchemy.exc import SQLAlchemyError
54
from sqlalchemy import not_
6-
from api.v1.models.blog import Blog
7-
from api.v1.schemas.blog import BlogCreateSchema, BlogResponseSchema
5+
from sqlalchemy.exc import SQLAlchemyError
6+
from sqlalchemy.orm import Session
7+
88
from api.db.database import get_db
9-
import logging
9+
from api.v1.models.blog import Blog
10+
from api.v1.schemas.blog import (
11+
BlogCreateResponseSchema,
12+
BlogCreateSchema,
13+
BlogListItemResponseSchema,
14+
BlogListResponseSchema,
15+
)
1016

1117
blog = APIRouter(prefix="/blogs", tags=["Blog"])
1218

@@ -15,10 +21,13 @@
1521

1622
@blog.post(
1723
"",
18-
response_model=BlogResponseSchema,
24+
response_model=BlogCreateResponseSchema,
1925
status_code=status.HTTP_201_CREATED,
2026
)
21-
async def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
27+
async def create_blog(
28+
blog: BlogCreateSchema,
29+
db: Session = Depends(get_db),
30+
) -> BlogCreateResponseSchema:
2231
try:
2332
existing_blog = db.query(Blog).filter(Blog.title == blog.title).first()
2433
if existing_blog:
@@ -38,7 +47,8 @@ async def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
3847
db.commit()
3948
db.refresh(new_blog)
4049
logger.info(f"Blog post '{new_blog.title}' created successfully.")
41-
return new_blog
50+
51+
return BlogCreateResponseSchema.model_validate(new_blog.__dict__)
4252

4353
except HTTPException as http_err:
4454
logger.warning(f"HTTP error occurred: {http_err.detail}")
@@ -59,12 +69,16 @@ async def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
5969
)
6070

6171

62-
@blog.get("", status_code=status.HTTP_200_OK)
72+
@blog.get(
73+
"",
74+
response_model=BlogListResponseSchema,
75+
status_code=status.HTTP_200_OK,
76+
)
6377
async def list_blog(
6478
page: int = Query(1, ge=1),
6579
page_size: int = Query(10, ge=1, le=100),
6680
db: Session = Depends(get_db),
67-
) -> dict[str, Any]:
81+
) -> BlogListResponseSchema:
6882
try:
6983
offset = (page - 1) * page_size
7084
query = (
@@ -84,22 +98,22 @@ async def list_blog(
8498
prev_page = f"/api/v1/blogs?page={page - 1}&page_size={page_size}"
8599

86100
results = [
87-
{
88-
"id": blog.id,
89-
"title": blog.title,
90-
"excerpt": blog.excerpt,
91-
"image_url": blog.image_url,
92-
"created_at": blog.created_at,
93-
}
101+
BlogListItemResponseSchema(
102+
id=blog.id,
103+
title=blog.title,
104+
excerpt=blog.excerpt,
105+
image_url=blog.image_url,
106+
created_at=blog.created_at,
107+
)
94108
for blog in blogs
95109
]
96110

97-
return {
98-
"count": total_count,
99-
"next": next_page,
100-
"previous": prev_page,
101-
"results": results,
102-
}
111+
return BlogListResponseSchema(
112+
count=total_count,
113+
next=next_page,
114+
previous=prev_page,
115+
results=results,
116+
)
103117

104118
except SQLAlchemyError as e:
105119
logger.error(f"Database error occurred: {e}")

api/v1/schemas/blog.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Blog Schema."""
22

3-
from pydantic import BaseModel, Field
43
from datetime import datetime
4+
from typing import List, Optional
5+
6+
from pydantic import BaseModel, Field
57

68

7-
class BlogResponseSchema(BaseModel):
8-
"""Schema for representing a blog post in the API response."""
9+
class BlogCreateResponseSchema(BaseModel):
10+
"""Schema for representing a created blog post in the API response."""
911

1012
id: int
1113
title: str
@@ -45,3 +47,26 @@ class BlogCreateSchema(BaseModel):
4547
max_length=255,
4648
description="URL of the blog post image",
4749
)
50+
51+
52+
class BlogListItemResponseSchema(BaseModel):
53+
"""Schema for representing a blog post item in the list response."""
54+
55+
id: int
56+
title: str
57+
excerpt: str
58+
image_url: str
59+
created_at: datetime
60+
61+
model_config = {"from_attributes": True}
62+
63+
64+
class BlogListResponseSchema(BaseModel):
65+
"""Schema for representing a blog post listing in the API response."""
66+
67+
count: int
68+
next: Optional[str]
69+
previous: Optional[str]
70+
results: List[BlogListItemResponseSchema]
71+
72+
model_config = {"from_attributes": True}

main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import uvicorn
21
from contextlib import asynccontextmanager
2+
3+
import uvicorn
34
from fastapi import FastAPI
45
from fastapi.middleware.cors import CORSMiddleware
56
from starlette.requests import Request
7+
68
from api.db.database import Base, engine
79
from api.utils.logging_config import setup_logging
810
from api.v1.routes import api_version_one

mypy.ini

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
11
[mypy]
2+
; Specifies the Python version to target for type checking.
23
python_version = 3.10
4+
5+
; Enables support for PEP 420 namespace packages.
36
namespace_packages = True
7+
8+
; Ensures mypy can find types for packages that use explicit relative imports.
49
explicit_package_bases = True
510

6-
[mypy-fastapi_blog.*]
11+
; Ignores imports that cannot be resolved.
712
ignore_missing_imports = True
13+
14+
; Disallows subclassing classes that are typed with Any.
15+
disallow_subclassing_any = False
16+
17+
; Disallows function definitions that do not have type annotations for all arguments and return types.
18+
disallow_incomplete_defs = True
19+
20+
; Type-checks functions that do not have type annotations.
21+
check_untyped_defs = True
22+
23+
; Issues warnings for redundant cast operations.
24+
warn_redundant_casts = True
25+
26+
; Disables warnings about unused # type: ignore comments.
27+
warn_unused_ignores = False
28+
29+
; Allows redefinition of variables within the same scope.
30+
allow_redefinition = True
31+
32+
; Enables pretty formatting for mypy output.
33+
pretty = True
34+
35+
; Controls how mypy handles imports.
36+
follow_imports = silent
37+
38+
; Shows error codes in mypy output.
39+
show_error_codes = True
40+
41+
; Displays column numbers in error messages.
42+
show_column_numbers = True

0 commit comments

Comments
 (0)