WiP: Trying to add postgresql to porject
This commit is contained in:
parent
6974cca514
commit
4ccdc39529
8 changed files with 82 additions and 20 deletions
10
.env.example
10
.env.example
|
|
@ -1,9 +1,9 @@
|
||||||
# PostgreSQL Database Configuration
|
# PostgreSQL Database Configuration
|
||||||
POSTGRES_DB=your_database_name
|
POSTGRES_DATABASE_DB=your_database_name
|
||||||
POSTGRES_USER=your_username
|
POSTGRES_DATABASE_USER=your_username
|
||||||
POSTGRES_PASSWORD=your_secure_password
|
POSTGRES_DATABASE_PASSWORD=your_secure_password
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_DATABASE_HOST=postgres
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_DATABASE_PORT=5432
|
||||||
|
|
||||||
# FastAPI Configuration
|
# FastAPI Configuration
|
||||||
PYTHONPATH=/app
|
PYTHONPATH=/app
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
# Base app settings
|
# Base app settings
|
||||||
|
model_config = SettingsConfigDict(env_file=".env")
|
||||||
app_name: str = "F1tness Parser API"
|
app_name: str = "F1tness Parser API"
|
||||||
version: str = "0.0.1"
|
version: str = "0.0.1"
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
|
|
@ -12,13 +13,10 @@ class Settings(BaseSettings):
|
||||||
POSTGRES_DATABASE_URL: Optional[str] = None
|
POSTGRES_DATABASE_URL: Optional[str] = None
|
||||||
POSTGRES_DATABASE_HOST: str = "localhost"
|
POSTGRES_DATABASE_HOST: str = "localhost"
|
||||||
POSTGRES_DATABASE_PORT: int = 5432
|
POSTGRES_DATABASE_PORT: int = 5432
|
||||||
POSTGRES_DATABASE_NAME: str = "f1tness_db"
|
POSTGRES_DATABASE_DB: str = "f1tness_db"
|
||||||
POSTGRES_DATABASE_USER: str = "postgres"
|
POSTGRES_DATABASE_USER: str = "postgres"
|
||||||
POSTGRES_DATABASE_PASSWORD: str = "password"
|
POSTGRES_DATABASE_PASSWORD: str = "password"
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
|
|
||||||
def get_postgres_database_url(self, async_driver: bool = True) -> str:
|
def get_postgres_database_url(self, async_driver: bool = True) -> str:
|
||||||
"""Method for receiving relational database URL"""
|
"""Method for receiving relational database URL"""
|
||||||
|
|
||||||
|
|
@ -30,9 +28,8 @@ class Settings(BaseSettings):
|
||||||
return (
|
return (
|
||||||
f"{driver}://{self.POSTGRES_DATABASE_USER}:{self.POSTGRES_DATABASE_PASSWORD}"
|
f"{driver}://{self.POSTGRES_DATABASE_USER}:{self.POSTGRES_DATABASE_PASSWORD}"
|
||||||
f"@{self.POSTGRES_DATABASE_HOST}"
|
f"@{self.POSTGRES_DATABASE_HOST}"
|
||||||
f":{self.POSTGRES_DATABASE_PORT}/{self.POSTGRES_DATABASE_NAME}"
|
f":{self.POSTGRES_DATABASE_PORT}/{self.POSTGRES_DATABASE_DB}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|
|
||||||
17
app/main.py
17
app/main.py
|
|
@ -1,13 +1,30 @@
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from app.api.v1.api import api_router
|
from app.api.v1.api import api_router
|
||||||
from app.api.v1.web import web_router
|
from app.api.v1.web import web_router
|
||||||
|
from app.config import settings
|
||||||
|
from migrations.runner import MigrationRunner
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
logging.info(f"\n\n{settings.get_postgres_database_url()}")
|
||||||
|
runner = MigrationRunner(settings.get_postgres_database_url())
|
||||||
|
await runner.run_migrations()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: (#ToLearn) Почитать про lifespan в FastAPI приложениях
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Fitness Parser API",
|
title="Fitness Parser API",
|
||||||
description="API for parsing fitness training data from various sources",
|
description="API for parsing fitness training data from various sources",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||||
|
|
|
||||||
22
compose.yaml
22
compose.yaml
|
|
@ -11,21 +11,29 @@ services:
|
||||||
- ./app:/app/app:ro
|
- ./app:/app/app:ro
|
||||||
- ./data:/app/data:ro
|
- ./data:/app/data:ro
|
||||||
- ./tests:/app/tests:ro
|
- ./tests:/app/tests:ro
|
||||||
|
- ./migrations:/app/migrations:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- PYTHONPATH=/app
|
- PYTHONPATH=${PYTHONPATH}
|
||||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/app
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/app
|
||||||
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
condition: service_healthy
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=fitness_db
|
- POSTGRES_DB=${POSTGRES_DATABASE_DB}
|
||||||
- POSTGRES_USER=fitness_user
|
- POSTGRES_USER=${POSTGRES_DATABASE_USER}
|
||||||
- POSTGRES_PASSWORD=fitness_password
|
- POSTGRES_PASSWORD=${POSTGRES_DATABASE_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "${POSTGRES_DATABASE_PORT}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U fitness_user -d fitness_db"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_DATABASE_USER} -d ${POSTGRES_DATABASE_DB}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class MigrationRunner:
|
||||||
def __init__(self, database_url: str) -> None:
|
def __init__(self, database_url: str) -> None:
|
||||||
self.engine = create_async_engine(database_url)
|
self.engine = create_async_engine(database_url)
|
||||||
self.migrations_dir = Path("migrations/sql")
|
self.migrations_dir = Path("migrations/sql")
|
||||||
|
logging.error(f"{database_url}")
|
||||||
|
|
||||||
async def get_applied_migrations(self) -> set:
|
async def get_applied_migrations(self) -> set:
|
||||||
"""Get list of applied migrations"""
|
"""Get list of applied migrations"""
|
||||||
|
|
@ -33,6 +34,7 @@ class MigrationRunner:
|
||||||
async def run_migrations(self):
|
async def run_migrations(self):
|
||||||
"""Run all unapplied migrations"""
|
"""Run all unapplied migrations"""
|
||||||
applied = await self.get_applied_migrations()
|
applied = await self.get_applied_migrations()
|
||||||
|
logging.info(f"Applied migrations: {applied}")
|
||||||
|
|
||||||
# Getting all sql files from migrations_dir
|
# Getting all sql files from migrations_dir
|
||||||
# TODO: (#ToLearn) Read about Path.glob function
|
# TODO: (#ToLearn) Read about Path.glob function
|
||||||
|
|
@ -47,3 +49,22 @@ class MigrationRunner:
|
||||||
await self._apply_migration(migration_file, migration_name)
|
await self._apply_migration(migration_file, migration_name)
|
||||||
else:
|
else:
|
||||||
logging.info(f"Skipping migration: {migration_name} (already applied)")
|
logging.info(f"Skipping migration: {migration_name} (already applied)")
|
||||||
|
|
||||||
|
async def _apply_migration(self, migration_file: Path, migration_name: str):
|
||||||
|
"""Apply migrations from migrations/sql folder"""
|
||||||
|
with open(migration_file, 'r', encoding='utf-8') as f:
|
||||||
|
sql_content = f.read()
|
||||||
|
|
||||||
|
async with self.engine.begin() as conn:
|
||||||
|
try:
|
||||||
|
statements = [s.strip() for s in sql_content.split(';') if s.strip()]
|
||||||
|
|
||||||
|
for statement in statements:
|
||||||
|
if statement:
|
||||||
|
await conn.execute(text(statement))
|
||||||
|
|
||||||
|
logging.info(f"Migration {migration_name} applied successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error applying migration {migration_name}: {e}")
|
||||||
|
raise
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ dependencies = [
|
||||||
"jinja2>=3.1.0",
|
"jinja2>=3.1.0",
|
||||||
"python-multipart>=0.0.6",
|
"python-multipart>=0.0.6",
|
||||||
"sqlalchemy>=2.0.43",
|
"sqlalchemy>=2.0.43",
|
||||||
|
"asyncpg>=0.30.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|
|
||||||
20
uv.lock
20
uv.lock
|
|
@ -1,5 +1,5 @@
|
||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -24,6 +24,22 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asyncpg"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload-time = "2024-10-20T00:30:00.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload-time = "2024-10-20T00:30:02.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload-time = "2024-10-20T00:30:04.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload-time = "2024-10-20T00:30:06.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.2.1"
|
version = "8.2.1"
|
||||||
|
|
@ -50,6 +66,7 @@ name = "f1tness-parser"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "asyncpg" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
|
@ -70,6 +87,7 @@ dev = [
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "asyncpg", specifier = ">=0.30.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||||
{ name = "jinja2", specifier = ">=3.1.0" },
|
{ name = "jinja2", specifier = ">=3.1.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue