Add sqlite + 0002 migration
This commit is contained in:
parent
4ccdc39529
commit
5c8a4566bd
8 changed files with 74 additions and 13 deletions
|
|
@ -9,6 +9,8 @@ class Settings(BaseSettings):
|
||||||
version: str = "0.0.1"
|
version: str = "0.0.1"
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
|
|
||||||
|
# TODO: Add sqlite 3 settings
|
||||||
|
|
||||||
# Postgres database settings
|
# Postgres database settings
|
||||||
POSTGRES_DATABASE_URL: Optional[str] = None
|
POSTGRES_DATABASE_URL: Optional[str] = None
|
||||||
POSTGRES_DATABASE_HOST: str = "localhost"
|
POSTGRES_DATABASE_HOST: str = "localhost"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ from app.api.v1.web import web_router
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from migrations.runner import MigrationRunner
|
from migrations.runner import MigrationRunner
|
||||||
|
|
||||||
|
# TODO: Replace level with settings value
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
|
@ -24,7 +29,7 @@ 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
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="navbar navbar-dark bg-primary">
|
<nav class="navbar navbar-dark bg-primary">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="/">
|
<!-- TODO: find solution to not harcode links in hrefs -->
|
||||||
|
<a class="navbar-brand" href="/app/obsidian/list">
|
||||||
<strong>{{ app_name }}</strong>
|
<strong>{{ app_name }}</strong>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ 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}")
|
logging.info(
|
||||||
|
f"Migrator initializer: engine = {self.engine}, migrations_dir = {self.migrations_dir}"
|
||||||
|
)
|
||||||
|
|
||||||
async def get_applied_migrations(self) -> set:
|
async def get_applied_migrations(self) -> set:
|
||||||
"""Get list of applied migrations"""
|
"""Get list of applied migrations"""
|
||||||
|
|
@ -29,7 +31,9 @@ class MigrationRunner:
|
||||||
result = await conn.execute(
|
result = await conn.execute(
|
||||||
text(f"SELECT version FROM {self.MIGRATIONS_SCHEMA_NAME}")
|
text(f"SELECT version FROM {self.MIGRATIONS_SCHEMA_NAME}")
|
||||||
)
|
)
|
||||||
return {row[0] for row in result.fetchall()}
|
result_data = {row[0] for row in result.fetchall()}
|
||||||
|
logging.info(f"Received migrator data: {result_data}")
|
||||||
|
return result_data
|
||||||
|
|
||||||
async def run_migrations(self):
|
async def run_migrations(self):
|
||||||
"""Run all unapplied migrations"""
|
"""Run all unapplied migrations"""
|
||||||
|
|
@ -38,7 +42,9 @@ class MigrationRunner:
|
||||||
|
|
||||||
# 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
|
||||||
migration_files = sorted([file for file in self.migrations_dir.glob(".*sql")])
|
migration_files = self.migrations_dir.glob('.*sql')
|
||||||
|
migration_files = sorted([file for file in self.migrations_dir.glob("*.sql")])
|
||||||
|
logging.info(f"Migration files: {migration_files}")
|
||||||
|
|
||||||
for migration_file in migration_files:
|
for migration_file in migration_files:
|
||||||
# TODO: (#ToLearn) Read about stem property
|
# TODO: (#ToLearn) Read about stem property
|
||||||
|
|
@ -52,19 +58,19 @@ class MigrationRunner:
|
||||||
|
|
||||||
async def _apply_migration(self, migration_file: Path, migration_name: str):
|
async def _apply_migration(self, migration_file: Path, migration_name: str):
|
||||||
"""Apply migrations from migrations/sql folder"""
|
"""Apply migrations from migrations/sql folder"""
|
||||||
with open(migration_file, 'r', encoding='utf-8') as f:
|
with open(migration_file, "r", encoding="utf-8") as f:
|
||||||
sql_content = f.read()
|
sql_content = f.read()
|
||||||
|
|
||||||
async with self.engine.begin() as conn:
|
async with self.engine.begin() as conn:
|
||||||
try:
|
try:
|
||||||
statements = [s.strip() for s in sql_content.split(';') if s.strip()]
|
statements = [s.strip() for s in sql_content.split(";") if s.strip()]
|
||||||
|
|
||||||
for statement in statements:
|
for statement in statements:
|
||||||
if statement:
|
if statement:
|
||||||
await conn.execute(text(statement))
|
await conn.execute(text(statement))
|
||||||
|
|
||||||
logging.info(f"Migration {migration_name} applied successfully")
|
logging.info(f"Migration {migration_name} applied successfully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error applying migration {migration_name}: {e}")
|
logging.error(f"Error applying migration {migration_name}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
28
migrations/sql/0002_trainings.sql
Normal file
28
migrations/sql/0002_trainings.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
-- Migration: 0002_trainings
|
||||||
|
-- Description: Basic migration to initialize and create basic tarinings table
|
||||||
|
|
||||||
|
-- Schemas for trainings
|
||||||
|
-- Trainings table
|
||||||
|
CREATE TABLE IF NOT EXISTS trainings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
trainer VARCHAR(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Exercies table
|
||||||
|
CREATE TABLE IF NOT EXISTS exercises (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
training_id INTEGER REFERENCES trainings(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
splitted_weigh BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Approaches table
|
||||||
|
CREATE TABLE IF NOT EXISTS approaches (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
approach_id INTEGER REFERENCES approaches(id) ON DELETE CASCADE,
|
||||||
|
weight FLOAT NOT NULL,
|
||||||
|
reps INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('0002_trainings');
|
||||||
|
|
@ -15,6 +15,7 @@ dependencies = [
|
||||||
"python-multipart>=0.0.6",
|
"python-multipart>=0.0.6",
|
||||||
"sqlalchemy>=2.0.43",
|
"sqlalchemy>=2.0.43",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
|
"aiosqlite>=0.21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|
@ -23,3 +24,7 @@ dev = [
|
||||||
"types-grpcio>=1.0.0.20250703",
|
"types-grpcio>=1.0.0.20250703",
|
||||||
"types-protobuf>=6.30.2.20250822",
|
"types-protobuf>=6.30.2.20250822",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tools.pyright]
|
||||||
|
venvPath = "."
|
||||||
|
venv = ".venv"
|
||||||
|
|
|
||||||
2
run.py
2
run.py
|
|
@ -11,4 +11,4 @@ if __name__ == "__main__":
|
||||||
port=8000,
|
port=8000,
|
||||||
reload=True,
|
reload=True,
|
||||||
log_level="info"
|
log_level="info"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
16
uv.lock
16
uv.lock
|
|
@ -1,7 +1,19 @@
|
||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosqlite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -66,6 +78,7 @@ name = "f1tness-parser"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "aiosqlite" },
|
||||||
{ name = "asyncpg" },
|
{ name = "asyncpg" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
|
|
@ -87,6 +100,7 @@ dev = [
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "aiosqlite", specifier = ">=0.21.0" },
|
||||||
{ name = "asyncpg", specifier = ">=0.30.0" },
|
{ 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" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue