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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
104 changes: 104 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Prerequisites

To work with this project, you need:
* make
* docker & docker-compose

These tools are used for building, running, and testing the project.

# Initial Setup

After cloning the repository, it is recommended to run the test suite to ensure the system is working correctly:
```console
make test_suite
```

The test suite covers:
* API controllers
* Services
* Repositories

# Running the Stage Environment

The stage environment is preconfigured for demo purposes. Secrets are included in the repository.

To start the stage environment:
```console
make run_stage
```

This will:
* Start the database
* Apply migrations
* Launch the FastAPI server

Swagger documentation will be available at:
```
http://localhost:8000/docs
```

You can test endpoints via Swagger or using the CLI.

# CLI Usage

You can test endpoints via Swagger or using the CLI.
```console
# Check available CLI commands
./cli servers --help

# List servers
./cli servers list

# Create a new server
./cli servers create <hostname> <ip_address> <state>
# Example:
./cli servers create dkushche 10.22.32.3 offline

# Get a server by ID
./cli servers get 1

# Update a server (all fields are required since PUT is used)
./cli servers update 1 <hostname> <ip_address> <state>
# Example:
./cli servers update 1 dima 10.22.32.3 active

# Delete a server
./cli servers delete 1
```

Note: The update operation is a PUT request, so all fields must be provided; missing fields will be removed, which is not desired.

# Development Environment

The development environment is designed with containerized toolboxes. Containers include all necessary software but do not run any services by default. The source code is mounted via bind volumes, and FastAPI is configured with autoreload for fast development.

Useful `make` commands:
```console
# Start all development containers
make run_dev_services

# Enter the API container
make enter_dev_api
# Inside:
# - Start server: uv run main.py
# - Install packages: uv sync --frozen
# - Run tests: uv run -- pytest

# Enter the migrations container
make enter_dev_migrations

# Enter the CLI container
make enter_dev_cli
```

Notes:
* The database is reset between test runs using fixtures to ensure consistent test results.
* The project uses FastAPI dependency injection, pytest-asyncio, and asyncpg for asynchronous PostgreSQL interactions.


# Further improvements

* Adding Linter
* Adding more sophisticated tests for API
* Improve test framework(refactore fixture recheck scoping)
* Add E2E tests
50 changes: 50 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.PHONY: run_dev_services \
enter_dev_api\
enter_dev_migrations
enter_dev_cli \
stop_dev_services \
run_stage clear_stage \
test_suite\
help

.DEFAULT_GOAL := help

## Show this help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nAvailable targets:\n\n"} \
/^[a-zA-Z_-]+:.*##/ { printf " %-25s %s\n", $$1, $$2 }' $(MAKEFILE_LIST)

## Run dev services (API, DB, migrator) in background
run_dev_services: clear_stage ## Start DEV environment (detached)
docker compose -f infra/compose/dev.yml up --build --detach

## Enter dev API container shell
enter_dev_api: ## Enter DEV API container
docker compose -f infra/compose/dev.yml exec api.mathpix.com bash

## Enter dev migrator container shell
enter_dev_migrations: ## Enter DEV migrator container
docker compose -f infra/compose/dev.yml exec migrator.mathpix.com bash

## Enter dev cli container shell
enter_dev_cli: ## Enter DEV CLI container
docker compose -f infra/compose/dev.yml exec cli.mathpix.com bash

## Stop and remove dev services and volumes
stop_dev_services: ## Stop DEV environment and remove volumes
docker compose -f infra/compose/dev.yml down --volumes

## Run stage services (foreground)
run_stage: stop_dev_services ## Start STAGE environment
docker compose -f infra/compose/stage.yml up --build

## Stop stage services and remove volumes
clear_stage: ## Stop STAGE environment and remove volumes
docker compose -f infra/compose/stage.yml down --volumes

test_suite: stop_dev_services run_dev_services ## Run test suite
docker compose -f infra/compose/dev.yml exec migrator.mathpix.com uv sync --frozen
docker compose -f infra/compose/dev.yml exec migrator.mathpix.com uv run main.py

docker compose -f infra/compose/dev.yml exec api.mathpix.com uv sync --frozen
docker compose -f infra/compose/dev.yml exec api.mathpix.com uv run uv run -- pytest
1 change: 1 addition & 0 deletions cli
8 changes: 8 additions & 0 deletions infra/compose/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=QVlTe2IKrRY2unSDu8P5N436wpx59ur7ctSHB5doZwxi
POSTGRES_DB=inventory
POSTGRES_HOST=inventorydb.mathpix.com

DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}
API_PORT=8000
API_URL=http://api.mathpix.com:${API_PORT}
10 changes: 10 additions & 0 deletions infra/compose/bin/stage_cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash

set -e -o pipefail

SCRIPT_PATH="$(readlink -f "$0")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"

cd "$SCRIPT_DIR/.."

docker compose -f stage.yml exec cli.mathpix.com python main.py "$@"
21 changes: 21 additions & 0 deletions infra/compose/common/db.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
inventorydb.mathpix.com:
image: postgres:18.1-alpine
container_name: inventorydb
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db_data:/var/lib/postgresql/data

healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s

volumes:
db_data:
45 changes: 45 additions & 0 deletions infra/compose/dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
include:
- common/db.yml
services:
migrator.mathpix.com:
build:
context: ../../projects/migrator/postgres
dockerfile: Dockerfile
target: dev
volumes:
- ../../projects/migrator/postgres:/app
container_name: migrator
depends_on:
- inventorydb.mathpix.com
environment:
DATABASE_URL: ${DATABASE_URL}

api.mathpix.com:
build:
context: ../../projects/api
dockerfile: Dockerfile
target: dev
volumes:
- ../../projects/api:/app
container_name: api
depends_on:
- inventorydb.mathpix.com
- migrator.mathpix.com
ports:
- ${API_PORT}:8000
environment:
DATABASE_URL: ${DATABASE_URL}
ENV: dev

cli.mathpix.com:
build:
context: ../../projects/cli
dockerfile: Dockerfile
target: dev
volumes:
- ../../projects/cli:/app
container_name: cli
depends_on:
- api.mathpix.com
environment:
API_URL: ${API_URL}
45 changes: 45 additions & 0 deletions infra/compose/stage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
include:
- common/db.yml
services:
migrator.mathpix.com:
build:
context: ../../projects/migrator/postgres
dockerfile: Dockerfile
target: runtime
container_name: migrator
depends_on:
inventorydb.mathpix.com:
condition: service_healthy
environment:
DATABASE_URL: ${DATABASE_URL}
restart: no

api.mathpix.com:
build:
context: ../../projects/api
dockerfile: Dockerfile
target: runtime
container_name: api
depends_on:
inventorydb.mathpix.com:
condition: service_healthy
migrator.mathpix.com:
condition: service_completed_successfully
ports:
- ${API_PORT}:8000
environment:
DATABASE_URL: ${DATABASE_URL}
ENV: stage
restart: always

cli.mathpix.com:
build:
context: ../../projects/cli
dockerfile: Dockerfile
target: runtime
container_name: cli
depends_on:
- api.mathpix.com
environment:
API_URL: ${API_URL}
restart: no
2 changes: 2 additions & 0 deletions projects/api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.venv
.pytest_cache
1 change: 1 addition & 0 deletions projects/api/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
71 changes: 71 additions & 0 deletions projects/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
##################################################
FROM python:3.12-slim AS base_builder

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

RUN <<EOF
set -ex

apt-get update
apt-get install -y --no-install-recommends \
build-essential \
curl

rm -rf /var/lib/apt/lists/*
EOF

RUN pip install --no-cache-dir uv

##################################################
FROM base_builder AS dev

ENV PORT=8000
ENV HOST=0.0.0.0

ENV REPOSITORY=postgres

WORKDIR /app

RUN useradd -m api
USER api

EXPOSE 8000

CMD ["sleep", "infinity"]

##################################################
FROM base_builder AS runtime_builder

COPY pyproject.toml uv.lock ./

ENV UV_PROJECT_ENVIRONMENT="/usr/local/"

RUN uv sync --frozen --no-dev

##################################################
FROM python:3.12-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

ENV PORT=8000
ENV HOST=0.0.0.0

ENV REPOSITORY=postgres

WORKDIR /app

COPY --from=runtime_builder /usr/local /usr/local

COPY src ./src
COPY main.py .

RUN useradd -m api
USER api

EXPOSE 8000

CMD ["python", "main.py"]
15 changes: 15 additions & 0 deletions projects/api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env python3

import uvicorn

from src.settings import get_settings

if __name__ == "__main__":
settings = get_settings()

uvicorn.run(
"src.app:app",
host=settings.host,
port=settings.port,
reload=settings.env == "dev",
)
Loading