From d58302c2c8157c9ed830d26153b6f352d0749d95 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Fri, 8 Aug 2025 19:48:03 +0900 Subject: [PATCH] init commit. Skeleton prepared --- .drone.yml | 119 ++++++++++++++++++ .github/workflows/readme.md | 3 + .gitignore | 26 ++++ README.md | 15 +++ docker-compose.yml | 105 ++++++++++++++++ infra/db/init/01_create_databases.sql | 6 + infra/gateway/nginx.conf | 50 ++++++++ services/auth/Dockerfile | 17 +++ services/auth/alembic.ini | 35 ++++++ services/auth/alembic/env.py | 55 ++++++++ services/auth/alembic/versions/.gitkeep | 0 services/auth/docker-entrypoint.sh | 6 + services/auth/requirements.txt | 10 ++ services/auth/src/app/__init__.py | 0 services/auth/src/app/api/__init__.py | 0 services/auth/src/app/api/routes/__init__.py | 0 services/auth/src/app/api/routes/ping.py | 7 ++ services/auth/src/app/core/__init__.py | 0 services/auth/src/app/core/config.py | 9 ++ services/auth/src/app/db/__init__.py | 0 services/auth/src/app/db/session.py | 26 ++++ services/auth/src/app/main.py | 11 ++ services/auth/src/app/models/__init__.py | 0 services/auth/src/app/models/base.py | 1 + .../auth/src/app/repositories/__init__.py | 0 services/auth/src/app/repositories/base.py | 5 + services/auth/src/app/schemas/__init__.py | 0 services/auth/src/app/schemas/common.py | 4 + services/auth/src/app/services/__init__.py | 0 services/auth/src/app/services/base.py | 5 + services/auth/tests/test_health.py | 10 ++ services/chat/Dockerfile | 17 +++ services/chat/alembic.ini | 35 ++++++ services/chat/alembic/env.py | 55 ++++++++ services/chat/alembic/versions/.gitkeep | 0 services/chat/docker-entrypoint.sh | 6 + services/chat/requirements.txt | 10 ++ services/chat/src/app/__init__.py | 0 services/chat/src/app/api/__init__.py | 0 services/chat/src/app/api/routes/__init__.py | 0 services/chat/src/app/api/routes/ping.py | 7 ++ services/chat/src/app/core/__init__.py | 0 services/chat/src/app/core/config.py | 9 ++ services/chat/src/app/db/__init__.py | 0 services/chat/src/app/db/session.py | 26 ++++ services/chat/src/app/main.py | 11 ++ services/chat/src/app/models/__init__.py | 0 services/chat/src/app/models/base.py | 1 + .../chat/src/app/repositories/__init__.py | 0 services/chat/src/app/repositories/base.py | 5 + services/chat/src/app/schemas/__init__.py | 0 services/chat/src/app/schemas/common.py | 4 + services/chat/src/app/services/__init__.py | 0 services/chat/src/app/services/base.py | 5 + services/chat/tests/test_health.py | 10 ++ services/match/Dockerfile | 17 +++ services/match/alembic.ini | 35 ++++++ services/match/alembic/env.py | 55 ++++++++ services/match/alembic/versions/.gitkeep | 0 services/match/docker-entrypoint.sh | 6 + services/match/requirements.txt | 10 ++ services/match/src/app/__init__.py | 0 services/match/src/app/api/__init__.py | 0 services/match/src/app/api/routes/__init__.py | 0 services/match/src/app/api/routes/ping.py | 7 ++ services/match/src/app/core/__init__.py | 0 services/match/src/app/core/config.py | 9 ++ services/match/src/app/db/__init__.py | 0 services/match/src/app/db/session.py | 26 ++++ services/match/src/app/main.py | 11 ++ services/match/src/app/models/__init__.py | 0 services/match/src/app/models/base.py | 1 + .../match/src/app/repositories/__init__.py | 0 services/match/src/app/repositories/base.py | 5 + services/match/src/app/schemas/__init__.py | 0 services/match/src/app/schemas/common.py | 4 + services/match/src/app/services/__init__.py | 0 services/match/src/app/services/base.py | 5 + services/match/tests/test_health.py | 10 ++ services/payments/Dockerfile | 17 +++ services/payments/alembic.ini | 35 ++++++ services/payments/alembic/env.py | 55 ++++++++ services/payments/alembic/versions/.gitkeep | 0 services/payments/docker-entrypoint.sh | 6 + services/payments/requirements.txt | 10 ++ services/payments/src/app/__init__.py | 0 services/payments/src/app/api/__init__.py | 0 .../payments/src/app/api/routes/__init__.py | 0 services/payments/src/app/api/routes/ping.py | 7 ++ services/payments/src/app/core/__init__.py | 0 services/payments/src/app/core/config.py | 9 ++ services/payments/src/app/db/__init__.py | 0 services/payments/src/app/db/session.py | 26 ++++ services/payments/src/app/main.py | 11 ++ services/payments/src/app/models/__init__.py | 0 services/payments/src/app/models/base.py | 1 + .../payments/src/app/repositories/__init__.py | 0 .../payments/src/app/repositories/base.py | 5 + services/payments/src/app/schemas/__init__.py | 0 services/payments/src/app/schemas/common.py | 4 + .../payments/src/app/services/__init__.py | 0 services/payments/src/app/services/base.py | 5 + services/payments/tests/test_health.py | 10 ++ services/profiles/Dockerfile | 17 +++ services/profiles/alembic.ini | 35 ++++++ services/profiles/alembic/env.py | 55 ++++++++ services/profiles/alembic/versions/.gitkeep | 0 services/profiles/docker-entrypoint.sh | 6 + services/profiles/requirements.txt | 10 ++ services/profiles/src/app/__init__.py | 0 services/profiles/src/app/api/__init__.py | 0 .../profiles/src/app/api/routes/__init__.py | 0 services/profiles/src/app/api/routes/ping.py | 7 ++ services/profiles/src/app/core/__init__.py | 0 services/profiles/src/app/core/config.py | 9 ++ services/profiles/src/app/db/__init__.py | 0 services/profiles/src/app/db/session.py | 26 ++++ services/profiles/src/app/main.py | 11 ++ services/profiles/src/app/models/__init__.py | 0 services/profiles/src/app/models/base.py | 1 + .../profiles/src/app/repositories/__init__.py | 0 .../profiles/src/app/repositories/base.py | 5 + services/profiles/src/app/schemas/__init__.py | 0 services/profiles/src/app/schemas/common.py | 4 + .../profiles/src/app/services/__init__.py | 0 services/profiles/src/app/services/base.py | 5 + services/profiles/tests/test_health.py | 10 ++ 127 files changed, 1329 insertions(+) create mode 100644 .drone.yml create mode 100644 .github/workflows/readme.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 infra/db/init/01_create_databases.sql create mode 100644 infra/gateway/nginx.conf create mode 100644 services/auth/Dockerfile create mode 100644 services/auth/alembic.ini create mode 100644 services/auth/alembic/env.py create mode 100644 services/auth/alembic/versions/.gitkeep create mode 100755 services/auth/docker-entrypoint.sh create mode 100644 services/auth/requirements.txt create mode 100644 services/auth/src/app/__init__.py create mode 100644 services/auth/src/app/api/__init__.py create mode 100644 services/auth/src/app/api/routes/__init__.py create mode 100644 services/auth/src/app/api/routes/ping.py create mode 100644 services/auth/src/app/core/__init__.py create mode 100644 services/auth/src/app/core/config.py create mode 100644 services/auth/src/app/db/__init__.py create mode 100644 services/auth/src/app/db/session.py create mode 100644 services/auth/src/app/main.py create mode 100644 services/auth/src/app/models/__init__.py create mode 100644 services/auth/src/app/models/base.py create mode 100644 services/auth/src/app/repositories/__init__.py create mode 100644 services/auth/src/app/repositories/base.py create mode 100644 services/auth/src/app/schemas/__init__.py create mode 100644 services/auth/src/app/schemas/common.py create mode 100644 services/auth/src/app/services/__init__.py create mode 100644 services/auth/src/app/services/base.py create mode 100644 services/auth/tests/test_health.py create mode 100644 services/chat/Dockerfile create mode 100644 services/chat/alembic.ini create mode 100644 services/chat/alembic/env.py create mode 100644 services/chat/alembic/versions/.gitkeep create mode 100755 services/chat/docker-entrypoint.sh create mode 100644 services/chat/requirements.txt create mode 100644 services/chat/src/app/__init__.py create mode 100644 services/chat/src/app/api/__init__.py create mode 100644 services/chat/src/app/api/routes/__init__.py create mode 100644 services/chat/src/app/api/routes/ping.py create mode 100644 services/chat/src/app/core/__init__.py create mode 100644 services/chat/src/app/core/config.py create mode 100644 services/chat/src/app/db/__init__.py create mode 100644 services/chat/src/app/db/session.py create mode 100644 services/chat/src/app/main.py create mode 100644 services/chat/src/app/models/__init__.py create mode 100644 services/chat/src/app/models/base.py create mode 100644 services/chat/src/app/repositories/__init__.py create mode 100644 services/chat/src/app/repositories/base.py create mode 100644 services/chat/src/app/schemas/__init__.py create mode 100644 services/chat/src/app/schemas/common.py create mode 100644 services/chat/src/app/services/__init__.py create mode 100644 services/chat/src/app/services/base.py create mode 100644 services/chat/tests/test_health.py create mode 100644 services/match/Dockerfile create mode 100644 services/match/alembic.ini create mode 100644 services/match/alembic/env.py create mode 100644 services/match/alembic/versions/.gitkeep create mode 100755 services/match/docker-entrypoint.sh create mode 100644 services/match/requirements.txt create mode 100644 services/match/src/app/__init__.py create mode 100644 services/match/src/app/api/__init__.py create mode 100644 services/match/src/app/api/routes/__init__.py create mode 100644 services/match/src/app/api/routes/ping.py create mode 100644 services/match/src/app/core/__init__.py create mode 100644 services/match/src/app/core/config.py create mode 100644 services/match/src/app/db/__init__.py create mode 100644 services/match/src/app/db/session.py create mode 100644 services/match/src/app/main.py create mode 100644 services/match/src/app/models/__init__.py create mode 100644 services/match/src/app/models/base.py create mode 100644 services/match/src/app/repositories/__init__.py create mode 100644 services/match/src/app/repositories/base.py create mode 100644 services/match/src/app/schemas/__init__.py create mode 100644 services/match/src/app/schemas/common.py create mode 100644 services/match/src/app/services/__init__.py create mode 100644 services/match/src/app/services/base.py create mode 100644 services/match/tests/test_health.py create mode 100644 services/payments/Dockerfile create mode 100644 services/payments/alembic.ini create mode 100644 services/payments/alembic/env.py create mode 100644 services/payments/alembic/versions/.gitkeep create mode 100755 services/payments/docker-entrypoint.sh create mode 100644 services/payments/requirements.txt create mode 100644 services/payments/src/app/__init__.py create mode 100644 services/payments/src/app/api/__init__.py create mode 100644 services/payments/src/app/api/routes/__init__.py create mode 100644 services/payments/src/app/api/routes/ping.py create mode 100644 services/payments/src/app/core/__init__.py create mode 100644 services/payments/src/app/core/config.py create mode 100644 services/payments/src/app/db/__init__.py create mode 100644 services/payments/src/app/db/session.py create mode 100644 services/payments/src/app/main.py create mode 100644 services/payments/src/app/models/__init__.py create mode 100644 services/payments/src/app/models/base.py create mode 100644 services/payments/src/app/repositories/__init__.py create mode 100644 services/payments/src/app/repositories/base.py create mode 100644 services/payments/src/app/schemas/__init__.py create mode 100644 services/payments/src/app/schemas/common.py create mode 100644 services/payments/src/app/services/__init__.py create mode 100644 services/payments/src/app/services/base.py create mode 100644 services/payments/tests/test_health.py create mode 100644 services/profiles/Dockerfile create mode 100644 services/profiles/alembic.ini create mode 100644 services/profiles/alembic/env.py create mode 100644 services/profiles/alembic/versions/.gitkeep create mode 100755 services/profiles/docker-entrypoint.sh create mode 100644 services/profiles/requirements.txt create mode 100644 services/profiles/src/app/__init__.py create mode 100644 services/profiles/src/app/api/__init__.py create mode 100644 services/profiles/src/app/api/routes/__init__.py create mode 100644 services/profiles/src/app/api/routes/ping.py create mode 100644 services/profiles/src/app/core/__init__.py create mode 100644 services/profiles/src/app/core/config.py create mode 100644 services/profiles/src/app/db/__init__.py create mode 100644 services/profiles/src/app/db/session.py create mode 100644 services/profiles/src/app/main.py create mode 100644 services/profiles/src/app/models/__init__.py create mode 100644 services/profiles/src/app/models/base.py create mode 100644 services/profiles/src/app/repositories/__init__.py create mode 100644 services/profiles/src/app/repositories/base.py create mode 100644 services/profiles/src/app/schemas/__init__.py create mode 100644 services/profiles/src/app/schemas/common.py create mode 100644 services/profiles/src/app/services/__init__.py create mode 100644 services/profiles/src/app/services/base.py create mode 100644 services/profiles/tests/test_health.py diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..ece62a1 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,119 @@ +--- +kind: pipeline +type: docker +name: ci + +trigger: + event: [ push, pull_request ] + +steps: + - name: test-auth + image: python:3.12 + environment: + PYTHONPATH: services/auth/src + commands: + - python -V + - pip install --no-cache-dir -r services/auth/requirements.txt + - python -m pytest -q services/auth/tests + - name: test-profiles + image: python:3.12 + environment: + PYTHONPATH: services/profiles/src + commands: + - python -V + - pip install --no-cache-dir -r services/profiles/requirements.txt + - python -m pytest -q services/profiles/tests + - name: test-match + image: python:3.12 + environment: + PYTHONPATH: services/match/src + commands: + - python -V + - pip install --no-cache-dir -r services/match/requirements.txt + - python -m pytest -q services/match/tests + - name: test-chat + image: python:3.12 + environment: + PYTHONPATH: services/chat/src + commands: + - python -V + - pip install --no-cache-dir -r services/chat/requirements.txt + - python -m pytest -q services/chat/tests + - name: test-payments + image: python:3.12 + environment: + PYTHONPATH: services/payments/src + commands: + - python -V + - pip install --no-cache-dir -r services/payments/requirements.txt + - python -m pytest -q services/payments/tests + - name: build-auth + image: plugins/docker + privileged: true + settings: + context: services/auth + dockerfile: services/auth/Dockerfile + repo: registry.example.com/your-namespace/marriage-auth + tags: [ latest ] + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: [ push ] + - name: build-profiles + image: plugins/docker + privileged: true + settings: + context: services/profiles + dockerfile: services/profiles/Dockerfile + repo: registry.example.com/your-namespace/marriage-profiles + tags: [ latest ] + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: [ push ] + - name: build-match + image: plugins/docker + privileged: true + settings: + context: services/match + dockerfile: services/match/Dockerfile + repo: registry.example.com/your-namespace/marriage-match + tags: [ latest ] + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: [ push ] + - name: build-chat + image: plugins/docker + privileged: true + settings: + context: services/chat + dockerfile: services/chat/Dockerfile + repo: registry.example.com/your-namespace/marriage-chat + tags: [ latest ] + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: [ push ] + - name: build-payments + image: plugins/docker + privileged: true + settings: + context: services/payments + dockerfile: services/payments/Dockerfile + repo: registry.example.com/your-namespace/marriage-payments + tags: [ latest ] + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: [ push ] diff --git a/.github/workflows/readme.md b/.github/workflows/readme.md new file mode 100644 index 0000000..9f9e68c --- /dev/null +++ b/.github/workflows/readme.md @@ -0,0 +1,3 @@ +# CI Note +This project uses Drone CI (.drone.yml). +This placeholder keeps the directory in the repo. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa209b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*.so +*.egg-info/ +.eggs/ +.venv/ +venv/ +.env +.env.* +.pytest_cache/ +.mypy_cache/ +coverage.xml +htmlcov/ +dist/ +build/ + +# IDE / OS +.DS_Store +.idea/ +.vscode/ + +# Docker +**/.python_packages/ +**/.pytest_cache/ +**/.ruff_cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c495c1c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Match Agency — Python Microservices Skeleton + +**Stack**: Python (FastAPI) • PostgreSQL • SQLAlchemy 2.0 • Alembic • Docker Compose +**Gateway**: Nginx • **CI/CD**: Drone + +## Quick Start +1) `cp .env.example .env` +2) `docker compose up --build` +3) Gateway: http://localhost:8080 (routes: /auth, /profiles, /match, /chat, /payments) + +## Structure +- services/* — микросервисы (ООП-слои: repositories/services) +- infra/db/init — init-скрипты Postgres (создание БД под сервисы) +- infra/gateway — конфиг Nginx (API gateway) +- .drone.yml — CI/CD (pytest + docker build) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..63aa1a6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,105 @@ +version: "3.9" +services: + postgres: + image: postgres:16 + container_name: postgres + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + - ./infra/db/init:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}"] + interval: 5s + timeout: 5s + retries: 40 + + gateway: + image: nginx:alpine + container_name: gateway + depends_on: + - auth + - profiles + - match + - chat + - payments + ports: + - "8080:80" + volumes: + - ./infra/gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro + + auth: + build: + context: ./services/auth + container_name: marriage_auth + env_file: + - .env + environment: + DATABASE_URL: ${AUTH_DATABASE_URL} + depends_on: + - postgres + ports: + - "${AUTH_PORT:-8001}:8000" + command: ./docker-entrypoint.sh + + profiles: + build: + context: ./services/profiles + container_name: marriage_profiles + env_file: + - .env + environment: + DATABASE_URL: ${PROFILES_DATABASE_URL} + depends_on: + - postgres + ports: + - "${PROFILES_PORT:-8002}:8000" + command: ./docker-entrypoint.sh + + match: + build: + context: ./services/match + container_name: marriage_match + env_file: + - .env + environment: + DATABASE_URL: ${MATCH_DATABASE_URL} + depends_on: + - postgres + ports: + - "${MATCH_PORT:-8003}:8000" + command: ./docker-entrypoint.sh + + chat: + build: + context: ./services/chat + container_name: marriage_chat + env_file: + - .env + environment: + DATABASE_URL: ${CHAT_DATABASE_URL} + depends_on: + - postgres + ports: + - "${CHAT_PORT:-8004}:8000" + command: ./docker-entrypoint.sh + + payments: + build: + context: ./services/payments + container_name: marriage_payments + env_file: + - .env + environment: + DATABASE_URL: ${PAYMENTS_DATABASE_URL} + depends_on: + - postgres + ports: + - "${PAYMENTS_PORT:-8005}:8000" + command: ./docker-entrypoint.sh + +volumes: + pgdata: diff --git a/infra/db/init/01_create_databases.sql b/infra/db/init/01_create_databases.sql new file mode 100644 index 0000000..6680ac5 --- /dev/null +++ b/infra/db/init/01_create_databases.sql @@ -0,0 +1,6 @@ +-- Executed once on fresh Postgres volume +CREATE DATABASE auth_db; +CREATE DATABASE profiles_db; +CREATE DATABASE match_db; +CREATE DATABASE chat_db; +CREATE DATABASE payments_db; diff --git a/infra/gateway/nginx.conf b/infra/gateway/nginx.conf new file mode 100644 index 0000000..784757b --- /dev/null +++ b/infra/gateway/nginx.conf @@ -0,0 +1,50 @@ +server { + listen 80; + server_name _; + + # Health of gateway itself + location = /health { + default_type application/json; + return 200 '{"status":"ok","gateway":"nginx"}'; + } + location /auth/ { + proxy_pass http://auth:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /profiles/ { + proxy_pass http://profiles:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /match/ { + proxy_pass http://match:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /chat/ { + proxy_pass http://chat:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /payments/ { + proxy_pass http://payments:8000/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/services/auth/Dockerfile b/services/auth/Dockerfile new file mode 100644 index 0000000..57c86d2 --- /dev/null +++ b/services/auth/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +COPY src ./src +COPY alembic.ini ./ +COPY alembic ./alembic +COPY docker-entrypoint.sh ./ +ENV PYTHONPATH=/app/src +EXPOSE 8000 +CMD ["./docker-entrypoint.sh"] diff --git a/services/auth/alembic.ini b/services/auth/alembic.ini new file mode 100644 index 0000000..f1adb58 --- /dev/null +++ b/services/auth/alembic.ini @@ -0,0 +1,35 @@ +[alembic] +script_location = alembic +timezone = UTC + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = console +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stdout,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s %(name)s %(message)s diff --git a/services/auth/alembic/env.py b/services/auth/alembic/env.py new file mode 100644 index 0000000..df746af --- /dev/null +++ b/services/auth/alembic/env.py @@ -0,0 +1,55 @@ +from __future__ import annotations +import os +import sys +from logging.config import fileConfig +from alembic import context +from sqlalchemy import engine_from_config, pool + +# Add ./src to sys.path +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +SRC_DIR = os.path.join(BASE_DIR, "src") +if SRC_DIR not in sys.path: + sys.path.append(SRC_DIR) + +from app.db.session import Base # noqa + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata +DATABASE_URL = os.getenv("DATABASE_URL") + +def run_migrations_offline() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + connectable = engine_from_config( + {"sqlalchemy.url": url}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/auth/alembic/versions/.gitkeep b/services/auth/alembic/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/docker-entrypoint.sh b/services/auth/docker-entrypoint.sh new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/auth/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e +# Run migrations (no-op if no revisions yet) +alembic -c alembic.ini upgrade head || true +# Start app +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 diff --git a/services/auth/requirements.txt b/services/auth/requirements.txt new file mode 100644 index 0000000..87d1574 --- /dev/null +++ b/services/auth/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn[standard] +SQLAlchemy>=2.0 +psycopg2-binary +alembic +pydantic>=2 +pydantic-settings +python-dotenv +httpx>=0.27 +pytest diff --git a/services/auth/src/app/__init__.py b/services/auth/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/api/__init__.py b/services/auth/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/api/routes/__init__.py b/services/auth/src/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/api/routes/ping.py b/services/auth/src/app/api/routes/ping.py new file mode 100644 index 0000000..b96be25 --- /dev/null +++ b/services/auth/src/app/api/routes/ping.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/ping") +def ping(): + return {"ping": "pong"} diff --git a/services/auth/src/app/core/__init__.py b/services/auth/src/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/core/config.py b/services/auth/src/app/core/config.py new file mode 100644 index 0000000..3ba50ef --- /dev/null +++ b/services/auth/src/app/core/config.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_url: str | None = None + + class Config: + env_prefix = "" + env_file = ".env" + case_sensitive = False diff --git a/services/auth/src/app/db/__init__.py b/services/auth/src/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/db/session.py b/services/auth/src/app/db/session.py new file mode 100644 index 0000000..42680ed --- /dev/null +++ b/services/auth/src/app/db/session.py @@ -0,0 +1,26 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from typing import Generator + +DEFAULT_DB_URL = "postgresql+psycopg2://postgres:postgres@postgres:5432/auth_db" + +class Base(DeclarativeBase): + pass + +DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_DB_URL) + +engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + future=True, +) + +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + +def get_db() -> Generator: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/services/auth/src/app/main.py b/services/auth/src/app/main.py new file mode 100644 index 0000000..8d35f4d --- /dev/null +++ b/services/auth/src/app/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from .api.routes.ping import router as ping_router + +app = FastAPI(title="AUTH Service") + +@app.get("/health") +def health(): + return {"status": "ok", "service": "auth"} + +# v1 API +app.include_router(ping_router, prefix="/v1") diff --git a/services/auth/src/app/models/__init__.py b/services/auth/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/models/base.py b/services/auth/src/app/models/base.py new file mode 100644 index 0000000..f344a9c --- /dev/null +++ b/services/auth/src/app/models/base.py @@ -0,0 +1 @@ +from app.db.session import Base # re-export for convenience diff --git a/services/auth/src/app/repositories/__init__.py b/services/auth/src/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/repositories/base.py b/services/auth/src/app/repositories/base.py new file mode 100644 index 0000000..fa54611 --- /dev/null +++ b/services/auth/src/app/repositories/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseRepository: + def __init__(self, db: Session): + self.db = db diff --git a/services/auth/src/app/schemas/__init__.py b/services/auth/src/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/schemas/common.py b/services/auth/src/app/schemas/common.py new file mode 100644 index 0000000..faa7726 --- /dev/null +++ b/services/auth/src/app/schemas/common.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class Message(BaseModel): + message: str diff --git a/services/auth/src/app/services/__init__.py b/services/auth/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth/src/app/services/base.py b/services/auth/src/app/services/base.py new file mode 100644 index 0000000..d3b5ce0 --- /dev/null +++ b/services/auth/src/app/services/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseService: + def __init__(self, db: Session): + self.db = db diff --git a/services/auth/tests/test_health.py b/services/auth/tests/test_health.py new file mode 100644 index 0000000..f4b1549 --- /dev/null +++ b/services/auth/tests/test_health.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_health(): + r = client.get("/health") + assert r.status_code == 200 + data = r.json() + assert data.get("status") == "ok" diff --git a/services/chat/Dockerfile b/services/chat/Dockerfile new file mode 100644 index 0000000..57c86d2 --- /dev/null +++ b/services/chat/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +COPY src ./src +COPY alembic.ini ./ +COPY alembic ./alembic +COPY docker-entrypoint.sh ./ +ENV PYTHONPATH=/app/src +EXPOSE 8000 +CMD ["./docker-entrypoint.sh"] diff --git a/services/chat/alembic.ini b/services/chat/alembic.ini new file mode 100644 index 0000000..f1adb58 --- /dev/null +++ b/services/chat/alembic.ini @@ -0,0 +1,35 @@ +[alembic] +script_location = alembic +timezone = UTC + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = console +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stdout,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s %(name)s %(message)s diff --git a/services/chat/alembic/env.py b/services/chat/alembic/env.py new file mode 100644 index 0000000..df746af --- /dev/null +++ b/services/chat/alembic/env.py @@ -0,0 +1,55 @@ +from __future__ import annotations +import os +import sys +from logging.config import fileConfig +from alembic import context +from sqlalchemy import engine_from_config, pool + +# Add ./src to sys.path +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +SRC_DIR = os.path.join(BASE_DIR, "src") +if SRC_DIR not in sys.path: + sys.path.append(SRC_DIR) + +from app.db.session import Base # noqa + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata +DATABASE_URL = os.getenv("DATABASE_URL") + +def run_migrations_offline() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + connectable = engine_from_config( + {"sqlalchemy.url": url}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/chat/alembic/versions/.gitkeep b/services/chat/alembic/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/docker-entrypoint.sh b/services/chat/docker-entrypoint.sh new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/chat/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e +# Run migrations (no-op if no revisions yet) +alembic -c alembic.ini upgrade head || true +# Start app +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 diff --git a/services/chat/requirements.txt b/services/chat/requirements.txt new file mode 100644 index 0000000..87d1574 --- /dev/null +++ b/services/chat/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn[standard] +SQLAlchemy>=2.0 +psycopg2-binary +alembic +pydantic>=2 +pydantic-settings +python-dotenv +httpx>=0.27 +pytest diff --git a/services/chat/src/app/__init__.py b/services/chat/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/api/__init__.py b/services/chat/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/api/routes/__init__.py b/services/chat/src/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/api/routes/ping.py b/services/chat/src/app/api/routes/ping.py new file mode 100644 index 0000000..b96be25 --- /dev/null +++ b/services/chat/src/app/api/routes/ping.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/ping") +def ping(): + return {"ping": "pong"} diff --git a/services/chat/src/app/core/__init__.py b/services/chat/src/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/core/config.py b/services/chat/src/app/core/config.py new file mode 100644 index 0000000..3ba50ef --- /dev/null +++ b/services/chat/src/app/core/config.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_url: str | None = None + + class Config: + env_prefix = "" + env_file = ".env" + case_sensitive = False diff --git a/services/chat/src/app/db/__init__.py b/services/chat/src/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/db/session.py b/services/chat/src/app/db/session.py new file mode 100644 index 0000000..1e478f0 --- /dev/null +++ b/services/chat/src/app/db/session.py @@ -0,0 +1,26 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from typing import Generator + +DEFAULT_DB_URL = "postgresql+psycopg2://postgres:postgres@postgres:5432/chat_db" + +class Base(DeclarativeBase): + pass + +DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_DB_URL) + +engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + future=True, +) + +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + +def get_db() -> Generator: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/services/chat/src/app/main.py b/services/chat/src/app/main.py new file mode 100644 index 0000000..e570874 --- /dev/null +++ b/services/chat/src/app/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from .api.routes.ping import router as ping_router + +app = FastAPI(title="CHAT Service") + +@app.get("/health") +def health(): + return {"status": "ok", "service": "chat"} + +# v1 API +app.include_router(ping_router, prefix="/v1") diff --git a/services/chat/src/app/models/__init__.py b/services/chat/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/models/base.py b/services/chat/src/app/models/base.py new file mode 100644 index 0000000..f344a9c --- /dev/null +++ b/services/chat/src/app/models/base.py @@ -0,0 +1 @@ +from app.db.session import Base # re-export for convenience diff --git a/services/chat/src/app/repositories/__init__.py b/services/chat/src/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/repositories/base.py b/services/chat/src/app/repositories/base.py new file mode 100644 index 0000000..fa54611 --- /dev/null +++ b/services/chat/src/app/repositories/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseRepository: + def __init__(self, db: Session): + self.db = db diff --git a/services/chat/src/app/schemas/__init__.py b/services/chat/src/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/schemas/common.py b/services/chat/src/app/schemas/common.py new file mode 100644 index 0000000..faa7726 --- /dev/null +++ b/services/chat/src/app/schemas/common.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class Message(BaseModel): + message: str diff --git a/services/chat/src/app/services/__init__.py b/services/chat/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/chat/src/app/services/base.py b/services/chat/src/app/services/base.py new file mode 100644 index 0000000..d3b5ce0 --- /dev/null +++ b/services/chat/src/app/services/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseService: + def __init__(self, db: Session): + self.db = db diff --git a/services/chat/tests/test_health.py b/services/chat/tests/test_health.py new file mode 100644 index 0000000..f4b1549 --- /dev/null +++ b/services/chat/tests/test_health.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_health(): + r = client.get("/health") + assert r.status_code == 200 + data = r.json() + assert data.get("status") == "ok" diff --git a/services/match/Dockerfile b/services/match/Dockerfile new file mode 100644 index 0000000..57c86d2 --- /dev/null +++ b/services/match/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +COPY src ./src +COPY alembic.ini ./ +COPY alembic ./alembic +COPY docker-entrypoint.sh ./ +ENV PYTHONPATH=/app/src +EXPOSE 8000 +CMD ["./docker-entrypoint.sh"] diff --git a/services/match/alembic.ini b/services/match/alembic.ini new file mode 100644 index 0000000..f1adb58 --- /dev/null +++ b/services/match/alembic.ini @@ -0,0 +1,35 @@ +[alembic] +script_location = alembic +timezone = UTC + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = console +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stdout,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s %(name)s %(message)s diff --git a/services/match/alembic/env.py b/services/match/alembic/env.py new file mode 100644 index 0000000..df746af --- /dev/null +++ b/services/match/alembic/env.py @@ -0,0 +1,55 @@ +from __future__ import annotations +import os +import sys +from logging.config import fileConfig +from alembic import context +from sqlalchemy import engine_from_config, pool + +# Add ./src to sys.path +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +SRC_DIR = os.path.join(BASE_DIR, "src") +if SRC_DIR not in sys.path: + sys.path.append(SRC_DIR) + +from app.db.session import Base # noqa + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata +DATABASE_URL = os.getenv("DATABASE_URL") + +def run_migrations_offline() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + connectable = engine_from_config( + {"sqlalchemy.url": url}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/match/alembic/versions/.gitkeep b/services/match/alembic/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/match/docker-entrypoint.sh b/services/match/docker-entrypoint.sh new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/match/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e +# Run migrations (no-op if no revisions yet) +alembic -c alembic.ini upgrade head || true +# Start app +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 diff --git a/services/match/requirements.txt b/services/match/requirements.txt new file mode 100644 index 0000000..87d1574 --- /dev/null +++ b/services/match/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn[standard] +SQLAlchemy>=2.0 +psycopg2-binary +alembic +pydantic>=2 +pydantic-settings +python-dotenv +httpx>=0.27 +pytest diff --git a/services/match/src/app/__init__.py b/services/match/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/api/__init__.py b/services/match/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/api/routes/__init__.py b/services/match/src/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/api/routes/ping.py b/services/match/src/app/api/routes/ping.py new file mode 100644 index 0000000..b96be25 --- /dev/null +++ b/services/match/src/app/api/routes/ping.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/ping") +def ping(): + return {"ping": "pong"} diff --git a/services/match/src/app/core/__init__.py b/services/match/src/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/core/config.py b/services/match/src/app/core/config.py new file mode 100644 index 0000000..3ba50ef --- /dev/null +++ b/services/match/src/app/core/config.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_url: str | None = None + + class Config: + env_prefix = "" + env_file = ".env" + case_sensitive = False diff --git a/services/match/src/app/db/__init__.py b/services/match/src/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/db/session.py b/services/match/src/app/db/session.py new file mode 100644 index 0000000..786cab8 --- /dev/null +++ b/services/match/src/app/db/session.py @@ -0,0 +1,26 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from typing import Generator + +DEFAULT_DB_URL = "postgresql+psycopg2://postgres:postgres@postgres:5432/match_db" + +class Base(DeclarativeBase): + pass + +DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_DB_URL) + +engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + future=True, +) + +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + +def get_db() -> Generator: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/services/match/src/app/main.py b/services/match/src/app/main.py new file mode 100644 index 0000000..b185770 --- /dev/null +++ b/services/match/src/app/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from .api.routes.ping import router as ping_router + +app = FastAPI(title="MATCH Service") + +@app.get("/health") +def health(): + return {"status": "ok", "service": "match"} + +# v1 API +app.include_router(ping_router, prefix="/v1") diff --git a/services/match/src/app/models/__init__.py b/services/match/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/models/base.py b/services/match/src/app/models/base.py new file mode 100644 index 0000000..f344a9c --- /dev/null +++ b/services/match/src/app/models/base.py @@ -0,0 +1 @@ +from app.db.session import Base # re-export for convenience diff --git a/services/match/src/app/repositories/__init__.py b/services/match/src/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/repositories/base.py b/services/match/src/app/repositories/base.py new file mode 100644 index 0000000..fa54611 --- /dev/null +++ b/services/match/src/app/repositories/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseRepository: + def __init__(self, db: Session): + self.db = db diff --git a/services/match/src/app/schemas/__init__.py b/services/match/src/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/schemas/common.py b/services/match/src/app/schemas/common.py new file mode 100644 index 0000000..faa7726 --- /dev/null +++ b/services/match/src/app/schemas/common.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class Message(BaseModel): + message: str diff --git a/services/match/src/app/services/__init__.py b/services/match/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/match/src/app/services/base.py b/services/match/src/app/services/base.py new file mode 100644 index 0000000..d3b5ce0 --- /dev/null +++ b/services/match/src/app/services/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseService: + def __init__(self, db: Session): + self.db = db diff --git a/services/match/tests/test_health.py b/services/match/tests/test_health.py new file mode 100644 index 0000000..f4b1549 --- /dev/null +++ b/services/match/tests/test_health.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_health(): + r = client.get("/health") + assert r.status_code == 200 + data = r.json() + assert data.get("status") == "ok" diff --git a/services/payments/Dockerfile b/services/payments/Dockerfile new file mode 100644 index 0000000..57c86d2 --- /dev/null +++ b/services/payments/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +COPY src ./src +COPY alembic.ini ./ +COPY alembic ./alembic +COPY docker-entrypoint.sh ./ +ENV PYTHONPATH=/app/src +EXPOSE 8000 +CMD ["./docker-entrypoint.sh"] diff --git a/services/payments/alembic.ini b/services/payments/alembic.ini new file mode 100644 index 0000000..f1adb58 --- /dev/null +++ b/services/payments/alembic.ini @@ -0,0 +1,35 @@ +[alembic] +script_location = alembic +timezone = UTC + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = console +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stdout,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s %(name)s %(message)s diff --git a/services/payments/alembic/env.py b/services/payments/alembic/env.py new file mode 100644 index 0000000..df746af --- /dev/null +++ b/services/payments/alembic/env.py @@ -0,0 +1,55 @@ +from __future__ import annotations +import os +import sys +from logging.config import fileConfig +from alembic import context +from sqlalchemy import engine_from_config, pool + +# Add ./src to sys.path +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +SRC_DIR = os.path.join(BASE_DIR, "src") +if SRC_DIR not in sys.path: + sys.path.append(SRC_DIR) + +from app.db.session import Base # noqa + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata +DATABASE_URL = os.getenv("DATABASE_URL") + +def run_migrations_offline() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + connectable = engine_from_config( + {"sqlalchemy.url": url}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/payments/alembic/versions/.gitkeep b/services/payments/alembic/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/docker-entrypoint.sh b/services/payments/docker-entrypoint.sh new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/payments/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e +# Run migrations (no-op if no revisions yet) +alembic -c alembic.ini upgrade head || true +# Start app +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 diff --git a/services/payments/requirements.txt b/services/payments/requirements.txt new file mode 100644 index 0000000..87d1574 --- /dev/null +++ b/services/payments/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn[standard] +SQLAlchemy>=2.0 +psycopg2-binary +alembic +pydantic>=2 +pydantic-settings +python-dotenv +httpx>=0.27 +pytest diff --git a/services/payments/src/app/__init__.py b/services/payments/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/api/__init__.py b/services/payments/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/api/routes/__init__.py b/services/payments/src/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/api/routes/ping.py b/services/payments/src/app/api/routes/ping.py new file mode 100644 index 0000000..b96be25 --- /dev/null +++ b/services/payments/src/app/api/routes/ping.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/ping") +def ping(): + return {"ping": "pong"} diff --git a/services/payments/src/app/core/__init__.py b/services/payments/src/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/core/config.py b/services/payments/src/app/core/config.py new file mode 100644 index 0000000..3ba50ef --- /dev/null +++ b/services/payments/src/app/core/config.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_url: str | None = None + + class Config: + env_prefix = "" + env_file = ".env" + case_sensitive = False diff --git a/services/payments/src/app/db/__init__.py b/services/payments/src/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/db/session.py b/services/payments/src/app/db/session.py new file mode 100644 index 0000000..9e3e2f1 --- /dev/null +++ b/services/payments/src/app/db/session.py @@ -0,0 +1,26 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from typing import Generator + +DEFAULT_DB_URL = "postgresql+psycopg2://postgres:postgres@postgres:5432/payments_db" + +class Base(DeclarativeBase): + pass + +DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_DB_URL) + +engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + future=True, +) + +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + +def get_db() -> Generator: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/services/payments/src/app/main.py b/services/payments/src/app/main.py new file mode 100644 index 0000000..58c9f4f --- /dev/null +++ b/services/payments/src/app/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from .api.routes.ping import router as ping_router + +app = FastAPI(title="PAYMENTS Service") + +@app.get("/health") +def health(): + return {"status": "ok", "service": "payments"} + +# v1 API +app.include_router(ping_router, prefix="/v1") diff --git a/services/payments/src/app/models/__init__.py b/services/payments/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/models/base.py b/services/payments/src/app/models/base.py new file mode 100644 index 0000000..f344a9c --- /dev/null +++ b/services/payments/src/app/models/base.py @@ -0,0 +1 @@ +from app.db.session import Base # re-export for convenience diff --git a/services/payments/src/app/repositories/__init__.py b/services/payments/src/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/repositories/base.py b/services/payments/src/app/repositories/base.py new file mode 100644 index 0000000..fa54611 --- /dev/null +++ b/services/payments/src/app/repositories/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseRepository: + def __init__(self, db: Session): + self.db = db diff --git a/services/payments/src/app/schemas/__init__.py b/services/payments/src/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/schemas/common.py b/services/payments/src/app/schemas/common.py new file mode 100644 index 0000000..faa7726 --- /dev/null +++ b/services/payments/src/app/schemas/common.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class Message(BaseModel): + message: str diff --git a/services/payments/src/app/services/__init__.py b/services/payments/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/payments/src/app/services/base.py b/services/payments/src/app/services/base.py new file mode 100644 index 0000000..d3b5ce0 --- /dev/null +++ b/services/payments/src/app/services/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseService: + def __init__(self, db: Session): + self.db = db diff --git a/services/payments/tests/test_health.py b/services/payments/tests/test_health.py new file mode 100644 index 0000000..f4b1549 --- /dev/null +++ b/services/payments/tests/test_health.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_health(): + r = client.get("/health") + assert r.status_code == 200 + data = r.json() + assert data.get("status") == "ok" diff --git a/services/profiles/Dockerfile b/services/profiles/Dockerfile new file mode 100644 index 0000000..57c86d2 --- /dev/null +++ b/services/profiles/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +COPY src ./src +COPY alembic.ini ./ +COPY alembic ./alembic +COPY docker-entrypoint.sh ./ +ENV PYTHONPATH=/app/src +EXPOSE 8000 +CMD ["./docker-entrypoint.sh"] diff --git a/services/profiles/alembic.ini b/services/profiles/alembic.ini new file mode 100644 index 0000000..f1adb58 --- /dev/null +++ b/services/profiles/alembic.ini @@ -0,0 +1,35 @@ +[alembic] +script_location = alembic +timezone = UTC + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = console +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stdout,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s %(name)s %(message)s diff --git a/services/profiles/alembic/env.py b/services/profiles/alembic/env.py new file mode 100644 index 0000000..df746af --- /dev/null +++ b/services/profiles/alembic/env.py @@ -0,0 +1,55 @@ +from __future__ import annotations +import os +import sys +from logging.config import fileConfig +from alembic import context +from sqlalchemy import engine_from_config, pool + +# Add ./src to sys.path +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +SRC_DIR = os.path.join(BASE_DIR, "src") +if SRC_DIR not in sys.path: + sys.path.append(SRC_DIR) + +from app.db.session import Base # noqa + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata +DATABASE_URL = os.getenv("DATABASE_URL") + +def run_migrations_offline() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + url = DATABASE_URL + if not url: + raise RuntimeError("DATABASE_URL is not set") + connectable = engine_from_config( + {"sqlalchemy.url": url}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/profiles/alembic/versions/.gitkeep b/services/profiles/alembic/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/docker-entrypoint.sh b/services/profiles/docker-entrypoint.sh new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/profiles/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e +# Run migrations (no-op if no revisions yet) +alembic -c alembic.ini upgrade head || true +# Start app +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 diff --git a/services/profiles/requirements.txt b/services/profiles/requirements.txt new file mode 100644 index 0000000..87d1574 --- /dev/null +++ b/services/profiles/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn[standard] +SQLAlchemy>=2.0 +psycopg2-binary +alembic +pydantic>=2 +pydantic-settings +python-dotenv +httpx>=0.27 +pytest diff --git a/services/profiles/src/app/__init__.py b/services/profiles/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/api/__init__.py b/services/profiles/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/api/routes/__init__.py b/services/profiles/src/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/api/routes/ping.py b/services/profiles/src/app/api/routes/ping.py new file mode 100644 index 0000000..b96be25 --- /dev/null +++ b/services/profiles/src/app/api/routes/ping.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/ping") +def ping(): + return {"ping": "pong"} diff --git a/services/profiles/src/app/core/__init__.py b/services/profiles/src/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/core/config.py b/services/profiles/src/app/core/config.py new file mode 100644 index 0000000..3ba50ef --- /dev/null +++ b/services/profiles/src/app/core/config.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_url: str | None = None + + class Config: + env_prefix = "" + env_file = ".env" + case_sensitive = False diff --git a/services/profiles/src/app/db/__init__.py b/services/profiles/src/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/db/session.py b/services/profiles/src/app/db/session.py new file mode 100644 index 0000000..bae2d34 --- /dev/null +++ b/services/profiles/src/app/db/session.py @@ -0,0 +1,26 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from typing import Generator + +DEFAULT_DB_URL = "postgresql+psycopg2://postgres:postgres@postgres:5432/profiles_db" + +class Base(DeclarativeBase): + pass + +DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_DB_URL) + +engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + future=True, +) + +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + +def get_db() -> Generator: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/services/profiles/src/app/main.py b/services/profiles/src/app/main.py new file mode 100644 index 0000000..c4b5a7f --- /dev/null +++ b/services/profiles/src/app/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from .api.routes.ping import router as ping_router + +app = FastAPI(title="PROFILES Service") + +@app.get("/health") +def health(): + return {"status": "ok", "service": "profiles"} + +# v1 API +app.include_router(ping_router, prefix="/v1") diff --git a/services/profiles/src/app/models/__init__.py b/services/profiles/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/models/base.py b/services/profiles/src/app/models/base.py new file mode 100644 index 0000000..f344a9c --- /dev/null +++ b/services/profiles/src/app/models/base.py @@ -0,0 +1 @@ +from app.db.session import Base # re-export for convenience diff --git a/services/profiles/src/app/repositories/__init__.py b/services/profiles/src/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/repositories/base.py b/services/profiles/src/app/repositories/base.py new file mode 100644 index 0000000..fa54611 --- /dev/null +++ b/services/profiles/src/app/repositories/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseRepository: + def __init__(self, db: Session): + self.db = db diff --git a/services/profiles/src/app/schemas/__init__.py b/services/profiles/src/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/schemas/common.py b/services/profiles/src/app/schemas/common.py new file mode 100644 index 0000000..faa7726 --- /dev/null +++ b/services/profiles/src/app/schemas/common.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class Message(BaseModel): + message: str diff --git a/services/profiles/src/app/services/__init__.py b/services/profiles/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/profiles/src/app/services/base.py b/services/profiles/src/app/services/base.py new file mode 100644 index 0000000..d3b5ce0 --- /dev/null +++ b/services/profiles/src/app/services/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session + +class BaseService: + def __init__(self, db: Session): + self.db = db diff --git a/services/profiles/tests/test_health.py b/services/profiles/tests/test_health.py new file mode 100644 index 0000000..f4b1549 --- /dev/null +++ b/services/profiles/tests/test_health.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_health(): + r = client.get("/health") + assert r.status_code == 200 + data = r.json() + assert data.get("status") == "ok"