From 53a7983497282c1ea95d782c64f27fc4829ad58a Mon Sep 17 00:00:00 2001 From: t0xa Date: Sun, 7 Sep 2025 11:14:41 +0300 Subject: [PATCH 01/18] Add config settings for project --- DOCS/database_scheme.wsd | 1 + DOCS/deployment.puml | 25 +++++++++++++++++++++++++ app/config.py | 32 ++++++++++++++++++++++++++++---- migrations/__init__.py | 0 migrations/runner.py | 0 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 DOCS/deployment.puml create mode 100644 migrations/__init__.py create mode 100644 migrations/runner.py diff --git a/DOCS/database_scheme.wsd b/DOCS/database_scheme.wsd index 053e886..dfe3745 100644 --- a/DOCS/database_scheme.wsd +++ b/DOCS/database_scheme.wsd @@ -19,6 +19,7 @@ $table("EXERCISE", "exercise"){ $column("NAME") VARCHAR } + $table("APPROACH", "approach"){ $pk("ID") INTEGER NOT NULL $fk("EXERCISE") INTEGER NOT NULL diff --git a/DOCS/deployment.puml b/DOCS/deployment.puml new file mode 100644 index 0000000..13008f2 --- /dev/null +++ b/DOCS/deployment.puml @@ -0,0 +1,25 @@ +@startuml +actor client +agent "web interface" as WEB_UI +agent "ios app" as IOS_APP +agent "telegram app" as TG_APP + +frame docker:f1tness_db { + database "PostgreSQL" as db +} + +frame docker:fastAPI { + node backend_API + node frontend_renderer +} + +client <--> WEB_UI +client <--> IOS_APP +client <--> TG_APP + +WEB_UI <--> frontend_renderer +IOS_APP <--> backend_API +TG_APP <--> backend_API + +backend_API --> db +@enduml \ No newline at end of file diff --git a/app/config.py b/app/config.py index 79864a8..0afd3a1 100644 --- a/app/config.py +++ b/app/config.py @@ -3,12 +3,36 @@ from typing import Optional class Settings(BaseSettings): - app_name: str = "Fitness Parser API" - version: str = "0.1.0" + # Base app settings + app_name: str = "F1tness Parser API" + version: str = "0.0.1" debug: bool = False - + + # Postgres database settings + POSTGRES_DATABASE_URL: Optional[str] = None + POSTGRES_DATABASE_HOST: str = "localhost" + POSTGRES_DATABASE_PORT: int = 5432 + POSTGRES_DATABASE_NAME: str = "f1tness_db" + POSTGRES_DATABASE_USER: str = "postgres" + POSTGRES_DATABASE_PASSWORD: str = "password" + class Config: env_file = ".env" + def get_postgres_database_url(self, async_driver: bool = True) -> str: + """Method for receiving relational database URL""" + + # If POSTGRES_DATABASE_URL is somehow set in env file - return it + if self.POSTGRES_DATABASE_URL: + return self.POSTGRES_DATABASE_URL + + driver = "postgresql+asyncpg" if async_driver else "postgresql" + return ( + f"{driver}://{self.POSTGRES_DATABASE_USER}:{self.POSTGRES_DATABASE_PASSWORD}" + f"@{self.POSTGRES_DATABASE_HOST}" + f":{self.POSTGRES_DATABASE_PORT}/{self.POSTGRES_DATABASE_NAME}" + ) + + +settings = Settings() -settings = Settings() \ No newline at end of file diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/migrations/runner.py b/migrations/runner.py new file mode 100644 index 0000000..e69de29 -- 2.39.5 From 618e30c9d8f1befa60079227a2cfbfa4bc2e460d Mon Sep 17 00:00:00 2001 From: t0xa Date: Sun, 7 Sep 2025 11:46:46 +0300 Subject: [PATCH 02/18] WiP: Implementing runner for migrations --- migrations/0001_initial.sql | 19 ++++++++++++++ migrations/runner.py | 49 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 47 +++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 migrations/0001_initial.sql diff --git a/migrations/0001_initial.sql b/migrations/0001_initial.sql new file mode 100644 index 0000000..717df90 --- /dev/null +++ b/migrations/0001_initial.sql @@ -0,0 +1,19 @@ +-- Migration: 0001_initial +-- Description: Create initial migration with user for f1tness app + +-- Default user schema +-- TODO: (#ToLearn) Read about TIMESTAMP and CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(250) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Schema for tracking applied migrations +CREATE TABLE IF NOT EXISTS schema_migrations ( + version VARCHAR(50) PRIMARY KEY, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO schema_migrations (version) VALUES ('0001_initial'); diff --git a/migrations/runner.py b/migrations/runner.py index e69de29..f774d5e 100644 --- a/migrations/runner.py +++ b/migrations/runner.py @@ -0,0 +1,49 @@ +import logging +from pathlib import Path +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy import text + + +class MigrationRunner: + MIGRATIONS_SCHEMA_NAME: str = "schema_migrations" + + def __init__(self, database_url: str) -> None: + self.engine = create_async_engine(database_url) + self.migrations_dir = Path("migrations/sql") + + async def get_applied_migrations(self) -> set: + """Get list of applied migrations""" + async with self.engine.begin() as conn: + # Create migrations list table if not exists + await conn.execute( + text(f""" + CREATE TABLE IF NOT EXISTS {self.MIGRATIONS_SCHEMA_NAME} ( + version VARCHAR(50) PRIMARY KEY, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """) + ) + + # Receiving list of applied migrations + result = await conn.execute( + text(f"SELECT version FROM {self.MIGRATIONS_SCHEMA_NAME}") + ) + return {row[0] for row in result.fetchall()} + + async def run_migrations(self): + """Run all unapplied migrations""" + applied = await self.get_applied_migrations() + + # Getting all sql files from migrations_dir + # TODO: (#ToLearn) Read about Path.glob function + migration_files = sorted([file for file in self.migrations_dir.glob(".*sql")]) + + for migration_file in migration_files: + # TODO: (#ToLearn) Read about stem property + migration_name = migration_file.stem + + if migration_name not in applied: + logging.info(f"Applying migration: {migration_name}") + await self._apply_migration(migration_file, migration_name) + else: + logging.info(f"Skipping migration: {migration_name} (already applied)") diff --git a/pyproject.toml b/pyproject.toml index 61f73fd..9d76db3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "python-dotenv>=1.0.1", "jinja2>=3.1.0", "python-multipart>=0.0.6", + "sqlalchemy>=2.0.43", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 57e6941..01f8909 100644 --- a/uv.lock +++ b/uv.lock @@ -57,6 +57,7 @@ dependencies = [ { name = "pytest" }, { name = "python-dotenv" }, { name = "python-multipart" }, + { name = "sqlalchemy" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -76,6 +77,7 @@ requires-dist = [ { name = "pytest", specifier = ">=8.3.4" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-multipart", specifier = ">=0.0.6" }, + { name = "sqlalchemy", specifier = ">=2.0.43" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, ] @@ -100,6 +102,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, ] +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -370,6 +396,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +] + [[package]] name = "starlette" version = "0.47.3" -- 2.39.5 From 6974cca5145348f4d172bba98346e920fbb4bb51 Mon Sep 17 00:00:00 2001 From: t0xa Date: Sun, 7 Sep 2025 22:09:03 +0300 Subject: [PATCH 03/18] Working with compose and env files --- .env.example | 12 ++++++++++++ .gitignore | 9 ++++++++- compose.override.yaml | 9 ++++++++- compose.yaml | 25 ++++++++++++++++++------- 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..713a1ee --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# PostgreSQL Database Configuration +POSTGRES_DB=your_database_name +POSTGRES_USER=your_username +POSTGRES_PASSWORD=your_secure_password +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 + +# FastAPI Configuration +PYTHONPATH=/app + +# Development settings +DEBUG=true diff --git a/.gitignore b/.gitignore index c340b9d..01f4c65 100644 --- a/.gitignore +++ b/.gitignore @@ -123,11 +123,14 @@ celerybeat.pid *.sage.py # Environments -.env .venv env/ venv/ ENV/ +.env +.env.local +.env.production + env.bak/ venv.bak/ @@ -163,3 +166,7 @@ cython_debug/ #.idea/ pyrightconfig.json + +# Database +*.db +*.sqlite3 diff --git a/compose.override.yaml b/compose.override.yaml index 1a62945..c558717 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -5,4 +5,11 @@ services: fastapi-app: environment: - DEBUG=true - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/app --log-level debug \ No newline at end of file + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/app --log-level debug + develop: + watch: + - action: sync + path: ./app + target: /app/app + - action: rebuild + path: ./pyproject.toml diff --git a/compose.yaml b/compose.yaml index 69be1b1..96ec801 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,10 +14,21 @@ services: environment: - PYTHONPATH=/app command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/app - develop: - watch: - - action: sync - path: ./app - target: /app/app - - action: rebuild - path: ./pyproject.toml + postgres: + image: postgres:15 + environment: + - POSTGRES_DB=fitness_db + - POSTGRES_USER=fitness_user + - POSTGRES_PASSWORD=fitness_password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fitness_user -d fitness_db"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: -- 2.39.5 From 4ccdc39529633f9f333e842dd3f5c2a7a10fb72e Mon Sep 17 00:00:00 2001 From: t0xa Date: Sun, 7 Sep 2025 22:57:52 +0300 Subject: [PATCH 04/18] WiP: Trying to add postgresql to porject --- .env.example | 10 +++++----- app/config.py | 11 ++++------- app/main.py | 17 +++++++++++++++++ compose.yaml | 22 +++++++++++++++------- migrations/runner.py | 21 +++++++++++++++++++++ migrations/{ => sql}/0001_initial.sql | 0 pyproject.toml | 1 + uv.lock | 20 +++++++++++++++++++- 8 files changed, 82 insertions(+), 20 deletions(-) rename migrations/{ => sql}/0001_initial.sql (100%) diff --git a/.env.example b/.env.example index 713a1ee..526d990 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ # PostgreSQL Database Configuration -POSTGRES_DB=your_database_name -POSTGRES_USER=your_username -POSTGRES_PASSWORD=your_secure_password -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 +POSTGRES_DATABASE_DB=your_database_name +POSTGRES_DATABASE_USER=your_username +POSTGRES_DATABASE_PASSWORD=your_secure_password +POSTGRES_DATABASE_HOST=postgres +POSTGRES_DATABASE_PORT=5432 # FastAPI Configuration PYTHONPATH=/app diff --git a/app/config.py b/app/config.py index 0afd3a1..f47ad35 100644 --- a/app/config.py +++ b/app/config.py @@ -1,9 +1,10 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Optional class Settings(BaseSettings): # Base app settings + model_config = SettingsConfigDict(env_file=".env") app_name: str = "F1tness Parser API" version: str = "0.0.1" debug: bool = False @@ -12,13 +13,10 @@ class Settings(BaseSettings): POSTGRES_DATABASE_URL: Optional[str] = None POSTGRES_DATABASE_HOST: str = "localhost" POSTGRES_DATABASE_PORT: int = 5432 - POSTGRES_DATABASE_NAME: str = "f1tness_db" + POSTGRES_DATABASE_DB: str = "f1tness_db" POSTGRES_DATABASE_USER: str = "postgres" POSTGRES_DATABASE_PASSWORD: str = "password" - class Config: - env_file = ".env" - def get_postgres_database_url(self, async_driver: bool = True) -> str: """Method for receiving relational database URL""" @@ -30,9 +28,8 @@ class Settings(BaseSettings): return ( f"{driver}://{self.POSTGRES_DATABASE_USER}:{self.POSTGRES_DATABASE_PASSWORD}" 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() - diff --git a/app/main.py b/app/main.py index a394083..e3af335 100644 --- a/app/main.py +++ b/app/main.py @@ -1,13 +1,30 @@ +import logging +from contextlib import asynccontextmanager + from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates + from app.api.v1.api import api_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( title="Fitness Parser API", description="API for parsing fitness training data from various sources", version="0.1.0", + lifespan=lifespan ) app.mount("/static", StaticFiles(directory="app/static"), name="static") diff --git a/compose.yaml b/compose.yaml index 96ec801..ee6daa9 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,21 +11,29 @@ services: - ./app:/app/app:ro - ./data:/app/data:ro - ./tests:/app/tests:ro + - ./migrations:/app/migrations:ro + env_file: + - .env environment: - - PYTHONPATH=/app + - PYTHONPATH=${PYTHONPATH} command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/app + depends_on: + postgres: + condition: service_healthy postgres: - image: postgres:15 + image: postgres:15-alpine + env_file: + - .env environment: - - POSTGRES_DB=fitness_db - - POSTGRES_USER=fitness_user - - POSTGRES_PASSWORD=fitness_password + - POSTGRES_DB=${POSTGRES_DATABASE_DB} + - POSTGRES_USER=${POSTGRES_DATABASE_USER} + - POSTGRES_PASSWORD=${POSTGRES_DATABASE_PASSWORD} ports: - - "5432:5432" + - "${POSTGRES_DATABASE_PORT}:5432" volumes: - postgres_data:/var/lib/postgresql/data 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 timeout: 5s retries: 5 diff --git a/migrations/runner.py b/migrations/runner.py index f774d5e..0f70323 100644 --- a/migrations/runner.py +++ b/migrations/runner.py @@ -10,6 +10,7 @@ class MigrationRunner: def __init__(self, database_url: str) -> None: self.engine = create_async_engine(database_url) self.migrations_dir = Path("migrations/sql") + logging.error(f"{database_url}") async def get_applied_migrations(self) -> set: """Get list of applied migrations""" @@ -33,6 +34,7 @@ class MigrationRunner: async def run_migrations(self): """Run all unapplied migrations""" applied = await self.get_applied_migrations() + logging.info(f"Applied migrations: {applied}") # Getting all sql files from migrations_dir # TODO: (#ToLearn) Read about Path.glob function @@ -47,3 +49,22 @@ class MigrationRunner: await self._apply_migration(migration_file, migration_name) else: 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 diff --git a/migrations/0001_initial.sql b/migrations/sql/0001_initial.sql similarity index 100% rename from migrations/0001_initial.sql rename to migrations/sql/0001_initial.sql diff --git a/pyproject.toml b/pyproject.toml index 9d76db3..1c971ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "jinja2>=3.1.0", "python-multipart>=0.0.6", "sqlalchemy>=2.0.43", + "asyncpg>=0.30.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 01f8909..2020598 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[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" }, ] +[[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]] name = "click" version = "8.2.1" @@ -50,6 +66,7 @@ name = "f1tness-parser" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "asyncpg" }, { name = "fastapi" }, { name = "jinja2" }, { name = "pydantic" }, @@ -70,6 +87,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "asyncpg", specifier = ">=0.30.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "jinja2", specifier = ">=3.1.0" }, { name = "pydantic", specifier = ">=2.11.7" }, -- 2.39.5 From 5c8a4566bdfa01dafa9b31095b7bad65562e26c6 Mon Sep 17 00:00:00 2001 From: t0xa Date: Sun, 12 Oct 2025 16:27:47 +0300 Subject: [PATCH 05/18] Add sqlite + 0002 migration --- app/config.py | 2 ++ app/main.py | 7 ++++++- app/templates/base.html | 3 ++- migrations/runner.py | 24 +++++++++++++++--------- migrations/sql/0002_trainings.sql | 28 ++++++++++++++++++++++++++++ pyproject.toml | 5 +++++ run.py | 2 +- uv.lock | 16 +++++++++++++++- 8 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 migrations/sql/0002_trainings.sql diff --git a/app/config.py b/app/config.py index f47ad35..6086eeb 100644 --- a/app/config.py +++ b/app/config.py @@ -9,6 +9,8 @@ class Settings(BaseSettings): version: str = "0.0.1" debug: bool = False + # TODO: Add sqlite 3 settings + # Postgres database settings POSTGRES_DATABASE_URL: Optional[str] = None POSTGRES_DATABASE_HOST: str = "localhost" diff --git a/app/main.py b/app/main.py index e3af335..d852401 100644 --- a/app/main.py +++ b/app/main.py @@ -10,6 +10,11 @@ from app.api.v1.web import web_router from app.config import settings 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 async def lifespan(app: FastAPI): @@ -24,7 +29,7 @@ app = FastAPI( title="Fitness Parser API", description="API for parsing fitness training data from various sources", version="0.1.0", - lifespan=lifespan + lifespan=lifespan, ) app.mount("/static", StaticFiles(directory="app/static"), name="static") diff --git a/app/templates/base.html b/app/templates/base.html index 503835c..ab1b48e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -13,7 +13,8 @@