Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.git
__pycache__
*.py[cod]
*.egg-info
.pytest_cache
.env
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Database Configuration
POSTGRES_USER=admin
POSTGRES_PASSWORD=password
POSTGRES_DB=inventory
POSTGRES_HOST=db
POSTGRES_PORT=5432

# Application Configuration
# Constructed automatically in docker-compose, but if running locally:
# DATABASE_URL=postgresql://admin:password@localhost:5432/inventory
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
test:
runs-on: ubuntu-latest
env:
POSTGRES_USER: ${{ secrets.POSTGRES_USER || 'testuser' }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD || 'testpass' }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB || 'testdb' }}

services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: pip

- name: Install dependencies
run: pip install ".[test]"

- name: Initialize database
run: |
PGPASSWORD=${{ env.POSTGRES_PASSWORD }} psql -h localhost -U ${{ env.POSTGRES_USER }} -d ${{ env.POSTGRES_DB }} -f init.sql

- name: Run tests
run: pytest -v
env:
DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }}

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install ruff
run: pip install ruff

- name: Run linter
run: ruff check . --output-format=github
73 changes: 73 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Python
__pycache__/
__pycache__
__pycache__/*
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual Environment
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# Editor / IDE
.vscode/
.idea/
*.swp
*.swo

# Docker
.dockerignore

# OS
.DS_Store
Thumbs.db

# Project specific
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
133 changes: 133 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Server Inventory API & CLI

## Overview
REST API and CLI for tracking server inventory. Built with FastAPI, PostgreSQL (raw SQL), and Typer.

## Quick Start

```bash
# Start with Docker/Podman
podman compose up -d --build

# Run tests
podman compose exec api pytest -v

# View API docs
open http://localhost:8000/docs
```

## Makefile Commands

```bash
make help # Show all commands
make podman-up # Start the stack
make podman-test # Run tests in container
make lint # Run linter
make format # Format code
```

---

## API Endpoints

### CRUD Operations

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/servers` | Create a server |
| GET | `/servers` | List servers (with filtering) |
| GET | `/servers/{id}` | Get a server |
| PUT | `/servers/{id}` | Update a server |
| DELETE | `/servers/{id}` | Delete a server |

### Filtering & Pagination

```bash
GET /servers?limit=10&offset=0 # Pagination
GET /servers?state=active # Filter by state
GET /servers?hostname_contains=web # Search hostname
GET /servers?state=active&hostname_contains=web # Combined
```

### ETag Concurrency Control

All responses include an `ETag` header for optimistic concurrency:

```bash
# Conditional GET (returns 304 if unchanged)
curl -H "If-None-Match: \"abc123\"" http://localhost:8000/servers/1

# Conditional PUT (returns 412 if stale)
curl -X PUT -H "If-Match: \"abc123\"" -d '{"state":"offline"}' http://localhost:8000/servers/1
```

### Health & Observability

| Endpoint | Description |
|----------|-------------|
| `/health` | Liveness probe |
| `/ready` | Readiness probe (checks DB) |
| `/metrics` | Prometheus metrics |

### API Versioning

All endpoints are available at both `/servers` and `/v1/servers`.

---

## CLI Usage

```bash
# Basic commands
python cli/main.py create web-01 192.168.1.5 active
python cli/main.py list
python cli/main.py get 1
python cli/main.py update 1 --state offline
python cli/main.py delete 1

# Output formats
python cli/main.py list --format json # JSON output
python cli/main.py list --format table # Table output (default)

# Filtering
python cli/main.py list --state active
python cli/main.py list --hostname web
```

### CLI Features
- **Retry with backoff** - Auto-retries on connection errors
- **Format options** - `--format json` or `--format table`
- **Filtering** - `--state` and `--hostname` flags

---

## Request Tracing

All responses include `X-Request-ID` header for distributed tracing.
Send your own `X-Request-ID` header and it will be echoed back.

---

## Development

```bash
# Install dev dependencies
pip install -e ".[test,dev]"

# Run pre-commit hooks
pre-commit install
pre-commit run --all-files

# Run Alembic migrations
alembic upgrade head
```

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | `postgresql://admin:password@db:5432/inventory` | Database connection |
| `POSTGRES_USER` | `admin` | DB username |
| `POSTGRES_PASSWORD` | `password` | DB password |
| `POSTGRES_DB` | `inventory` | DB name |
| `OTEL_CONSOLE_EXPORT` | `false` | Enable trace console output |
40 changes: 40 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Build Stage
FROM python:3.10-slim-bullseye as builder

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

RUN pip install --upgrade pip

COPY pyproject.toml .
COPY . .

# Build wheels for all dependencies including test extras
RUN pip wheel --no-cache-dir --wheel-dir /app/wheels ".[test]"

# Runtime Stage
FROM python:3.10-slim-bullseye

WORKDIR /app

# Create a non-root user
RUN addgroup --system app && adduser --system --group app

# Install Runtime Dependencies
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/pyproject.toml .

# Install dependencies from wheels
RUN pip install --no-cache /wheels/*

COPY . /app

# Change ownership
RUN chown -R app:app /app

# Switch to non-root user
USER app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
37 changes: 37 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.PHONY: help install dev test lint format run clean docker-up docker-down docker-test

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

install: ## Install production dependencies
pip install .

dev: ## Install development dependencies
pip install -e ".[test]"

test: ## Run tests
pytest -v

lint: ## Run linter (ruff)
ruff check .

format: ## Format code (ruff)
ruff format .

run: ## Run the API locally
uvicorn app.main:app --reload

clean: ## Clean up cache files
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true

# Podman commands
podman-up: ## Start the stack with Podman
podman compose up -d --build

podman-down: ## Stop the stack
podman compose down

podman-test: ## Run tests in container
podman compose exec api pytest -v
Loading