Purpose of this page
Show what each file and folder in metals_api/ is responsible for, and how a request actually moves through them.
The folder, top to bottom structure
| File / folder | What it's for, in plain terms |
|---|---|
| app.py | Creates the Flask app, reads JWT_SECRET_KEY / JWT_EXPIRATION_MINUTES from the environment, registers every blueprint (auth, elements, alloys, alloy elements, coins), and defines the one route that isn't in a blueprint: /health. The module-level app object it creates is what Gunicorn serves in the container. |
| Dockerfile | Builds the metals-api container image — Python 3.11, the dependencies, this folder, and Gunicorn on port 5000. Its build context is the repository root, not this folder. (dig deeper) |
| auth.py | The two decorators covered in Section 2: require_auth and require_roles. Shared by every route file below. |
| database/connection.py | Builds the SQLAlchemy engine and SessionFactory from the DB_* variables in .env. |
| models/ | One SQLAlchemy class per table (Element, Alloy, AlloyElement, Coin, User, Role), plus user_role.py for the users↔roles join table and base.py for the shared declarative base. |
| dtos/ | Plain dataclasses shaping request and response JSON — a Create*DTO, Update*DTO, and *ResponseDTO per resource, each with its own validate(). base.py holds the shared to_dictionary / from_dictionary conversion logic. |
| repositories/ | Plain functions that run the actual SQLAlchemy queries — get, add, update, delete — one file per table. user_repository.py is what auth_service.py uses to look up accounts and roles. |
| services/ | Business logic: opens a session, calls the repository, enforces rules (like “no duplicate element name”), and converts between models and DTOs. auth_service.py additionally hashes/verifies passwords with Argon2id and issues JWTs. exceptions.py defines BusinessValidationError, the one exception every service raises for a rule violation. |
| routes/ | Flask Blueprints — one per resource — wiring HTTP verb + path to a service call, applying @require_auth / @require_roles, and turning DTO validation errors or BusinessValidationError into the JSON error shapes from Section 1. |
| tests/ | A pytest suite mirroring the folders above (models/, dtos/, repositories/, services/, routes/), plus conftest.py, which sets test-only environment variables and a make_token() helper for building a valid JWT without actually calling /api/auth/login. |
One request, traced through every layer POST /api/elements
The clearest way to see how these layers connect is to follow one real request all the way through — an Admin creating a new element.
@element_blueprint.post("")
@require_auth
@require_roles("Admin")
def create_element():
data = request.get_json(silent=True)
errors = CreateElementDTO.validate(data)
if errors:
return jsonify({"errors": errors}), 400
request_dto = CreateElementDTO.from_dictionary(data)
response_dto = service.create_element(request_dto)
return jsonify(response_dto.to_dictionary()), 201
require_auth and require_roles already ran (see Section 2). The route's own job is small: validate the raw JSON with the DTO, turn it into a typed CreateElementDTO, and hand it to the service.
CreateElementDTO.validate(data) checks required fields, types, string lengths, and value ranges — all before a database session is even opened. from_dictionary also converts numeric strings into Decimal for fields like density, so the service never has to think about that conversion.
def create_element(dto: CreateElementDTO) -> ElementResponseDTO:
with SessionFactory() as session:
if element_repository.get_element_by_id(session, dto.atomic_number):
raise BusinessValidationError(["An element with this atomic_number already exists."])
...
element = Element(**dto.to_dictionary())
element = element_repository.add_element(session, element)
return ElementResponseDTO.from_model(element)
Opens a database session, enforces business rules the DTO can't know about by itself (like uniqueness, which requires checking the database), builds a Element model instance, and asks the repository to save it.
The repository's add_element is a thin wrapper around session.add(element) / session.commit() — it doesn't know or care why it's being asked to save this row. Element, the model, is what SQLAlchemy uses to translate that Python object into the actual INSERT statement against the elements table described in the Database schema tutorial.
ElementResponseDTO.from_model(element) converts the saved model back into a response DTO, the route calls .to_dictionary() on it and wraps it in jsonify(...), 201 — the JSON body an Insomnia request would actually see.
Key terms glossary
- Blueprint
- Flask's way of grouping related routes together (all of /api/elements, for example) and registering them on the app as one unit in app.py.
- DTO (Data Transfer Object)
- A plain object whose only job is describing the shape of data crossing a boundary — here, JSON in and out of the API. DTOs validate; models don't.
- Repository
- A layer whose only job is running database queries for one table, so services never write raw SQLAlchemy queries directly.
- Session / SessionFactory
- SQLAlchemy's unit of work with the database — a session is opened, used for one logical operation, and closed. SessionFactory() creates a new one each time a service needs one.
- Model
- A Python class mapped to a database table by SQLAlchemy's ORM (Object-Relational Mapper) — an instance of Element corresponds to one row in the elements table.