Skip to content
Merged
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
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI - Tests and Coverage

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

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.12' ]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install pytest pytest-django pytest-cov

- name: Run tests and collect coverage
run: |
pytest --maxfail=1 --disable-warnings -q --cov=entities --cov-report=xml:coverage.xml --cov-report=html:coverage_html

- name: Upload coverage artifacts
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
coverage.xml
coverage_html/
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
*.pyc
.DS_Store
.env
db.sqlite3
db.sqlite3
.coverage
128 changes: 60 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,83 @@
# Inventory API (Django + Django Ninja)

A RESTful backend for managing entities, implemented in Python using Django and Django Ninja.

Key features
- CRUD for the `Entity` model (fields: `type`, `name`, `description`, `created_at`).
- OpenAPI/Swagger documentation provided by Django Ninja (`/api/docs`).

Entity model
- `id`: int (auto)
- `type`: string, required, max 100 characters
- `name`: string, optional, max 255 characters
- `description`: text, optional
- `created_at`: automatic creation timestamp

Planned endpoints
- GET /api/entities — list all entities
- GET /api/entities/{id} — retrieve entity by id
- POST /api/entities — create a new entity
- PUT /api/entities/{id} — update an existing entity
- DELETE /api/entities/{id} — delete an entity

Prerequisites
- Python 3.11+ (or compatible)
- Virtual environment (`venv`) recommended

Development installation
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
```
A simple RESTful API for managing `Entity` records, built with **Django** and **Django Ninja**.

Running the project
## Main Features

Unix / macOS
```bash
# create and activate virtual environment
python -m venv .venv
source .venv/bin/activate
- Full CRUD operations for entities
- Interactive OpenAPI/Swagger documentation at `/api/docs`
- Tests with pytest + coverage reporting
- Production-ready structure (PostgreSQL + Docker friendly)

# install dependencies (first time)
pip install -r requirements.txt
## Entity Model

# apply migrations and run the development server
python manage.py migrate
python manage.py runserver
```
| Field | Type | Required | Notes |
|---------------|--------------|----------|---------------------------------|
| `id` | int | auto | Primary key |
| `type` | string(100) | yes | Entity type |
| `name` | string(255) | no | Descriptive name |
| `description` | text | no | Detailed description |
| `created_at` | datetime | auto | Creation timestamp |

## API Endpoints

| Method | Endpoint | Description |
|--------|-------------------------|---------------------------------|
| GET | `/api/entities` | List all entities |
| GET | `/api/entities/{id}` | Retrieve single entity |
| POST | `/api/entities` | Create a new entity |
| PUT | `/api/entities/{id}` | Update an existing entity |
| DELETE | `/api/entities/{id}` | Delete an entity |

## Prerequisites

Windows (PowerShell)
```powershell
# create and activate virtual environment
- Python 3.11+
- Virtual environment recommended (`venv`)

## Local Development Setup

### 1. Create and activate virtual environment
```bash
# Linux/macOS:
python -m venv .venv
source .venv/bin/activate

# or on Windows (PowerShell):
.\.venv\Scripts\Activate.ps1
```

# install dependencies (first time)
### 2. Install dependencies
```bash
pip install -r requirements.txt
```

# apply migrations and run the development server
### 3. Apply migrations
```bash
python manage.py migrate
python manage.py runserver
```

Run with ASGI server (uvicorn)
### 4. Run the server:
```bash
# hot-reload ASGI server (useful for Django Ninja/OpenAPI during development)
uvicorn inventory_api.asgi:application --reload
python manage.py runserver
```

API documentation
- After starting the server, interactive API docs will be available at: `http://127.0.0.1:8000/api/docs`
Interactive API documentation will be available at:
**http://localhost:8000/api/docs**

Tests
- Tests use `pytest`/`pytest-django`.
```bash
pytest --cov=.
```

Docker (optional)
- You can dockerize the application and use a Postgres service for production. Add `Dockerfile` and `docker-compose.yml` for orchestration.
## Testing & Coverage

Security notes
- Do not commit secrets to the repository. Use environment variables or a secrets manager for production credentials.
### 1. Run tests with HTML coverage report
```bash
pytest --cov=entities --cov-report=html:coverage_html
```

Suggested next steps
- Implement the API with Django Ninja (`entities/api.py`) and expose routes in `inventory_api/urls.py`.
- Write tests for the endpoints and add CI/coverage.
### 2. View the report
```bash
python -m http.server --directory coverage_html 8000
```
→ open **http://localhost:8000**

Files edited: `entities/models.py`, `entities/migrations/0001_initial.py`, `inventory_api/settings.py` (registered `entities` app).
![Coverage Report](./docs/test-screenshots/files_test.png)
![Coverage Report](./docs/test-screenshots/functions_test.png)
![Coverage Report](./docs/test-screenshots/classes_test.png)
Binary file added docs/test-screenshots/classes_test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/test-screenshots/files_test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/test-screenshots/functions_test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions tests/test_api_direct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pytest
from django.http import Http404

from entities import api, services
from entities.schemas import EntityCreate, EntityUpdate
from entities.models import Entity


@pytest.mark.django_db
def test_list_entities_direct_empty_then_nonempty():
# ensure DB empty for entities
for e in services.list_entities():
e.delete()

res = api.list_entities(None)
assert isinstance(res, list)
assert res == []

# create one and verify
obj = services.create_entity({"type": "t"})
res2 = api.list_entities(None)
assert any(isinstance(x, Entity) for x in res2)


@pytest.mark.django_db
def test_get_entity_direct_404_and_success():
with pytest.raises(Http404):
api.get_entity(None, 999999)

obj = services.create_entity({"type": "g"})
got = api.get_entity(None, obj.id)
assert got.id == obj.id


@pytest.mark.django_db
def test_create_entity_direct():
payload = EntityCreate(type="x", name="n")
status, obj = api.create_entity(None, payload)
assert status == 201
assert isinstance(obj, Entity)


@pytest.mark.django_db
def test_update_entity_direct_404_and_partial():
payload = EntityUpdate(name="updated")
with pytest.raises(Http404):
api.update_entity(None, 999999, payload)

obj = services.create_entity({"type": "t", "name": "old", "description": "d"})
updated = api.update_entity(None, obj.id, payload)
assert updated.name == "updated"
assert updated.type == "t"


@pytest.mark.django_db
def test_delete_entity_direct_404_and_success():
with pytest.raises(Http404):
api.delete_entity(None, 999999)

obj = services.create_entity({"type": "t"})
status, body = api.delete_entity(None, obj.id)
assert status == 204
with pytest.raises(Entity.DoesNotExist):
services.get_entity(obj.id)
44 changes: 44 additions & 0 deletions tests/test_api_extra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest
import json
from django.test import Client


@pytest.mark.django_db
def test_get_not_found():
client = Client()
assert client.get("/api/entities/999999").status_code == 404


@pytest.mark.django_db
def test_create_invalid_payload():
client = Client()
resp = client.post(
"/api/entities", json.dumps({"name": "x"}), content_type="application/json"
)
assert resp.status_code in (400, 422)


@pytest.mark.django_db
def test_delete_nonexistent_returns_404():
client = Client()
resp = client.delete("/api/entities/999999")
assert resp.status_code == 404


@pytest.mark.django_db
def test_list_empty_returns_empty_list():
client = Client()
resp = client.get("/api/entities")
assert resp.status_code == 200
assert resp.json() == []


@pytest.mark.django_db
def test_update_not_found_returns_404():
client = Client()
resp = client.put(
"/api/entities/999999",
data=json.dumps({"name": "nope"}),
content_type="application/json",
)
assert resp.status_code == 404
12 changes: 12 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest

from entities import services
from entities.models import Entity


@pytest.mark.django_db
def test_entity_str_representation():
obj = services.create_entity({"type": "mytype", "name": "myname"})
s = str(obj)
assert "mytype" in s
assert "myname" in s
4 changes: 4 additions & 0 deletions tests/test_views_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def test_views_import():
# simple import test to cover views module
import entities.views
assert hasattr(entities.views, "render")