WiP: Trying to add postgresql to porject

This commit is contained in:
t0xa 2025-09-07 22:57:52 +03:00
parent 6974cca514
commit 4ccdc39529
8 changed files with 82 additions and 20 deletions

View file

@ -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

View file

@ -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()

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -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" },