Skip to content

Commit dccfae7

Browse files
committed
⬆️ Python 3.11.7 + Pydantic V2
1 parent d3ead39 commit dccfae7

15 files changed

+832
-380
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.10
1+
FROM python:3.11.7
22

33

44

Pipfile

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ name = "pypi"
77
fastapi = "*"
88
sqlmodel = "*"
99
uvicorn = "*"
10+
pydantic-settings = "*"
1011

1112
[dev-packages]
1213
black = "*"
1314
pylint = "*"
1415

1516
[requires]
16-
python_version = "3.10"
17+
python_version = "3.11.7"
1718

1819
[pipenv]
1920
allow_prereleases = true

Pipfile.lock

+710-333
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/app.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1+
from contextlib import asynccontextmanager
2+
13
from fastapi import FastAPI
24

3-
from pydantic import BaseSettings
5+
from api.config import Settings
46
from api.database import create_db_and_tables
57
from api.public import api as public_api
68
from api.utils.mock_data_generator import create_heroes_and_teams
79

810

9-
def create_app(settings: BaseSettings):
11+
@asynccontextmanager
12+
async def lifespan(app: FastAPI):
13+
create_db_and_tables()
14+
create_heroes_and_teams()
15+
16+
yield
17+
18+
19+
def create_app(settings: Settings):
1020
app = FastAPI(
1121
title=settings.PROJECT_NAME,
1222
version=settings.VERSION,
1323
docs_url="/",
1424
description=settings.DESCRIPTION,
25+
lifespan=lifespan,
1526
)
1627

17-
@app.on_event("startup")
18-
def on_starup():
19-
create_db_and_tables()
20-
create_heroes_and_teams()
21-
2228
app.include_router(public_api)
2329

2430
return app

api/config.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import os
22
import secrets
33

4-
from pydantic import BaseSettings
5-
from pydantic.types import Optional
4+
from pydantic_settings import BaseSettings
65

76

87
class Settings(BaseSettings):
98
PROJECT_NAME: str = f"SQLModel API - {os.getenv('ENV', 'development').capitalize()}"
109
DESCRIPTION: str = "A FastAPI + SQLModel production-ready API"
11-
ENV: str
10+
ENV: str = "development"
1211
VERSION: str = "0.1"
1312
SECRET_KEY: str = secrets.token_urlsafe(32)
14-
DATABASE_URI: Optional[str] # pydantic.PostgresDsn can be used for PostgresDB
13+
DATABASE_URI: str = "sqlite:////Users/anth/dev/fastapi-sqlmodel/database.db"
1514

1615
class Config:
1716
case_sensitive = True

api/public/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from fastapi import APIRouter
22

3+
from api.public.health import views as health
34
from api.public.hero import views as heroes
45
from api.public.team import views as teams
56

67
api = APIRouter()
78

89

10+
api.include_router(health.router, prefix="/health", tags=["Health"])
911
api.include_router(heroes.router, prefix="/heroes", tags=["Heroes"])
1012
api.include_router(teams.router, prefix="/teams", tags=["Teams"])

api/public/health/crud.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from fastapi import Depends
2+
from sqlmodel import Session
3+
4+
from api.config import settings
5+
from api.database import get_session
6+
from api.public.health.models import Health, Stats
7+
from api.utils.logger import logger_config
8+
9+
logger = logger_config(__name__)
10+
11+
12+
def get_health() -> Health:
13+
return Health(status="OK", environment=settings.ENV)

api/public/health/models.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from enum import Enum
2+
3+
from pydantic import BaseModel
4+
5+
6+
class Status(str, Enum):
7+
OK = "OK"
8+
KO = "KO"
9+
10+
11+
class Environment(str, Enum):
12+
dev = "development"
13+
stg = "staging"
14+
prd = "production"
15+
16+
17+
class Health(BaseModel):
18+
status: Status | None
19+
environment: Environment | None
20+
21+
22+
class Stats(BaseModel):
23+
heroes: int | None
24+
teams: int | None

api/public/health/views.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from fastapi import APIRouter, Depends, status
2+
from sqlmodel import Session
3+
4+
from api.database import Session, get_session
5+
from api.public.health.crud import get_health
6+
from api.public.health.models import Health, Stats
7+
from api.utils.logger import logger_config
8+
9+
router = APIRouter()
10+
logger = logger_config(__name__)
11+
12+
13+
@router.get(
14+
"",
15+
response_model=Health,
16+
status_code=status.HTTP_200_OK,
17+
responses={200: {"model": Health}},
18+
)
19+
def health(db: Session = Depends(get_session)):
20+
return get_health()

api/public/hero/models.py

+13-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from pydantic.types import Optional, List
21
from sqlmodel import Field, Relationship, SQLModel
32

43
from api.public.team.models import Team
@@ -8,10 +7,10 @@
87
class HeroBase(SQLModel):
98
name: str
109
secret_name: str
11-
age: Optional[int] = None
10+
age: int | None = None
1211

1312
class Config:
14-
schema_extra = {
13+
json_schema_extra = {
1514
"example": {
1615
"id": 1,
1716
"name": "Super Man",
@@ -23,8 +22,8 @@ class Config:
2322

2423

2524
class Hero(HeroBase, table=True):
26-
id: Optional[int] = Field(default=None, primary_key=True)
27-
teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
25+
id: int | None = Field(default=None, primary_key=True)
26+
teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
2827

2928

3029
class HeroCreate(HeroBase):
@@ -33,20 +32,20 @@ class HeroCreate(HeroBase):
3332

3433
class HeroRead(HeroBase):
3534
id: int
36-
name: Optional[str] = None
37-
secret_name: Optional[str] = None
38-
age: Optional[int] = None
39-
teams: List[Team] = None
35+
name: str | None = None
36+
secret_name: str | None = None
37+
age: int | None = None
38+
teams: list[Team] = None
4039

4140

4241
class HeroUpdate(HeroBase):
43-
name: Optional[str] = None
44-
secret_name: Optional[str] = None
45-
age: Optional[int] = None
46-
teams: List[Team] = None
42+
name: str | None = None
43+
secret_name: str | None = None
44+
age: int | None = None
45+
teams: list[Team] = None
4746

4847
class Config:
49-
schema_extra = {
48+
json_schema_extra = {
5049
"example": {
5150
"name": "Super Man",
5251
"secret_name": "Clark Kent",

api/public/hero/views.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from fastapi import APIRouter, Depends, Query
2-
from pydantic.types import List
32
from sqlmodel import Session
43

54
from api.database import get_session
@@ -20,7 +19,7 @@ def create_a_hero(hero: HeroCreate, db: Session = Depends(get_session)):
2019
return create_hero(hero=hero, db=db)
2120

2221

23-
@router.get("", response_model=List[HeroRead])
22+
@router.get("", response_model=list[HeroRead])
2423
def get_heroes(
2524
offset: int = 0,
2625
limit: int = Query(default=100, lte=100),

api/public/team/models.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from pydantic.types import List, Optional
21
from sqlmodel import Field, Relationship, SQLModel
32

43
from api.utils.generic_models import HeroTeamLink
@@ -9,7 +8,7 @@ class TeamBase(SQLModel):
98
headquarters: str
109

1110
class Config:
12-
schema_extra = {
11+
json_schema_extra = {
1312
"example": {
1413
"name": "wonderful league",
1514
"headquarters": "Fortress of Solitude",
@@ -18,9 +17,9 @@ class Config:
1817

1918

2019
class Team(TeamBase, table=True):
21-
id: Optional[int] = Field(default=None, primary_key=True)
20+
id: int | None = Field(default=None, primary_key=True)
2221

23-
heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink)
22+
heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink)
2423

2524

2625
class TeamCreate(TeamBase):
@@ -29,11 +28,11 @@ class TeamCreate(TeamBase):
2928

3029
class TeamRead(TeamBase):
3130
id: int
32-
name: Optional[str] = None
33-
headquarters: Optional[str] = None
34-
heroes: List = None
31+
name: str | None = None
32+
headquarters: str | None = None
33+
heroes: list = None
3534

3635

3736
class TeamUpdate(TeamBase):
38-
name: Optional[str] = None
39-
headquarters: Optional[str] = None
37+
name: str | None = None
38+
headquarters: str | None = None

api/public/team/views.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from fastapi import APIRouter, Depends, Query
2-
from pydantic.types import List
32
from sqlmodel import Session
43

54
from api.database import get_session
@@ -20,7 +19,7 @@ def create_a_team(team: TeamCreate, db: Session = Depends(get_session)):
2019
return create_team(team=team, db=db)
2120

2221

23-
@router.get("", response_model=List[TeamRead])
22+
@router.get("", response_model=list[TeamRead])
2423
def get_teams(
2524
offset: int = 0,
2625
limit: int = Query(default=100, lte=100),

api/utils/generic_models.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
from pydantic.types import Optional
21
from sqlmodel import Field, SQLModel
32

43

54
class HeroTeamLink(SQLModel, table=True):
6-
team_id: Optional[int] = Field(
7-
default=None, foreign_key="team.id", primary_key=True
8-
)
9-
hero_id: Optional[int] = Field(
10-
default=None, foreign_key="hero.id", primary_key=True
11-
)
5+
team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True)
6+
hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True)

api/utils/logger.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import logging
2+
3+
4+
def logger_config(module):
5+
"""
6+
Logger function. Extends Python loggin module and set a custom config.
7+
params: Module Name. e.i: logger_config(__name__).
8+
return: Custom logger_config Object.
9+
"""
10+
formatter = logging.Formatter("%(asctime) %(levelname) %(message)")
11+
handler = logging.StreamHandler()
12+
handler.setFormatter(formatter)
13+
14+
custom_logger = logging.getLogger(module)
15+
custom_logger.setLevel(logging.DEBUG)
16+
17+
custom_logger.addHandler(handler)
18+
19+
return custom_logger

0 commit comments

Comments
 (0)