Skip to content

Commit 807229b

Browse files
committed
first commit 🚀
1 parent 1ce8aee commit 807229b

34 files changed

+2122
-0
lines changed

‎.env.example‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Application Settings
2+
PROJECT_NAME=Hero API
3+
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/heroes
4+
DEBUG=true
5+
6+
# JWT Settings
7+
JWT_SECRET=your-secret-key-change-in-production
8+
JWT_ALGORITHM=HS256
9+
JWT_EXPIRATION=30

‎.github/workflows/ci.yml‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Install uv
16+
uses: astral-sh/setup-uv@v4
17+
with:
18+
version: "0.5.8"
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "pyproject.toml"
24+
25+
- name: Install the project
26+
run: uv sync --all-extras
27+
28+
- name: Check imports with isort
29+
run: uv run isort --check-only --diff .
30+
31+
- name: Check code formatting with black
32+
run: uv run black --check .
33+
34+
test:
35+
needs: lint
36+
runs-on: ubuntu-latest
37+
strategy:
38+
matrix:
39+
python-version: ["3.12"]
40+
41+
steps:
42+
- uses: actions/checkout@v4
43+
44+
- name: Install uv
45+
uses: astral-sh/setup-uv@v4
46+
with:
47+
version: "0.5.8"
48+
49+
- name: Set up Python ${{ matrix.python-version }}
50+
uses: actions/setup-python@v5
51+
with:
52+
python-version: "pyproject.toml"
53+
54+
- name: Install the project
55+
run: uv sync --all-extras
56+
57+
- name: Run tests
58+
run: uv run pytest tests
59+
env:
60+
DATABASE_URL: "postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
61+
SECRET_KEY: "test_secret_key"
62+
ALGORITHM: "HS256"
63+
64+
services:
65+
postgres:
66+
image: postgres:16
67+
env:
68+
POSTGRES_USER: postgres
69+
POSTGRES_PASSWORD: postgres
70+
POSTGRES_DB: test_db
71+
ports:
72+
- 5432:5432
73+
options: >-
74+
--health-cmd pg_isready
75+
--health-interval 10s
76+
--health-timeout 5s
77+
--health-retries 5

‎.python-version‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

‎.vscode/launch.json‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "FastAPI: Debug",
6+
"type": "debugpy",
7+
"request": "launch",
8+
"module": "uvicorn",
9+
"args": [
10+
"api.main:app",
11+
"--host",
12+
"0.0.0.0",
13+
"--port",
14+
"8000",
15+
"--reload"
16+
],
17+
"jinja": true,
18+
"justMyCode": true,
19+
"env": {
20+
"PYTHONPATH": "${workspaceFolder}"
21+
}
22+
},
23+
{
24+
"name": "FastAPI: Run with Reload",
25+
"type": "debugpy",
26+
"request": "launch",
27+
"module": "uvicorn",
28+
"args": [
29+
"api.main:app",
30+
"--host",
31+
"0.0.0.0",
32+
"--port",
33+
"8000",
34+
"--reload"
35+
],
36+
"jinja": true,
37+
"env": {
38+
"PYTHONPATH": "${workspaceFolder}"
39+
}
40+
},
41+
{
42+
"name": "FastAPI: Run without Reload",
43+
"type": "debugpy",
44+
"request": "launch",
45+
"module": "uvicorn",
46+
"args": [
47+
"api.main:app",
48+
"--host",
49+
"0.0.0.0",
50+
"--port",
51+
"8000"
52+
],
53+
"jinja": true,
54+
"env": {
55+
"PYTHONPATH": "${workspaceFolder}"
56+
}
57+
}
58+
]
59+
}

‎README.md‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Hero API
2+
3+
A FastAPI-based CRUD application for managing heroes, following best practices and modern Python async patterns.
4+
5+
## Features
6+
7+
- Full CRUD operations for heroes
8+
- Async SQLAlchemy with PostgreSQL
9+
- Alembic migrations
10+
- Clean architecture with repository pattern
11+
- Custom exception handling
12+
- Type hints and documentation
13+
14+
## Project Structure
15+
16+
```
17+
app/
18+
├── core/
19+
│ ├── config.py # Application configuration
20+
│ └── database.py # Database setup and session management
21+
├── heroes/
22+
│ ├── exceptions.py # Custom exceptions
23+
│ ├── models.py # SQLAlchemy models
24+
│ ├── repository.py # Database operations
25+
│ ├── routes.py # API endpoints
26+
│ ├── schemas.py # Pydantic models
27+
│ └── service.py # Business logic
28+
└── main.py # FastAPI application setup
29+
```
30+
31+
## Requirements
32+
33+
- Python 3.8+
34+
- PostgreSQL
35+
- Dependencies listed in requirements.txt
36+
37+
## Setup
38+
39+
1. Create a virtual environment:
40+
```bash
41+
python -m venv venv
42+
source venv/bin/activate # Linux/Mac
43+
venv\Scripts\activate # Windows
44+
```
45+
46+
2. Install dependencies:
47+
```bash
48+
pip install -r requirements.txt
49+
```
50+
51+
3. Create PostgreSQL database:
52+
```sql
53+
CREATE DATABASE hero_db;
54+
```
55+
56+
4. Set up environment variables (or create .env file):
57+
```
58+
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/hero_db
59+
```
60+
61+
5. Run database migrations:
62+
```bash
63+
alembic upgrade head
64+
```
65+
66+
6. Start the application:
67+
```bash
68+
uvicorn app.main:app --reload
69+
```
70+
71+
## API Endpoints
72+
73+
- `GET /heroes` - List all heroes
74+
- `GET /heroes/{id}` - Get a specific hero
75+
- `POST /heroes` - Create a new hero
76+
- `PATCH /heroes/{id}` - Update a hero
77+
- `DELETE /heroes/{id}` - Delete a hero
78+
79+
## Example Usage
80+
81+
Create a new hero:
82+
```bash
83+
curl -X POST "http://localhost:8000/heroes/" -H "Content-Type: application/json" -d '{
84+
"name": "Peter Parker",
85+
"alias": "Spider-Man",
86+
"powers": "Wall-crawling, super strength, spider-sense"
87+
}'
88+
```
89+
90+
## Development
91+
92+
To create a new database migration:
93+
```bash
94+
alembic revision --autogenerate -m "description"
95+
```
96+
97+
To apply migrations:
98+
```bash
99+
alembic upgrade head
100+
```

‎alembic.ini‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[alembic]
2+
script_location = alembic
3+
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/hero_db
4+
5+
[loggers]
6+
keys = root,sqlalchemy,alembic
7+
8+
[handlers]
9+
keys = console
10+
11+
[formatters]
12+
keys = generic
13+
14+
[logger_root]
15+
level = INFO
16+
handlers = console
17+
qualname =
18+
19+
[logger_sqlalchemy]
20+
level = ERROR
21+
handlers =
22+
qualname = sqlalchemy.engine
23+
24+
[logger_alembic]
25+
level = ERROR
26+
handlers =
27+
qualname = alembic
28+
29+
[handler_console]
30+
class = StreamHandler
31+
args = (sys.stderr,)
32+
level = NOTSET
33+
formatter = generic
34+
35+
[formatter_generic]
36+
format = %(levelname)-5.5s [%(name)s] %(message)s
37+
datefmt = %H:%M:%S

‎alembic/env.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import asyncio
2+
import os
3+
import sys
4+
from logging.config import fileConfig
5+
6+
# Add the project root directory to the Python path
7+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8+
9+
from sqlalchemy import pool
10+
from sqlalchemy.engine import Connection
11+
from sqlalchemy.ext.asyncio import async_engine_from_config
12+
13+
from alembic import context
14+
from api.core.config import settings
15+
16+
# Import your models here
17+
from api.core.database import Base
18+
19+
# this is the Alembic Config object
20+
config = context.config
21+
22+
# Interpret the config file for Python logging
23+
if config.config_file_name is not None:
24+
fileConfig(config.config_file_name)
25+
26+
# Set sqlalchemy.url
27+
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
28+
29+
# Add your model's MetaData object here for 'autogenerate' support
30+
target_metadata = Base.metadata
31+
32+
33+
def run_migrations_offline() -> None:
34+
"""Run migrations in 'offline' mode."""
35+
url = config.get_main_option("sqlalchemy.url")
36+
context.configure(
37+
url=url,
38+
target_metadata=target_metadata,
39+
literal_binds=True,
40+
dialect_opts={"paramstyle": "named"},
41+
)
42+
43+
with context.begin_transaction():
44+
context.run_migrations()
45+
46+
47+
def do_run_migrations(connection: Connection) -> None:
48+
context.configure(connection=connection, target_metadata=target_metadata)
49+
50+
with context.begin_transaction():
51+
context.run_migrations()
52+
53+
54+
async def run_async_migrations() -> None:
55+
"""In this scenario we need to create an Engine
56+
and associate a connection with the context."""
57+
58+
connectable = async_engine_from_config(
59+
config.get_section(config.config_ini_section, {}),
60+
prefix="sqlalchemy.",
61+
poolclass=pool.NullPool,
62+
)
63+
64+
async with connectable.connect() as connection:
65+
await connection.run_sync(do_run_migrations)
66+
67+
await connectable.dispose()
68+
69+
70+
def run_migrations_online() -> None:
71+
"""Run migrations in 'online' mode."""
72+
73+
asyncio.run(run_async_migrations())
74+
75+
76+
if context.is_offline_mode():
77+
run_migrations_offline()
78+
else:
79+
run_migrations_online()

‎alembic/script.py.mako‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""${message}
2+
3+
Revision ID: ${up_revision}
4+
Revises: ${down_revision | comma,n}
5+
Create Date: ${create_date}
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
${imports if imports else ""}
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = ${repr(up_revision)}
16+
down_revision: Union[str, None] = ${repr(down_revision)}
17+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19+
20+
21+
def upgrade() -> None:
22+
${upgrades if upgrades else "pass"}
23+
24+
25+
def downgrade() -> None:
26+
${downgrades if downgrades else "pass"}

0 commit comments

Comments
 (0)