Skip to content

Commit cafe660

Browse files
committed
Version 1.0.0 orybachok app
1 parent 53b6c94 commit cafe660

File tree

14 files changed

+937
-23
lines changed

14 files changed

+937
-23
lines changed

.gitignore

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Virtual environments
24+
venv/
25+
ENV/
26+
env/
27+
28+
# IDE
29+
.idea/
30+
.vscode/
31+
*.swp
32+
*.swo
33+
34+
# Environment
35+
.env
36+
.env.local
37+
38+
# Testing
39+
.pytest_cache/
40+
.coverage
41+
htmlcov/
42+
43+
# Docker
44+
postgres_data/
45+

Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# STAGE 1: Base
2+
# We load dependencies here so they are cached for both test and prod
3+
FROM python:3.11-slim AS base
4+
5+
WORKDIR /app
6+
7+
# Prevent Python from writing pyc files and buffering stdout
8+
ENV PYTHONDONTWRITEBYTECODE=1
9+
ENV PYTHONUNBUFFERED=1
10+
11+
# Install dependencies
12+
COPY requirements.txt .
13+
RUN pip install --no-cache-dir -r requirements.txt
14+
15+
# STAGE 2: Test
16+
# This stage prepares the test environment (tests run via docker compose, not during build)
17+
FROM base AS test
18+
COPY . .
19+
# You can install dev-dependencies here if you have a separate requirements-dev.txt
20+
# RUN pip install pytest-cov
21+
CMD ["pytest", "--verbose", "tests/"]
22+
23+
# STAGE 3: Production
24+
# This stage is optimized for runtime
25+
FROM base AS production
26+
COPY . .
27+
EXPOSE 8000
28+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

README.md

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,76 @@
1-
# Instructions
1+
# Server Inventory Manager
22

3-
You are developing an inventory management software solution for a cloud services company that provisions servers in multiple data centers. You must build a CRUD app for tracking the state of all the servers.
3+
A CRUD application to manage server inventory.
44

5-
Deliverables:
6-
- PR to https://github.com/Mathpix/hiring-challenge-devops-python that includes:
7-
- API code
8-
- CLI code
9-
- pytest test suite
10-
- Working Docker Compose stack
5+
## Specs
116

12-
Short API.md on how to run everything, also a short API and CLI spec
7+
### API
8+
API documentation available at [http://&lt;hostname&gt;:8000/docs](http://<hostname>:8000/docs)
9+
- **POST /servers**: Create
10+
- **GET /servers**: List all
11+
- **GET /servers/{id}**: Details
12+
- **PUT /servers/{id}**: Update
13+
- **DELETE /servers/{id}**: Remove
1314

14-
Required endpoints:
15-
- POST /servers → create a server
16-
- GET /servers → list all servers
17-
- GET /servers/{id} → get one server
18-
- PUT /servers/{id} → update server
19-
- DELETE /servers/{id} → delete server
15+
### CLI
16+
- `list`: Show table of servers
17+
- `create [hostname] [ip] [state] [datacenter](optional) [cpu](optional) [ram_gb](optional)`: Add server
18+
- `update [id] [hostname] [ip] [state] [datacenter](optional) [cpu](optional) [ram_gb](optional)`: Edit server
19+
- `delete [id]`: Remove server
2020

21-
Requirements:
22-
- Use FastAPI or Flask
23-
- Store data in PostgreSQL
24-
- Use raw SQL
21+
## How to Run
2522

26-
Validate that:
27-
- hostname is unique
28-
- IP address looks like an IP
23+
### Option 1: Run Tests Then Deploy API (Interactive)
24+
25+
Run tests on the `inventory_test` database, and if successful, automatically build and start the API:
26+
27+
```bash
28+
./run-tests-and-deploy.sh
29+
```
30+
31+
This script will:
32+
1. Start the database service
33+
2. Run tests against `inventory_test` database
34+
3. Remove the test container on success
35+
4. Build and start the API service
36+
37+
### Option 2: Run Tests Only
38+
39+
```bash
40+
docker-compose up -d db --wait
41+
docker-compose run --rm test
42+
```
43+
44+
### Option 3: Start Full Stack Directly (Without test container cleanup)
45+
46+
```bash
47+
docker-compose up --build
48+
```
49+
50+
51+
## Using the CLI
52+
53+
First, install the dependencies:
54+
55+
```bash
56+
python -m venv .venv
57+
source .venv/bin/activate
58+
59+
pip install -r requirements.txt
60+
```
61+
62+
Then, you can use the CLI to manage servers:
63+
64+
```bash
65+
python -m app.cli list
66+
python -m app.cli create web-01 10.0.0.5 active
67+
```
68+
69+
## Future Improvements
70+
71+
- Add extra columns
72+
- Add IP address unique check
73+
- Add ability to update one field independently
74+
- Deploy separate binary for CLI
2975

30-
State is one of: active, offline, retired
3176

app/__init__.py

Whitespace-only changes.

app/cli.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import typer
2+
import requests
3+
import json
4+
from rich.console import Console
5+
from rich.table import Table
6+
7+
app = typer.Typer()
8+
console = Console()
9+
API_URL = "http://localhost:8000/servers"
10+
11+
@app.command()
12+
def list():
13+
"""List all servers."""
14+
try:
15+
response = requests.get(API_URL)
16+
response.raise_for_status()
17+
servers = response.json()
18+
19+
table = Table(title="Server Inventory")
20+
table.add_column("ID", style="cyan")
21+
table.add_column("Hostname", style="magenta")
22+
table.add_column("IP Address", style="green")
23+
table.add_column("State", style="yellow")
24+
table.add_column("Datacenter", style="blue")
25+
table.add_column("CPU Cores", style="purple")
26+
table.add_column("RAM GB", style="red")
27+
28+
for s in servers:
29+
table.add_row(
30+
str(s['id']), s['hostname'], s['ip_address'],
31+
s['state'], s['datacenter'] or '',
32+
str(s['cpu_cores']) if s['cpu_cores'] else '',
33+
str(s['ram_gb']) if s['ram_gb'] else ''
34+
)
35+
36+
console.print(table)
37+
except Exception as e:
38+
console.print(f"[bold red]Error:[/bold red] {e}")
39+
40+
@app.command()
41+
def create(hostname: str, ip: str, state: str, datacenter: str, cpu_cores: int, ram_gb: int):
42+
"""Create a new server. State must be: active, offline, retired"""
43+
payload = {"hostname": hostname, "ip_address": ip, "state": state, "datacenter": datacenter,
44+
"cpu_cores": cpu_cores, "ram_gb": ram_gb}
45+
response = requests.post(API_URL, json=payload)
46+
if response.status_code == 201:
47+
console.print(f"[bold green]Created:[/bold green] {response.json()}")
48+
else:
49+
console.print(f"[bold red]Failed:[/bold red] {response.text}")
50+
51+
@app.command()
52+
def get(id: int):
53+
"""Get server details by ID."""
54+
response = requests.get(f"{API_URL}/{id}")
55+
if response.status_code == 200:
56+
console.print(response.json())
57+
else:
58+
console.print(f"[bold red]Error:[/bold red] {response.text}")
59+
60+
@app.command()
61+
def update(id: int, hostname: str, ip: str, state: str, datacenter: str, cpu_cores: int, ram_gb: int):
62+
"""Update a server."""
63+
payload = {"hostname": hostname, "ip_address": ip, "state": state, "datacenter": datacenter,
64+
"cpu_cores": cpu_cores, "ram_gb": ram_gb}
65+
response = requests.put(f"{API_URL}/{id}", json=payload)
66+
if response.status_code == 200:
67+
console.print(f"[bold green]Updated:[/bold green] {response.json()}")
68+
else:
69+
console.print(f"[bold red]Failed:[/bold red] {response.text}")
70+
71+
@app.command()
72+
def delete(id: int):
73+
"""Delete a server."""
74+
response = requests.delete(f"{API_URL}/{id}")
75+
if response.status_code == 204:
76+
console.print(f"[bold green]Server {id} deleted.[/bold green]")
77+
else:
78+
console.print(f"[bold red]Failed:[/bold red] {response.text}")
79+
80+
if __name__ == "__main__":
81+
app()

0 commit comments

Comments
 (0)