diff --git a/API.md b/API.md new file mode 100644 index 0000000..1cab330 --- /dev/null +++ b/API.md @@ -0,0 +1,198 @@ +# Inventory Management API + +This is a CRUD application for tracking server states in a cloud service environment. + +## Requirements + +- Docker +- Docker Compose + +## Running the Application + +1. Clone the repository. +2. Navigate to the project directory. +3. Build and run the Docker containers: + +ash + docker-compose up --build + +4. The API will be accessible at `http://localhost:8000`. +- Swagger UI: `http://localhost:8000/docs` + +## API Endpoints + +- `POST /servers`: Create a server +- `GET /servers`: List all servers +- `GET /servers/{id}`: Get one server +- `PUT /servers/{id}`: Update server +- `DELETE /servers/{id}`: Delete server + +## Project Structure + +``` +devops_inventory/ +├── app/ +│ ├── main.py # FastAPI application +│ └── database.py # Database connection and initialization +│ └── test_main.py # Pytest test suite +├── cli.py # Command-line interface +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +├── README.md +├── API.md +└── CLI.md +``` + +## API Endpoints + +### Create Server +```http +POST /servers +Content-Type: application/json + +{ + "hostname": "web-server-01", + "ip_address": "192.168.1.100", + "state": "active" +} +``` + +**Response:** `201 Created` +```json +{ + "id": 1, + "hostname": "web-server-01", + "ip_address": "192.168.1.100", + "state": "active" +} +``` + +### List All Servers +```http +GET /servers +``` + +**Response:** `200 OK` +```json +[ + { + "id": 1, + "hostname": "web-server-01", + "ip_address": "192.168.1.100", + "state": "active" + } +] +``` + +### Get Single Server +```http +GET /servers/{id} +``` + +**Response:** `200 OK` or `404 Not Found` + +### Update Server +```http +PUT /servers/{id} +Content-Type: application/json + +{ + "hostname": "web-server-02", + "state": "offline" +} +``` + +**Response:** `200 OK` or `404 Not Found` + +### Delete Server +```http +DELETE /servers/{id} +``` + +**Response:** `200 OK` or `404 Not Found` + +## Validation Rules + +### Hostname +- Must be unique across all servers +- Maximum length: 255 characters + +### IP Address +- Must be a valid IPv4 address format +- Pattern: `0-255.0-255.0-255.0-255` +- Examples: `192.168.1.1`, `10.0.0.1`, `172.16.0.1` + +### State +- Must be one of: `active`, `offline`, `retired` + +## Development + +### Stop the application +```bash +docker-compose down +``` + +### Stop and remove volumes (clean database) +```bash +docker-compose down -v +``` + +### View logs +```bash +docker-compose logs -f web +``` + +### Rebuild after code changes +```bash +docker-compose up --build +``` + +## Database + +The application uses PostgreSQL with raw SQL queries as required. The database schema: + +```sql +CREATE TABLE servers ( + id SERIAL PRIMARY KEY, + hostname VARCHAR(255) UNIQUE NOT NULL, + ip_address VARCHAR(15) NOT NULL, + state VARCHAR(20) NOT NULL CHECK (state IN ('active', 'offline', 'retired')) +); +``` + +## Error Handling + +The API returns appropriate HTTP status codes: + +- `200 OK` - Successful GET, PUT, DELETE +- `201 Created` - Successful POST +- `400 Bad Request` - Validation errors (duplicate hostname, invalid data) +- `404 Not Found` - Resource not found +- `422 Unprocessable Entity` - Invalid request format +- `500 Internal Server Error` - Server errors + +## Example Usage + +### Using curl + +```bash +# Create a server +curl -X POST http://localhost:8000/servers \ + -H "Content-Type: application/json" \ + -d '{"hostname":"web-01","ip_address":"192.168.1.100","state":"active"}' + +# List all servers +curl http://localhost:8000/servers + +# Get a specific server +curl http://localhost:8000/servers/1 + +# Update a server +curl -X PUT http://localhost:8000/servers/1 \ + -H "Content-Type: application/json" \ + -d '{"state":"offline"}' + +# Delete a server +curl -X DELETE http://localhost:8000/servers/1 +``` diff --git a/CLI.md b/CLI.md new file mode 100644 index 0000000..7ba6431 --- /dev/null +++ b/CLI.md @@ -0,0 +1,48 @@ +## CLI Usage + +The CLI provides an easy way to interact with the API from the command line. + +### Install CLI dependencies (if running locally) + +```bash +pip install -r requirements.txt +``` + +### CLI Commands + +**Create a server:** +```bash +python cli.py create --hostname web-01 --ip 192.168.1.100 --state active +``` + +**List all servers:** +```bash +python cli.py list +``` + +**Get a specific server:** +```bash +python cli.py get 1 +``` + +**Update a server:** +```bash +python cli.py update 1 --hostname web-02 --state offline +``` + +**Delete a server:** +```bash +python cli.py delete 1 +``` + +### Using CLI inside Docker + +```bash +docker-compose exec web python cli.py list +``` + +## Running Tests + +```bash +docker-compose exec web -q +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a275acf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY ./app ./app +COPY cli.py . + + + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..fb0630a --- /dev/null +++ b/app/database.py @@ -0,0 +1,35 @@ +import psycopg2 +import os + +def get_db_connection(): + """Create and return a database connection""" + return psycopg2.connect( + host=os.getenv("DB_HOST", "db"), + database=os.getenv("DB_NAME", "inventory"), + user=os.getenv("DB_USER", "postgres"), + password=os.getenv("DB_PASSWORD", "postgres"), + port=os.getenv("DB_PORT", "5432") + ) + +def init_db(): + """Initialize the database with the servers table""" + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS servers ( + id SERIAL PRIMARY KEY, + hostname VARCHAR(255) UNIQUE NOT NULL, + ip_address VARCHAR(15) NOT NULL, + state VARCHAR(20) NOT NULL CHECK (state IN ('active', 'offline', 'retired')) + ) + """) + conn.commit() + print("Database initialized successfully") + except Exception as e: + print(f"Error initializing database: {e}") + conn.rollback() + finally: + cursor.close() + conn.close() \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..cf11d34 --- /dev/null +++ b/app/main.py @@ -0,0 +1,195 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field, field_validator +from typing import List +import re +from .database import get_db_connection, init_db + +app = FastAPI() + +# Initialize database on startup +@app.on_event("startup") +def startup(): + init_db() + +class ServerCreate(BaseModel): + hostname: str = Field(..., max_length=255) + ip_address: str + state: str + + @field_validator('ip_address') + def validate_ip(cls, v): + ip_pattern = r'^(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?))$' + if not re.match(ip_pattern, v): + raise ValueError('Invalid IP address format') + return v + + @field_validator('state') + def validate_state(cls, v): + if v not in ['active', 'offline', 'retired']: + raise ValueError('State must be one of: active, offline, retired') + return v + +class ServerUpdate(BaseModel): + hostname: str | None = Field(None, max_length=255) + ip_address: str | None = None + state: str | None = None + + @field_validator('ip_address') + def validate_ip(cls, v): + if v is not None: + ip_pattern = r'^(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?))$' + if not re.match(ip_pattern, v): + raise ValueError('Invalid IP address format') + return v + + @field_validator('state') + def validate_state(cls, v): + if v is not None and v not in ['active', 'offline', 'retired']: + raise ValueError('State must be one of: active, offline, retired') + return v + +class Server(BaseModel): + id: int + hostname: str + ip_address: str + state: str + +@app.post("/servers", response_model=Server, status_code=201) +def create_server(server: ServerCreate): + conn = get_db_connection() + cursor = conn.cursor() + + try: + # Check if hostname already exists + cursor.execute("SELECT id FROM servers WHERE hostname = %s", (server.hostname,)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Hostname already exists") + + # Insert new server + cursor.execute( + "INSERT INTO servers (hostname, ip_address, state) VALUES (%s, %s, %s) RETURNING id", + (server.hostname, server.ip_address, server.state) + ) + server_id = cursor.fetchone()[0] + conn.commit() + + # Fetch the created server + cursor.execute("SELECT id, hostname, ip_address, state FROM servers WHERE id = %s", (server_id,)) + row = cursor.fetchone() + + return Server(id=row[0], hostname=row[1], ip_address=row[2], state=row[3]) + + except Exception as e: + conn.rollback() + if "unique constraint" in str(e).lower(): + raise HTTPException(status_code=400, detail="Hostname already exists") + raise HTTPException(status_code=500, detail=str(e)) + finally: + cursor.close() + conn.close() + +@app.get("/servers", response_model=List[Server]) +def list_servers(): + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute("SELECT id, hostname, ip_address, state FROM servers ORDER BY id") + rows = cursor.fetchall() + + return [Server(id=row[0], hostname=row[1], ip_address=row[2], state=row[3]) for row in rows] + finally: + cursor.close() + conn.close() + +@app.get("/servers/{server_id}", response_model=Server) +def get_server(server_id: int): + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute("SELECT id, hostname, ip_address, state FROM servers WHERE id = %s", (server_id,)) + row = cursor.fetchone() + + if not row: + raise HTTPException(status_code=404, detail="Server not found") + + return Server(id=row[0], hostname=row[1], ip_address=row[2], state=row[3]) + finally: + cursor.close() + conn.close() + +@app.put("/servers/{server_id}", response_model=Server) +def update_server(server_id: int, server: ServerUpdate): + conn = get_db_connection() + cursor = conn.cursor() + + try: + # Check if server exists + cursor.execute("SELECT id FROM servers WHERE id = %s", (server_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Server not found") + + # Check if new hostname already exists (if hostname is being updated) + if server.hostname: + cursor.execute("SELECT id FROM servers WHERE hostname = %s AND id != %s", (server.hostname, server_id)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Hostname already exists") + + # Build update query dynamically + update_fields = [] + values = [] + + if server.hostname is not None: + update_fields.append("hostname = %s") + values.append(server.hostname) + if server.ip_address is not None: + update_fields.append("ip_address = %s") + values.append(server.ip_address) + if server.state is not None: + update_fields.append("state = %s") + values.append(server.state) + + if not update_fields: + raise HTTPException(status_code=400, detail="No fields to update") + + values.append(server_id) + query = f"UPDATE servers SET {', '.join(update_fields)} WHERE id = %s" + + cursor.execute(query, values) + conn.commit() + + # Fetch updated server + cursor.execute("SELECT id, hostname, ip_address, state FROM servers WHERE id = %s", (server_id,)) + row = cursor.fetchone() + + return Server(id=row[0], hostname=row[1], ip_address=row[2], state=row[3]) + + except HTTPException: + conn.rollback() + raise + except Exception as e: + conn.rollback() + raise HTTPException(status_code=500, detail=str(e)) + finally: + cursor.close() + conn.close() + +@app.delete("/servers/{server_id}") +def delete_server(server_id: int): + conn = get_db_connection() + cursor = conn.cursor() + + try: + # Check if server exists + cursor.execute("SELECT id FROM servers WHERE id = %s", (server_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Server not found") + + cursor.execute("DELETE FROM servers WHERE id = %s", (server_id,)) + conn.commit() + + return {"message": "Server deleted successfully"} + finally: + cursor.close() + conn.close() \ No newline at end of file diff --git a/app/test_main.py b/app/test_main.py new file mode 100644 index 0000000..0587c97 --- /dev/null +++ b/app/test_main.py @@ -0,0 +1,261 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app +from app.database import get_db_connection, init_db + +client = TestClient(app) + +@pytest.fixture(scope="module", autouse=True) +def setup_database(): + """Setup test database""" + init_db() + # Clean up any existing test data + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM servers") + conn.commit() + cursor.close() + conn.close() + yield + # Cleanup after tests + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM servers") + conn.commit() + cursor.close() + conn.close() + +@pytest.fixture(autouse=True) +def cleanup_between_tests(): + """Clean database between tests""" + yield + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM servers") + conn.commit() + cursor.close() + conn.close() + +def test_create_server(): + """Test creating a new server""" + response = client.post( + "/servers", + json={ + "hostname": "web-server-01", + "ip_address": "192.168.1.100", + "state": "active" + } + ) + assert response.status_code == 201 + data = response.json() + assert data["hostname"] == "web-server-01" + assert data["ip_address"] == "192.168.1.100" + assert data["state"] == "active" + assert "id" in data + +def test_create_server_duplicate_hostname(): + """Test creating a server with duplicate hostname""" + # Create first server + client.post( + "/servers", + json={ + "hostname": "duplicate-host", + "ip_address": "192.168.1.101", + "state": "active" + } + ) + + # Try to create with same hostname + response = client.post( + "/servers", + json={ + "hostname": "duplicate-host", + "ip_address": "192.168.1.102", + "state": "active" + } + ) + assert response.status_code == 400 + assert "hostname already exists" in response.json()["detail"].lower() + +def test_create_server_invalid_ip(): + """Test creating a server with invalid IP""" + response = client.post( + "/servers", + json={ + "hostname": "test-server", + "ip_address": "999.999.999.999", + "state": "active" + } + ) + assert response.status_code == 422 + +def test_create_server_invalid_state(): + """Test creating a server with invalid state""" + response = client.post( + "/servers", + json={ + "hostname": "test-server", + "ip_address": "192.168.1.100", + "state": "invalid_state" + } + ) + assert response.status_code == 422 + +def test_list_servers(): + """Test listing all servers""" + # Create test servers + servers_data = [ + {"hostname": "server-01", "ip_address": "192.168.1.1", "state": "active"}, + {"hostname": "server-02", "ip_address": "192.168.1.2", "state": "offline"}, + {"hostname": "server-03", "ip_address": "192.168.1.3", "state": "retired"} + ] + + for server in servers_data: + client.post("/servers", json=server) + + response = client.get("/servers") + assert response.status_code == 200 + data = response.json() + assert len(data) == 3 + assert all(key in data[0] for key in ["id", "hostname", "ip_address", "state"]) + +def test_get_server(): + """Test getting a specific server""" + # Create a server + create_response = client.post( + "/servers", + json={ + "hostname": "get-test-server", + "ip_address": "192.168.1.50", + "state": "active" + } + ) + server_id = create_response.json()["id"] + + # Get the server + response = client.get(f"/servers/{server_id}") + assert response.status_code == 200 + data = response.json() + assert data["id"] == server_id + assert data["hostname"] == "get-test-server" + +def test_get_server_not_found(): + """Test getting a non-existent server""" + response = client.get("/servers/99999") + assert response.status_code == 404 + assert "not found" in response.json()["detail"].lower() + +def test_update_server(): + """Test updating a server""" + # Create a server + create_response = client.post( + "/servers", + json={ + "hostname": "update-test", + "ip_address": "192.168.1.60", + "state": "active" + } + ) + server_id = create_response.json()["id"] + + # Update the server + response = client.put( + f"/servers/{server_id}", + json={ + "hostname": "updated-hostname", + "state": "offline" + } + ) + assert response.status_code == 200 + data = response.json() + assert data["hostname"] == "updated-hostname" + assert data["state"] == "offline" + assert data["ip_address"] == "192.168.1.60" # Should remain unchanged + +def test_update_server_not_found(): + """Test updating a non-existent server""" + response = client.put( + "/servers/99999", + json={"state": "offline"} + ) + assert response.status_code == 404 + +def test_update_server_duplicate_hostname(): + """Test updating server with existing hostname""" + # Create two servers + client.post( + "/servers", + json={"hostname": "server-a", "ip_address": "192.168.1.70", "state": "active"} + ) + create_response = client.post( + "/servers", + json={"hostname": "server-b", "ip_address": "192.168.1.71", "state": "active"} + ) + server_id = create_response.json()["id"] + + # Try to update server-b to use server-a's hostname + response = client.put( + f"/servers/{server_id}", + json={"hostname": "server-a"} + ) + assert response.status_code == 400 + +def test_delete_server(): + """Test deleting a server""" + # Create a server + create_response = client.post( + "/servers", + json={ + "hostname": "delete-test", + "ip_address": "192.168.1.80", + "state": "active" + } + ) + server_id = create_response.json()["id"] + + # Delete the server + response = client.delete(f"/servers/{server_id}") + assert response.status_code == 200 + assert "deleted successfully" in response.json()["message"].lower() + + # Verify it's deleted + get_response = client.get(f"/servers/{server_id}") + assert get_response.status_code == 404 + +def test_delete_server_not_found(): + """Test deleting a non-existent server""" + response = client.delete("/servers/99999") + assert response.status_code == 404 + +def test_list_servers_empty(): + """Test listing servers when database is empty""" + response = client.get("/servers") + assert response.status_code == 200 + assert response.json() == [] + +def test_ip_validation(): + """Test various IP address validations""" + valid_ips = ["192.168.1.1", "10.0.0.1", "255.255.255.255", "0.0.0.0"] + invalid_ips = ["256.1.1.1", "192.168.1", "192.168.1.1.1", "abc.def.ghi.jkl"] + + for i, ip in enumerate(valid_ips): + response = client.post( + "/servers", + json={"hostname": f"valid-ip-test-{i}", "ip_address": ip, "state": "active"} + ) + assert response.status_code == 201, f"Valid IP {ip} was rejected" + + # Clean up for invalid tests + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM servers") + conn.commit() + cursor.close() + conn.close() + + for ip in invalid_ips: + response = client.post( + "/servers", + json={"hostname": f"invalid-ip-test-{ip}", "ip_address": ip, "state": "active"} + ) + assert response.status_code == 422, f"Invalid IP {ip} was accepted" \ No newline at end of file diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..a2511c2 --- /dev/null +++ b/cli.py @@ -0,0 +1,131 @@ +import click +import requests +import json +from tabulate import tabulate + +API_URL = "http://localhost:8000" + +@click.group() +def cli(): + """Server Inventory Management CLI""" + pass + +@cli.command() +@click.option('--hostname', required=True, help='Server hostname') +@click.option('--ip', required=True, help='Server IP address') +@click.option('--state', required=True, type=click.Choice(['active', 'offline', 'retired']), help='Server state') +def create(hostname, ip, state): + """Create a new server""" + try: + response = requests.post( + f"{API_URL}/servers", + json={"hostname": hostname, "ip_address": ip, "state": state} + ) + response.raise_for_status() + server = response.json() + click.echo(click.style(f"✓ Server created successfully!", fg='green')) + click.echo(f"ID: {server['id']}") + click.echo(f"Hostname: {server['hostname']}") + click.echo(f"IP: {server['ip_address']}") + click.echo(f"State: {server['state']}") + except requests.exceptions.HTTPError as e: + click.echo(click.style(f"✗ Error: {e.response.json().get('detail', str(e))}", fg='red')) + except Exception as e: + click.echo(click.style(f"✗ Error: {str(e)}", fg='red')) + +@cli.command() +def list(): + """List all servers""" + try: + response = requests.get(f"{API_URL}/servers") + response.raise_for_status() + servers = response.json() + + if not servers: + click.echo("No servers found.") + return + + table_data = [[s['id'], s['hostname'], s['ip_address'], s['state']] for s in servers] + headers = ['ID', 'Hostname', 'IP Address', 'State'] + + click.echo(tabulate(table_data, headers=headers, tablefmt='grid')) + except Exception as e: + click.echo(click.style(f"✗ Error: {str(e)}", fg='red')) + +@cli.command() +@click.argument('server_id', type=int) +def get(server_id): + """Get server by ID""" + try: + response = requests.get(f"{API_URL}/servers/{server_id}") + response.raise_for_status() + server = response.json() + + click.echo(f"ID: {server['id']}") + click.echo(f"Hostname: {server['hostname']}") + click.echo(f"IP: {server['ip_address']}") + click.echo(f"State: {server['state']}") + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + click.echo(click.style(f"✗ Server not found", fg='red')) + else: + click.echo(click.style(f"✗ Error: {e.response.json().get('detail', str(e))}", fg='red')) + except Exception as e: + click.echo(click.style(f"✗ Error: {str(e)}", fg='red')) + +@cli.command() +@click.argument('server_id', type=int) +@click.option('--hostname', help='New hostname') +@click.option('--ip', help='New IP address') +@click.option('--state', type=click.Choice(['active', 'offline', 'retired']), help='New state') +def update(server_id, hostname, ip, state): + """Update a server""" + if not any([hostname, ip, state]): + click.echo(click.style("✗ Error: At least one field must be provided to update", fg='red')) + return + + try: + update_data = {} + if hostname: + update_data['hostname'] = hostname + if ip: + update_data['ip_address'] = ip + if state: + update_data['state'] = state + + response = requests.put(f"{API_URL}/servers/{server_id}", json=update_data) + response.raise_for_status() + server = response.json() + + click.echo(click.style(f"✓ Server updated successfully!", fg='green')) + click.echo(f"ID: {server['id']}") + click.echo(f"Hostname: {server['hostname']}") + click.echo(f"IP: {server['ip_address']}") + click.echo(f"State: {server['state']}") + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + click.echo(click.style(f"✗ Server not found", fg='red')) + else: + click.echo(click.style(f"✗ Error: {e.response.json().get('detail', str(e))}", fg='red')) + except Exception as e: + click.echo(click.style(f"✗ Error: {str(e)}", fg='red')) + +@cli.command() +@click.argument('server_id', type=int) +@click.confirmation_option(prompt='Are you sure you want to delete this server?') +def delete(server_id): + """Delete a server""" + try: + response = requests.delete(f"{API_URL}/servers/{server_id}") + response.raise_for_status() + click.echo(click.style(f"✓ Server deleted successfully!", fg='green')) + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + click.echo(click.style(f"✗ Server not found", fg='red')) + else: + click.echo(click.style(f"✗ Error: {e.response.json().get('detail', str(e))}", fg='red')) + except Exception as e: + click.echo(click.style(f"✗ Error: {str(e)}", fg='red')) + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9cf6474 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + db: + image: postgres:15 + environment: + POSTGRES_DB: inventory + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + web: + build: . + ports: + - "8000:8000" + environment: + DB_HOST: db + DB_NAME: inventory + DB_USER: postgres + DB_PASSWORD: postgres + DB_PORT: 5432 + depends_on: + db: + condition: service_healthy + volumes: + - ./app:/app/app + - ./cli.py:/app/cli.py + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + +volumes: + postgres_data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3a9d671 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +psycopg2-binary==2.9.9 +pydantic==2.5.0 +click==8.1.7 +requests==2.31.0 +pytest==7.4.3 +httpx==0.25.1 +tabulate==0.9.0