From 1a980a7a707412f1cfeadacd7d39abb37886822c Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Tue, 30 Dec 2025 16:15:46 +0530 Subject: [PATCH] init db and docker --- .gitignore | 25 +++++++++++++++++++ Dockerfile | 18 ++++++++++++++ db/init.sql | 43 ++++++++++++++++++++++++++++++++ docker-compose.yml | 62 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 30 ++++++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 db/init.sql create mode 100644 docker-compose.yml create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df0af1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +.venv/ +venv/ +*.egg-info/ + +# Environment +.env +.env.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Docker +docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd6ce3e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install uv for fast package management +RUN pip install uv + +# Copy dependency files +COPY pyproject.toml . + +# Install dependencies +RUN uv pip install --system -e . + +# Copy application code +COPY app/ app/ + +# Run with uvicorn +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 0000000..e92a465 --- /dev/null +++ b/db/init.sql @@ -0,0 +1,43 @@ +-- URL Shortener Database Schema + +CREATE TABLE IF NOT EXISTS urls ( + id BIGSERIAL PRIMARY KEY, + short_code VARCHAR(10) UNIQUE NOT NULL, + original_url TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + expires_at TIMESTAMP WITH TIME ZONE, + click_count BIGINT DEFAULT 0, + + -- Metadata + user_agent TEXT, + ip_address INET +); + +-- Index for fast lookups by short_code (most common query) +CREATE INDEX idx_urls_short_code ON urls(short_code); + +-- Index for cleanup of expired URLs +CREATE INDEX idx_urls_expires_at ON urls(expires_at) WHERE expires_at IS NOT NULL; + +-- Analytics table (for click tracking) +CREATE TABLE IF NOT EXISTS clicks ( + id BIGSERIAL PRIMARY KEY, + short_code VARCHAR(10) NOT NULL, + clicked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + ip_address INET, + user_agent TEXT, + referer TEXT, + country_code VARCHAR(2) +); + +-- Partition-friendly index (clicks will be high volume) +CREATE INDEX idx_clicks_short_code ON clicks(short_code); +CREATE INDEX idx_clicks_clicked_at ON clicks(clicked_at); + +-- Function to increment click count (atomic) +CREATE OR REPLACE FUNCTION increment_click_count(code VARCHAR(10)) +RETURNS VOID AS $$ +BEGIN + UPDATE urls SET click_count = click_count + 1 WHERE short_code = code; +END; +$$ LANGUAGE plpgsql; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e50a6e1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,62 @@ +services: + # PostgreSQL - Primary database + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: urlshortner + POSTGRES_PASSWORD: localdev + POSTGRES_DB: urlshortner + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U urlshortner"] + interval: 5s + timeout: 5s + retries: 5 + + # Redis - Caching layer + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + # API Server (can scale with --scale api=3) + api: + build: + context: . + dockerfile: Dockerfile + environment: + DATABASE_URL: postgresql://urlshortner:localdev@postgres:5432/urlshortner + REDIS_URL: redis://redis:6379 + MACHINE_ID: 1 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "8000" # Random port mapping for scaling + + # Nginx - Load balancer + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - api + +volumes: + postgres_data: + redis_data: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..41ff3aa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "url-shortner" +version = "0.1.0" +description = "Distributed URL shortening service for learning system design" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.109.0", + "uvicorn[standard]>=0.27.0", + "asyncpg>=0.29.0", + "redis>=5.0.0", + "pydantic>=2.5.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "httpx>=0.26.0", + "ruff>=0.1.0", +] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] + +[tool.pytest.ini_options] +asyncio_mode = "auto"