# Equipment Documentation > Documentation for building and maintaining Python applications with Equipment. Complete LLM-oriented documentation bundle for Equipment. Equipment is a Python project scaffolding framework. It generates a ready-to-run project with application structure, configuration, dependency injection, logging, SQLAlchemy database access, queues, scheduling, local/S3 storage, unittest-based tests, bytecode compilation support, and an optional FastAPI entry point. Read this file when you need enough context to build applications using Equipment or maintain the Equipment repository. Preserve Python 3.12, 3.13, and 3.14 support, keep Unix and Windows behavior in mind, and keep README, website docs, and generated LLM outputs consistent. ## Equipment Overview Equipment is a scaffolding framework for creating Python application projects. It gives a new project a working structure for configuration, dependency injection, logging, storage, database access, queues, scheduling, tests, and optional FastAPI serving. The goal is to remove the repetitive setup that appears at the beginning of many Python applications while keeping the generated project simple enough to modify. Equipment is not a web framework and it does not force one application architecture. It creates a practical starting point that can support scripts, workers, scheduled jobs, APIs, and internal tools. ## Who Should Use It Use Equipment when you want: - a Python project scaffold with common application services already wired; - a consistent layout for scripts, web entry points, queue workers, and schedulers; - configuration files that can be driven by environment variables; - a small dependency injection container for framework and application services; - a template that works on Windows, macOS, and Linux. Equipment is less useful when you only need a single-file script, when you already have a strong framework-specific project generator, or when project creation must work offline without a local copy of the template. ## Supported Platforms - Python 3.12, 3.13, and 3.14. - Windows, macOS, and Linux. - CI is configured to test all supported Python versions on all three operating systems. ## Quick Start ```bash python -m pip install equipment equipment new my-app cd my-app python -m pip install . python main.py ``` Windows users can use the Python launcher: ```bat py -3.14 -m pip install equipment equipment new my-app cd my-app py -3.14 -m pip install . py -3.14 main.py ``` ## What Gets Generated An Equipment project includes: - `app/` for custom services and scheduler definitions; - `config/` for YAML and JSON configuration; - `database/` for SQLite and Alembic migrations; - `storage/` for local files and logs; - `tests/` with a `unittest.TestCase` base class and Faker support; - `main.py`, `queues.py`, `scheduler.py`, and `web.py` entry points; - `pyproject.toml`, `.env.example`, and `README.md`. ## Core Concepts - Configuration is loaded from `.env` and `config/*` files. - The `Equipment` container exposes framework services: `log`, `queue`, `storage`, and `database`. - The generated `App` class registers application services with `dependency-injector` singletons. - Queue and storage drivers can be switched through configuration. - Tests use standard `unittest` discovery, so they run with the Python standard library test runner. ## Mental Model An Equipment application has two layers: 1. The framework layer, provided by the `equipment` package. 2. The application layer, generated into your project and owned by you. The framework layer is intentionally small. It knows how to load configuration, create loggers, create queue drivers, create storage drivers, and create a SQLAlchemy database factory. The application layer decides what the app does with those services. The generated project is not meant to be a black box. You should edit it. Add services under `app/`, add configuration under `config/`, and add tests under `tests/`. The generated files are a starting contract, not a code generator that needs to own the project forever. ## Request And Process Flow Most Equipment entry points follow the same flow: 1. Import `app` from `app/__init__.py`. 2. Call `app()` to create or retrieve an application container for the current base path. 3. Read config values from `application.config`. 4. Use framework services such as `application.log()`, `application.storage()`, `application.database()`, or `application.queue()`. 5. Delegate business logic to application services registered on the generated `App` class. For example, `main.py`, `web.py`, `scheduler.py`, and `queues.py` all start by creating an application context. That keeps scripts, web routes, scheduled jobs, and workers aligned around the same configuration and dependency graph. ## What Equipment Owns Equipment owns these reusable concerns: - loading `.env` and `config/` files; - wiring framework services into a dependency injection container; - creating log handlers from config; - selecting sync or Redis queue behavior; - selecting local or S3 storage behavior; - creating SQLAlchemy engines and sessions; - compiling project files for bytecode distribution; - scaffolding a new project from the maintained template. Your application owns these concerns: - domain models and business services; - database schema design and migrations; - HTTP routes and request validation; - queued task functions; - scheduler definitions; - deployment-specific environment variables; - tests for your own behavior. ## What Equipment Does Not Do Equipment does not replace FastAPI, SQLAlchemy, Alembic, Redis, boto3, or `unittest`. It provides a structure for using those tools consistently. When you need advanced behavior, use the underlying library directly inside your own service layer. Equipment also does not provide a built-in authentication system, object-relational model base, API router generator, Docker deployment contract, secret manager, production process supervisor, or cloud deployment platform. Those choices stay with the application. ## Good First Changes After generating a project, common first edits are: - rename the project in `.env` by changing `APP_NAME`; - set `APP_ENV=local` for local development; - replace `app/Inspire.py` with your first domain service; - register that service in `app/__init__.py`; - add tests under `tests/`; - choose whether `web.py`, `queues.py`, or `scheduler.py` are needed; - remove optional generated dependencies from `pyproject.toml` if a feature will not be used. ## Reading Order New users should read these pages in order: 1. Installation. 2. Generated Directory Structure. 3. Common Workflows. 4. Configuration. 5. Dependency Injection. 6. The feature pages for the services they plan to use. LLM coding agents should also read [llms.txt](https://equipment-python.vercel.app/llms.txt) and [llms-full.txt](https://equipment-python.vercel.app/llms-full.txt) before making broad changes, because those hosted files summarize project constraints and common mistakes. ## Maintenance Contract The repository avoids dependency upgrades inside compatibility or documentation work. Dependency updates should be done as their own change with the full test suite and generated-project workflow validated afterwards. --- ## Installation ## Requirements - Python 3.12, 3.13, or 3.14. - Windows, macOS, or Linux. - `pip` installed for the selected Python interpreter. - Network access when running `equipment new`, because the command downloads the current project template from GitHub. Check your interpreter: ```bash python --version ``` On Windows, the Python launcher can select a version explicitly: ```bat py -3.14 --version ``` ## Install Equipment Install the CLI into your active Python environment: ```bash python -m pip install equipment ``` If you use `pipx` for command-line tools: ```bash pipx install equipment ``` ## Create A Virtual Environment Unix shells: ```bash python -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip python -m pip install equipment ``` Windows PowerShell or Command Prompt: ```bat py -3.14 -m venv .venv .venv\Scripts\activate python -m pip install --upgrade pip python -m pip install equipment ``` ## Create A Project ```bash equipment new my-app cd my-app python -m pip install . python main.py ``` The generated project has its own `pyproject.toml`. Install it from inside the generated directory so imports such as `from app import app` resolve correctly. ## What `equipment new` Does The `new` command creates a project from the maintained template in this repository. At a high level it: 1. downloads the Equipment repository archive from GitHub; 2. extracts the `project/` template from that archive; 3. copies the template into the target directory; 4. creates `.env` from `.env.example`; 5. replaces `PROJECT_NAME` in the generated `pyproject.toml` with your project name; 6. prompts before overwriting an existing directory. The command is intentionally simple: it does not ask a long list of questions or generate different project flavors. Start with the full template, then remove optional pieces you do not need. ## Project Names Use project names that are valid package names after normalization. Short lowercase names with hyphens or underscores are easiest to work with: ```bash equipment new billing-api equipment new data_worker ``` Avoid names with spaces, shell metacharacters, or path separators. The command creates a directory with the name you pass and writes that name into generated metadata. ## Recommended Local Setup For an application project, install Equipment globally with `pipx` or in a tooling environment, then create a separate virtual environment inside the generated project: ```bash python -m pip install --user pipx pipx install equipment equipment new my-app cd my-app python -m venv .venv source .venv/bin/activate python -m pip install .[dev] python -m unittest discover -s tests ``` On Windows PowerShell: ```powershell py -3.14 -m pip install --user pipx py -3.14 -m pipx ensurepath pipx install equipment equipment new my-app cd my-app py -3.14 -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install .[dev] python -m unittest discover -s tests ``` Using `python -m ...` commands helps ensure that packages install into the same interpreter that runs the application. ## Verify The Generated Project Run the generated tests: ```bash python -m unittest discover -s tests ``` Run optional entry points only when their dependencies are configured: ```bash python scheduler.py python queues.py python web.py ``` `queues.py` expects Redis. `web.py` expects the FastAPI and Uvicorn dependencies from the generated `pyproject.toml`. ## Post-install Checklist After the first successful run, check these files: - `.env`: local environment values; do not commit secrets from this file. - `config/app.yaml`: application name and environment defaults. - `config/database.yaml`: default database connection and local SQLite path. - `config/log.yaml`: log destination and format. - `config/queue.yaml`: sync or Redis queue selection. - `config/storage.yaml`: local or S3 storage selection. - `pyproject.toml`: optional dependencies you may remove or uncomment. - `tests/TestCase.py`: shared test setup for your app. Then make one small application change and add a test for it. This verifies that the generated project is correctly installed and that imports resolve in your local environment. ## Offline And Restricted Networks `equipment new` needs access to GitHub because it downloads the current template archive. In restricted environments, create the project on a machine with network access and copy the generated project into the restricted environment, or vendor the generated project template internally. After a project exists, normal development does not require the `new` command. Your generated project can be installed from its own `pyproject.toml` like any other Python project. ## Dependency Managers Equipment does not require a specific dependency manager. These are equivalent ways to use it: ```bash python -m pip install equipment ``` ```bash pipenv --python 3.14 pipenv install equipment pipenv run equipment new my-app ``` ```bash poetry add equipment poetry run equipment new my-app ``` ## Troubleshooting Python version error: Equipment requires Python 3.12 or newer and is tested on Python 3.12, 3.13, and 3.14. Check `python --version` or `py -0p` on Windows. `equipment` command not found: Use `python -m pip show equipment` to confirm where it is installed. If the scripts directory is not on `PATH`, use a virtual environment or `pipx`. Project creation fails before files are copied: Check network access to GitHub. The `new` command downloads `https://github.com/rogervila/equipment/archive/refs/heads/main.zip`. Generated project install fails: Run `python -m pip install .` from inside the generated project directory and confirm that `README.md` and `pyproject.toml` are present. Imports fail with `ModuleNotFoundError: app`: Run commands from the generated project root, or install the generated project into the active environment with `python -m pip install .`. The generated code expects the project root to be importable. Redis worker fails to connect: Keep `QUEUE_CONNECTION=sync` until Redis is installed and reachable. Then set `QUEUE_CONNECTION=redis`, configure `REDIS_HOST`, `REDIS_PORT`, and `REDIS_DB`, and run `python queues.py` in a separate process. S3 storage fails: Confirm `FILESYSTEM_DISK=s3` and all `S3_*` variables are present. For local development, prefer `FILESYSTEM_DISK=local` until the bucket and credentials are available. --- ## Generated Directory Structure `equipment new my-app` creates an application scaffold with runtime code, configuration, tests, and project metadata. The structure is intentionally small so it can be understood before it is customized. ```text my-app/ ├── app/ │ ├── __init__.py │ ├── Inspire.py │ └── Scheduler.py ├── config/ │ ├── app.yaml │ ├── database.yaml │ ├── inspiring.json │ ├── log.yaml │ ├── queue.yaml │ ├── storage.yaml │ └── web.yaml ├── database/ │ ├── .gitignore │ └── migrations/ ├── storage/ │ ├── app/ │ └── logs/ ├── tests/ │ ├── TestCase.py │ └── app/test_Inspire.py ├── .coveragerc ├── .editorconfig ├── .env.example ├── .gitignore ├── README.md ├── main.py ├── pyproject.toml ├── queues.py ├── scheduler.py └── web.py ``` ## Application Code `app/__init__.py` defines the generated `App` container. It inherits from `equipment.Equipment` and registers application services with `dependency-injector` singletons. `app/Inspire.py` is a small example service that reads quote data from `config/inspiring.json`. `app/Scheduler.py` defines scheduled tasks for `scheduler.py`. ## File-by-file Reference | Path | Owned By | Purpose | | --- | --- | --- | | `app/__init__.py` | You | Defines the application container and service registrations. | | `app/Inspire.py` | You | Example service; replace or remove when your own services exist. | | `app/Scheduler.py` | You | Defines recurring jobs for the scheduler process. | | `config/app.yaml` | You | App name and environment defaults. | | `config/database.yaml` | You | SQLAlchemy connection configuration. | | `config/inspiring.json` | You | Example data for `Inspire`; safe to remove with the example service. | | `config/log.yaml` | You | Log channels, handlers, levels, and formatters. | | `config/queue.yaml` | You | Queue driver selection and Redis connection values. | | `config/storage.yaml` | You | Local/S3 storage driver settings. | | `config/web.yaml` | You | Host and port used by `web.py`. | | `database/migrations/` | You | Alembic migration environment and migration versions. | | `storage/app/` | Runtime | Local storage root for application-managed files. | | `storage/logs/` | Runtime | Log output directory for file-based handlers. | | `tests/TestCase.py` | You | Shared test setup with Faker and an application container. | | `main.py` | You | Script entry point and examples. | | `queues.py` | You | Redis worker entry point. | | `scheduler.py` | You | Scheduler process entry point. | | `web.py` | You | FastAPI entry point. | | `.env.example` | You | Documented environment variable template. | | `.env` | Local runtime | Local overrides copied from `.env.example`; keep secrets out of Git. | | `pyproject.toml` | You | Generated project metadata and dependencies. | | `README.md` | You | Project-specific instructions for maintainers and users. | ## Configuration The `config/` directory is loaded by filename and extension. Equipment supports `.ini`, `.yaml`, and `.json` files. The generated project uses YAML for application services and JSON for the example quote list. Use `${ENV_NAME:default}` syntax to let `.env` or system environment variables override defaults. ## Entry Points - `main.py`: script entry point and examples for storage, queue, database, and logging. - `scheduler.py`: starts the schedule loop defined in `app/Scheduler.py`. - `queues.py`: starts an RQ worker for Redis-backed queues. - `web.py`: starts the FastAPI example using `config/web.yaml`. ## When To Remove Generated Files The scaffold includes several optional capabilities. You can remove them when a project does not use them: - Remove `web.py` and FastAPI/Uvicorn dependencies if the project is not a web app. - Remove `queues.py` and Redis configuration if the project never uses async workers. - Remove `scheduler.py` and `app/Scheduler.py` if there are no recurring jobs. - Remove Alembic dependencies and `database/migrations/` if the project does not own a relational schema. - Remove `app/Inspire.py` and `config/inspiring.json` after replacing the example service. When removing a feature, remove its tests, config references, and documentation together. That keeps the project understandable for future humans and LLMs. ## Where To Put New Code Use `app/` for application code. For a small project, flat files are fine: ```text app/ ├── __init__.py ├── Billing.py ├── Reports.py └── Scheduler.py ``` For a larger project, organize by domain or responsibility: ```text app/ ├── billing/ │ ├── __init__.py │ ├── models.py │ ├── repository.py │ └── service.py ├── notifications/ │ ├── __init__.py │ └── service.py └── Scheduler.py ``` Register only the services that need container-managed dependencies in `app/__init__.py`. Plain helper functions and data models do not need DI registrations. ## Tests The generated `tests/TestCase.py` is based on `unittest.TestCase`. It creates an app instance, sets `APP_ENV` to `testing`, and exposes `self.fake` from Faker. Run generated tests with: ```bash python -m unittest discover -s tests ``` ## Files Created During Development The scaffold intentionally ignores local runtime files such as `.env`, compiled Python files, coverage output, SQLite files, logs, and virtual environments. This keeps generated projects portable across Unix and Windows. Common generated or local-only files include: - `.env`: local secrets and environment overrides; - `.coverage`: coverage data; - `htmlcov/`: HTML coverage reports; - `*.pyc` and `__pycache__/`: Python bytecode cache; - `database/*.sqlite*`: local SQLite database files; - `storage/logs/*.log`: file log output; - `dist/` and `build/`: compile or packaging outputs; - `.venv/` or `venv/`: virtual environments. Do not rely on these files being present in production. They should be recreated by deployment or runtime setup. ## Safe Customization - Add business logic under `app/`. - Add configuration files under `config/`. - Keep secrets in `.env` or real environment variables. - Keep tests under `tests/` and prefer workflow tests around public entry points. - Use `pathlib` for custom file handling so code works on Windows and Unix. --- ## Common Workflows This page collects practical workflows for building an application with Equipment after the project has been generated. ## Create A New Application Service 1. Add a service class under `app/`. 2. Register it in `app/__init__.py` if it needs framework services or shared lifecycle management. 3. Add tests under `tests/`. 4. Call the service from `main.py`, `web.py`, a queue job, or a scheduled task. Example service: ```python # app/Reports.py from equipment.Storage.AbstractStorage import AbstractStorage from equipment.Log.AbstractLogger import AbstractLogger class Reports: def __init__(self, storage: AbstractStorage, log: AbstractLogger): self.storage = storage self.log = log def write_daily_report(self, content: str) -> str: path = "reports/daily.txt" self.storage.write(path, content) self.log.info("Daily report written", extra={"path": path}) return path ``` Register it: ```python # app/__init__.py from dependency_injector.providers import ThreadSafeSingleton as Singleton from equipment import Equipment from app.Reports import Reports class App(Equipment): reports = Singleton(Reports, Equipment.storage, Equipment.log) ``` Use it: ```python from app import app application = app() application.reports().write_daily_report("Ready") ``` ## Add Configuration For A Service Create `config/reports.yaml`: ```yaml reports: output_path: ${REPORTS_OUTPUT_PATH:reports/daily.txt} enabled: ${REPORTS_ENABLED:true} ``` Inject the config value: ```python class App(Equipment): reports = Singleton( Reports, Equipment.storage, Equipment.log, Equipment.config.reports.output_path, ) ``` Use environment variables in `.env` for machine-specific values: ```env REPORTS_OUTPUT_PATH=reports/local-daily.txt ``` ## Add A Database-backed Feature 1. Define SQLAlchemy models in your application code. 2. Add or update Alembic migration metadata. 3. Create a migration under `database/migrations`. 4. Run the migration locally. 5. Add repository/service tests. Typical commands: ```bash cd database/migrations alembic revision --autogenerate -m "create invoices table" alembic upgrade head ``` Use sessions carefully: ```python session = application.database().session() try: session.add(invoice) session.commit() except Exception: session.rollback() raise finally: session.close() ``` ## Add A Queued Task Put queued functions at module scope so Redis/RQ can import them: ```python # app/jobs.py from app import app def rebuild_report(report_id: int) -> None: application = app() application.log().info("Rebuilding report", extra={"report_id": report_id}) ``` Queue it from a script or route: ```python from app import app from app.jobs import rebuild_report application = app() application.queue().push(rebuild_report, 123) ``` Local development can keep `QUEUE_CONNECTION=sync`. Production worker mode usually sets `QUEUE_CONNECTION=redis` and runs: ```bash python queues.py ``` ## Add A Scheduled Task Add scheduled work in `app/Scheduler.py`: ```python from app.jobs import rebuild_report class Scheduler(Equipment): def run(self) -> None: self.schedule.every().day.at("03:00").do( self.queue.push, rebuild_report, 123, ) super().run() ``` Run it with: ```bash python scheduler.py ``` Use the queue for slow work so the scheduler loop stays responsive. ## Add A FastAPI Route Add routes in `web.py` for small apps, or move them into a router module for larger apps. ```python from fastapi import APIRouter from app import app router = APIRouter() application = app() @router.get("/health") def health() -> dict[str, str]: return {"status": "ok", "app": application.config.app.name()} ``` Then include the router from `web.py`: ```python from app.routes import router web.include_router(router) ``` Keep route functions thin. Put business logic in services registered on the `App` container. ## Add Tests For A Workflow Use the generated `TestCase` when a test needs an application container: ```python from tests.TestCase import TestCase class ReportsTest(TestCase): def test_report_is_written_to_storage(self): path = self.app.reports().write_daily_report("Ready") self.assertTrue(self.app.storage().exists(path)) self.assertEqual("Ready", self.app.storage().read(path)) ``` Use `unittest.mock` for external systems that should not run in unit tests: ```python from unittest.mock import Mock self.app.storage.override(Mock()) ``` ## Prepare For Deployment Before deployment, decide these values explicitly: - `APP_ENV`: environment name such as `production`, `staging`, or `local`. - `LOG_CHANNEL`: `console`, `stack`, `single`, `daily`, `sqlite`, or `null`. - `DB_CONNECTION`: `sqlite`, `mysql`, or `postgresql`. - `QUEUE_CONNECTION`: `sync` or `redis`. - `FILESYSTEM_DISK`: `local` or `s3`. - `WEB_HOST` and `PORT`: web server binding values. Run a deployment-style smoke test: ```bash python -m pip install . python -m unittest discover -s tests python main.py ``` If you compile the project, smoke test the compiled output too: ```bash equipment compile dist cd dist python main.pyc ``` ## Upgrade Dependencies Safely Dependency upgrades should be their own change. Before upgrading, run the current tests to establish a baseline. After upgrading, run: ```bash python -m coverage run -m unittest discover -s tests python -m coverage report cd project python -m pip install . python -m unittest discover -s tests ``` Pay close attention to SQLAlchemy, boto3/botocore/moto, redis/rq, python-json-logger, dependency-injector, and FastAPI/Uvicorn upgrades because Equipment integrates directly with those packages. --- ## Dependency Injection Equipment uses `dependency-injector` to keep service creation explicit and reusable. The framework container provides shared services, and the generated project adds application-specific services on top. ## Runtime Container The base `Equipment` container loads configuration and exposes these singleton providers: - `log`: configured logger factory; - `queue`: sync or Redis queue factory; - `storage`: local or S3 storage factory; - `database`: SQLAlchemy factory. The generated `App` class is the application composition root. A composition root is the place where concrete services are assembled. Keeping service registration in one file makes the dependency graph easy to inspect and easy for LLMs to reason about. Generated projects subclass the base container in `app/__init__.py`: ```python from dependency_injector.providers import ThreadSafeSingleton as Singleton from equipment import Equipment from app.Inspire import Inspire from app.Scheduler import Scheduler class App(Equipment): inspiring = Singleton(Inspire, Equipment.config.inspiring.quotes) scheduler = Singleton( Scheduler, Equipment.log, Equipment.queue, inspiring, ) def app(base_path: str | None = None) -> App: return App.make(base_path) ``` ## Access Services ```python from app import app application = app() application.log().info("Application started") quote = application.inspiring().quote() application.storage().write("quote.txt", quote) ``` ## Add Your Own Service Define a class in `app/` and register it in `App`: ```python from dependency_injector.providers import ThreadSafeSingleton as Singleton from equipment import Equipment from app.Reports import Reports class App(Equipment): reports = Singleton( Reports, Equipment.database, Equipment.storage, Equipment.log, ) ``` Prefer constructor injection. It keeps dependencies visible, easier to test, and easier for LLMs to follow. ## Constructor Injection Pattern Prefer this pattern: ```python class InvoiceService: def __init__(self, database, log): self.database = database self.log = log def create_invoice(self, customer_id: int) -> int: self.log.info("Creating invoice", extra={"customer_id": customer_id}) # Use self.database here. return 1 ``` Register it once: ```python class App(Equipment): invoices = Singleton(InvoiceService, Equipment.database, Equipment.log) ``` Then use it from entry points: ```python from app import app application = app() invoice_id = application.invoices().create_invoice(customer_id=42) ``` Avoid this pattern inside business services: ```python class InvoiceService: def create_invoice(self, customer_id: int) -> int: from app import app application = app() application.log().info("Creating invoice") return 1 ``` The second version hides dependencies, makes tests more difficult, and can create surprising container instances when called from workers or scripts. ## Provider Lifecycle Generated services use `ThreadSafeSingleton`. The first call creates the service, and later calls reuse it. This is useful for services that hold references to framework factories such as logging, database, storage, or queue providers. Use singleton services for: - stateless business services; - repositories that use shared framework factories; - adapters around external systems; - scheduler classes; - services that are safe to reuse between calls. Avoid storing request-specific mutable state on singleton services. In web apps, request data should live in function parameters, local variables, database rows, or dedicated request-scoped objects that you create manually. ## Passing Configuration Into Services You can inject config values directly: ```python class Reports: def __init__(self, storage, output_path: str): self.storage = storage self.output_path = output_path class App(Equipment): reports = Singleton( Reports, Equipment.storage, Equipment.config.reports.output_path, ) ``` The injected config provider is evaluated when the service is created. If tests need a different value, override the config before calling the service for the first time: ```python self.app.config.reports.output_path.from_value("test-report.txt") report = self.app.reports() ``` ## Testing Overrides Tests can override providers when a service should be replaced with a fake or mock: ```python from unittest.mock import Mock from tests.TestCase import TestCase class ReportsTest(TestCase): def test_report_uses_storage(self): fake_storage = Mock() self.app.storage.override(fake_storage) self.app.reports().run() fake_storage.write.assert_called() ``` When overriding providers, keep the override local to the test. `unittest` creates a new `TestCase` instance for each method, but singleton providers can still hold created objects. For tests that override core providers, reset the provider or create a fresh app base path if the test needs strict isolation. ## Designing Services For Workers And Schedulers Queue workers and schedulers run in separate processes. Services should therefore be importable from module scope and should not depend on local state created only in `main.py`. Good worker-friendly pattern: ```python # app/jobs.py from app import app def send_invoice(invoice_id: int) -> None: application = app() application.invoices().send(invoice_id) ``` The queued function creates its own application container in the worker process and delegates to a service. That keeps the queued function small and lets tests target `InvoiceService` directly. ## Naming Conventions - Use lowercase provider names such as `reports`, `invoices`, or `mailer`. - Use class names for service classes such as `Reports`, `InvoiceService`, or `Mailer`. - Keep provider names stable because entry points and tests may call them directly. - Group related providers together in `app/__init__.py` when the application grows. ## Troubleshooting `AttributeError` when accessing a service: The service is not registered on the generated `App` class, or the entry point imported the wrong `app` object. Configuration value is missing: Confirm the config file exists under `config/`, the top-level key matches the filename, and the application is running from the project root or an explicit base path. Service uses stale config in a test: The singleton may have already been created. Override config before first access, or reset the provider before creating the service again. ## Guidance - Register long-lived services as `ThreadSafeSingleton` providers. - Keep application registrations in `app/__init__.py` so the container stays discoverable. - Pass framework services into constructors instead of importing a global app object inside business logic. - Reset or override providers in tests when a dependency touches the filesystem, network, database, or queue. - Keep singleton services stateless with respect to per-request data. - Put process entry-point logic in `main.py`, `web.py`, `queues.py`, or `scheduler.py`, and reusable behavior in registered services. --- ## Configuration Equipment loads configuration from a project base path. It first loads `.env` when present, then merges files from `config/*.ini`, `config/*.yaml`, and `config/*.json`. Generated projects use YAML for framework settings and JSON for the example quote data. ## Loading Order The base path is the directory passed to `app(base_path)` or the current working directory when no base path is provided. Equipment then loads: 1. `.env` from the base path, if it exists. 2. `config/*.ini` files. 3. `config/*.yaml` files. 4. `config/*.json` files. 5. `config.base_path`, which is set to the resolved base path. The generated app relies on this order so environment variables are available before YAML and JSON config values are read. ## Environment Interpolation Configuration values can use `${VARIABLE:default}` syntax: ```yaml app: name: ${APP_NAME:Equipment} env: ${APP_ENV:local} ``` Set values in `.env` for local development or in the real process environment for production. The value before the colon is the environment variable name. The value after the colon is the default. If the variable is not set and no default is provided, the loaded value may be empty or unresolved depending on the underlying config loader. Examples: ```yaml app: env: ${APP_ENV:local} database: connection: ${DB_CONNECTION:sqlite} web: port: ${PORT:8000} ``` Environment values are loaded as strings. Convert them when a library expects another type: ```python port = int(application.config.web.port()) debug = str(application.config.app.debug()).lower() == "true" ``` ## Generated Config Files | File | Purpose | | --- | --- | | `config/app.yaml` | Application name and environment. | | `config/database.yaml` | SQLAlchemy connection selection and database settings. | | `config/log.yaml` | Log level, channel, handlers, and JSON formatter. | | `config/queue.yaml` | `sync` or `redis` queue driver settings. | | `config/storage.yaml` | `local` or `s3` storage disk settings. | | `config/web.yaml` | FastAPI host and port. | | `config/inspiring.json` | Example quote data used by `app/Inspire.py`. | ## Environment Variable Reference | Variable | Default | Used By | Meaning | | --- | --- | --- | --- | | `APP_NAME` | `Equipment` | `config/app.yaml` | Human-readable application name. | | `APP_ENV` | `local` | `config/app.yaml` | Environment name such as `local`, `testing`, `staging`, or `production`. | | `LOG_LEVEL` | `debug` | `config/log.yaml` | Python logging level. | | `LOG_CHANNEL` | `stack` | `config/log.yaml` | Active logging channel. | | `DB_CONNECTION` | `sqlite` | `config/database.yaml` | Active database connection key. | | `DB_HOST` | `127.0.0.1` | `config/database.yaml` | MySQL/PostgreSQL host. | | `DB_PORT` | `3306` or `5432` | `config/database.yaml` | MySQL/PostgreSQL port. | | `DB_DATABASE` | connection-specific | `config/database.yaml` | SQLite path or database name. | | `DB_USERNAME` | connection-specific | `config/database.yaml` | MySQL/PostgreSQL username. | | `DB_PASSWORD` | connection-specific | `config/database.yaml` | MySQL/PostgreSQL password. | | `DB_CHARSET` | `utf8mb4` | `config/database.yaml` | MySQL charset. | | `QUEUE_CONNECTION` | `sync` | `config/queue.yaml` | Active queue driver. | | `REDIS_HOST` | `127.0.0.1` | `config/queue.yaml` | Redis host. | | `REDIS_PORT` | `6379` | `config/queue.yaml` | Redis port. | | `REDIS_DB` | `0` | `config/queue.yaml` | Redis database number. | | `REDIS_USERNAME` | `null` | `config/queue.yaml` | Redis username when required. | | `REDIS_PASSWORD` | `null` | `config/queue.yaml` | Redis password when required. | | `FILESYSTEM_DISK` | `local` | `config/storage.yaml` | Active storage disk. | | `S3_ENDPOINT` | none | `config/storage.yaml` | S3-compatible endpoint. | | `S3_BUCKET` | none | `config/storage.yaml` | S3 bucket name. | | `S3_ACCESS_KEY` | none | `config/storage.yaml` | S3 access key. | | `S3_SECRET_KEY` | none | `config/storage.yaml` | S3 secret key. | | `S3_REGION` | `auto` | `config/storage.yaml` | S3 region. | | `S3_PREFIX` | `null` | `config/storage.yaml` | Optional object key prefix. | | `WEB_HOST` | `0.0.0.0` | `config/web.yaml` | Host passed to Uvicorn. | | `PORT` | `8000` | `config/web.yaml` | Port passed to Uvicorn. | ## App Settings ```yaml app: name: ${APP_NAME:Equipment} env: ${APP_ENV:local} ``` ## Database Settings ```yaml database: connection: ${DB_CONNECTION:sqlite} connections: sqlite: schema: sqlite database: "${DB_DATABASE:database/database.sqlite}" ``` MySQL and PostgreSQL examples are present in the generated file. Uncomment or install the matching optional driver in the generated `pyproject.toml` before using them. ## Queue Settings ```yaml queue: connection: ${QUEUE_CONNECTION:sync} connections: redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379} db: ${REDIS_DB:0} ``` Use `sync` for local development and simple scripts. Use `redis` when work should be processed by `queues.py`. ## Storage Settings ```yaml storage: disk: ${FILESYSTEM_DISK:local} disks: local: path: storage/app s3: endpoint: ${S3_ENDPOINT} bucket: ${S3_BUCKET} access_key: ${S3_ACCESS_KEY} secret_key: ${S3_SECRET_KEY} region: ${S3_REGION:auto} prefix: ${S3_PREFIX:null} ``` The local storage key is `path`, not `root`. ## Add Custom Config Add a new file under `config/`, for example `config/services.yaml`: ```yaml services: api_base_url: ${API_BASE_URL:https://example.test} ``` Then access it through the loaded config: ```python application = app() base_url = application.config.services.api_base_url() ``` ## Config File Shape Use a top-level key that matches the filename. This makes the loaded config predictable: ```yaml # config/billing.yaml billing: currency: ${BILLING_CURRENCY:EUR} invoice_prefix: ${BILLING_INVOICE_PREFIX:INV} ``` Access it as: ```python currency = application.config.billing.currency() prefix = application.config.billing.invoice_prefix() ``` Nested structures are supported: ```yaml notifications: email: enabled: ${EMAIL_ENABLED:false} sender: ${EMAIL_SENDER:no-reply@example.test} ``` Access nested values as chained attributes: ```python sender = application.config.notifications.email.sender() ``` ## Environment Profiles The generated project uses a single config directory with environment interpolation rather than separate `config/local`, `config/production`, and `config/testing` directories. This keeps the project smaller and makes defaults obvious. A typical local `.env` might be: ```env APP_ENV=local LOG_CHANNEL=stack DB_CONNECTION=sqlite QUEUE_CONNECTION=sync FILESYSTEM_DISK=local ``` A production environment might set: ```env APP_ENV=production LOG_CHANNEL=console DB_CONNECTION=postgresql QUEUE_CONNECTION=redis FILESYSTEM_DISK=s3 ``` Do not commit production secrets. Keep `.env.example` as documentation and inject real values through your deployment platform. ## Testing Config The generated `tests/TestCase.py` sets `self.app.config.app.env` to `testing`. Tests can also override any config value explicitly: ```python self.app.config.queue.connection.from_value("sync") self.app.config.storage.disk.from_value("local") ``` Override config before creating services that depend on those values, because singleton services may cache the first created instance. ## Troubleshooting Config attribute does not exist: Check the filename, extension, and top-level key. `config/reports.yaml` should usually define `reports:`. Environment default is ignored: Confirm `.env` is located at the app base path and that the process starts from the generated project root. You can also pass a base path explicitly with `app(base_path="/path/to/project")`. Boolean or integer config behaves like a string: Cast values before passing them to libraries. Environment variables are text. ## Guidance - Keep secrets out of committed config files. - Prefer defaults that let local tests start without external services. - Keep config file names stable; they become attributes on `application.config`. - Use strings for environment-derived values and cast at the call site when a library needs `int` or `bool`. - Add new config files for new application services instead of overloading `app.yaml`. - Keep `.env.example` synchronized with documented environment variables. --- ## Database Equipment wraps SQLAlchemy setup in `SQLAlchemyFactory`. The generated project defaults to SQLite so the scaffold can run locally without an external database. The database component exposes a SQLAlchemy `engine`, a `session()` factory, and a `text()` helper. Equipment does not define your ORM models. Your application owns table definitions, migrations, repositories, and transaction boundaries. ## Configuration `config/database.yaml` selects the active connection: ```yaml database: connection: ${DB_CONNECTION:sqlite} connections: sqlite: schema: sqlite database: "${DB_DATABASE:database/database.sqlite}" ``` The generated file also includes MySQL and PostgreSQL examples. Those require optional drivers in the generated `pyproject.toml`, such as `mysql-connector-python` or `psycopg2-binary`. ## Connection URLs Equipment builds SQLAlchemy URLs from config values: - SQLite file: `sqlite:///BASE_PATH/database/database.sqlite` - SQLite memory: `sqlite://` - MySQL: `mysql+mysqlconnector://user:pass@host:port/database?charset=utf8mb4` - PostgreSQL: `postgresql+psycopg2://user:pass@host:port/database` The generated SQLite database path is relative to the project base path. This keeps local development portable across Unix and Windows. ## SQLite Local Development For the fastest local setup, use: ```env DB_CONNECTION=sqlite DB_DATABASE=database/database.sqlite ``` SQLite files are local runtime artifacts and should not be committed. Use migrations to recreate schema. ## MySQL Configuration Install or uncomment the matching generated dependency before using MySQL. The generated config uses the `mysql+mysqlconnector` SQLAlchemy dialect: ```toml dependencies = [ "mysql-connector-python>=9.1,<10", ] ``` ```env DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=equipment DB_USERNAME=equipment DB_PASSWORD=equipment DB_CHARSET=utf8mb4 ``` ## PostgreSQL Configuration Install or uncomment a PostgreSQL driver before using PostgreSQL: ```toml dependencies = [ "psycopg2-binary>=2.9,<3", ] ``` ```env DB_CONNECTION=postgresql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=equipment DB_USERNAME=equipment DB_PASSWORD=equipment ``` ## Raw SQL Use `app.database().text()` for SQLAlchemy text queries: ```python from app import app application = app() with application.database().engine.connect() as connection: result = connection.execute( application.database().text("SELECT * FROM todos ORDER BY id DESC LIMIT 1") ) latest = result.mappings().first() ``` Use bind parameters for user-provided values: ```python result = connection.execute( application.database().text("SELECT * FROM todos WHERE id = :todo_id"), {"todo_id": todo_id}, ) ``` Avoid formatting user values directly into SQL strings. ## ORM Sessions ```python from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.orm import declarative_base from app import app Base = declarative_base() class Todo(Base): __tablename__ = "todos" id = Column(Integer, primary_key=True) title = Column(String(255), nullable=False) completed = Column(Boolean, nullable=False) application = app() session = application.database().session() try: session.add(Todo(title="Learn Equipment", completed=False)) session.commit() except Exception: session.rollback() raise finally: session.close() ``` ## Repository Pattern For larger apps, keep SQLAlchemy calls in repository classes and call repositories from services: ```python class TodoRepository: def __init__(self, database): self.database = database def latest(self): with self.database.engine.connect() as connection: result = connection.execute( self.database.text("SELECT * FROM todos ORDER BY id DESC LIMIT 1") ) return result.mappings().first() ``` Register the repository in `app/__init__.py`: ```python todos = Singleton(TodoRepository, Equipment.database) ``` This keeps database details out of routes, queue jobs, and scheduler definitions. ## Migrations The generated project includes an Alembic environment under `database/migrations`. Create a migration: ```bash cd database/migrations alembic revision --autogenerate -m "create todos table" ``` Apply migrations: ```bash cd database/migrations alembic upgrade head ``` ## Migration Workflow Use this loop for schema changes: 1. Change or add ORM model metadata. 2. Generate an Alembic revision. 3. Inspect the generated migration file before running it. 4. Apply it locally. 5. Run tests. 6. Apply it in staging/production with deployment controls. Do not rely blindly on autogenerated migrations. Always read the migration to confirm indexes, constraints, nullable changes, defaults, and destructive operations. ## Transactions Keep transaction boundaries explicit. A good service method either completes fully or rolls back: ```python def create_todo(application, title: str): session = application.database().session() try: todo = Todo(title=title, completed=False) session.add(todo) session.commit() return todo.id except Exception: session.rollback() raise finally: session.close() ``` For request-heavy web applications, consider using FastAPI dependencies or context managers around session lifecycle so every request closes its session. ## Testing Database Code Prefer fast tests with SQLite when logic is database-agnostic. Use separate integration tests for MySQL or PostgreSQL-specific SQL, constraints, and driver behavior. Common test patterns: - use SQLite `:memory:` for unit-level repository tests; - use a temporary SQLite file when migrations or file paths matter; - skip MySQL/PostgreSQL tests unless required drivers and services are available; - seed only the rows needed for the behavior under test; - close sessions in `finally` blocks. ## Troubleshooting Unknown connection type: `DB_CONNECTION` must match a key under `database.connections`. SQLite file is created in an unexpected place: Check the application base path. Relative SQLite paths are resolved from the base path. Driver import error: Install the optional driver dependency for the selected database. The generated `pyproject.toml` includes commented examples. Locked SQLite database: Close sessions and connections promptly. SQLite is convenient for local development but has different concurrency behavior from server databases. ## Testing And Maintenance - Use SQLite `:memory:` for fast unit tests when possible. - Keep MySQL and PostgreSQL tests optional unless CI provides those services. - Run database URL and session tests before upgrading SQLAlchemy. - Do not commit local SQLite database files. - Keep business logic outside migration files. - Review autogenerated Alembic migrations before applying them. - Prefer bind parameters for user input in raw SQL. --- ## Logging Equipment builds logging on top of the Python standard library. `LoggerFactory` reads `config/log.yaml`, creates handlers, and exposes familiar methods such as `debug`, `info`, `warning`, `error`, and `critical` through `application.log()`. Use the logger from scripts, services, web routes, queue jobs, and scheduled tasks. Application code should not need to know whether logs go to stdout, a file, a rotating file, or SQLite. That is a configuration decision. ## Configuration ```yaml log: level: ${LOG_LEVEL:debug} channel: ${LOG_CHANNEL:stack} channels: stack: channels: - single - console single: formatter: json filename: 'storage/logs/app.log' daily: formatter: json filename: 'storage/logs/app.log' when: 'midnight' interval: 1 backupCount: 7 console: formatter: null stream: null sqlite: filename: 'storage/logs/app.sqlite' table_name: logs formatters: json: format: '%(message)s %(asctime)s %(levelname)s %(levelno)d %(pathname)s %(lineno)d' indent: null ``` ## Channels | Channel | Behavior | Typical Use | | --- | --- | --- | | `stack` | Sends one log record to multiple configured channels. | Local development where console and file logs are both useful. | | `single` | Writes to one file. | Small deployments or local debugging. | | `daily` | Writes to a timed rotating file. | Single-server deployments where local files are retained briefly. | | `console` | Writes to standard output or configured stream. | Containers, PaaS, CI, and production log collection. | | `sqlite` | Writes to a SQLite table. | Local inspection or small internal tools. | | `null` | Emits no visible logs. | Tests or intentionally quiet scripts. | Set `LOG_CHANNEL=null` to use a null handler. ## Channel Selection | Environment | Suggested Channel | Why | | --- | --- | --- | | Unit tests | `null` or `NullLogger` override | Avoid noisy output and file locks. | | Local scripts | `stack` | See output immediately and retain a local log file. | | Docker or PaaS | `console` | Let the platform collect logs. | | One VM | `daily` | Keep rotating local files without extra infrastructure. | | Debugging SQLite logging | `sqlite` | Query logs with SQL locally. | ## Usage ```python from app import app application = app() application.log().info("Application started") application.log().warning("Cache miss", extra={"key": "homepage"}) try: raise RuntimeError("example") except RuntimeError: application.log().error("Operation failed", exc_info=True) ``` ## Log Levels - `debug`: detailed developer diagnostics. - `info`: normal operational lifecycle events. - `warning`: unexpected but recoverable situations. - `error`: failed operation that needs attention. - `critical`: severe failure that may require immediate intervention. Avoid using `error` for expected validation failures. Reserve high-severity logs for conditions that operators should investigate. ## Structured Context Use `extra` for stable, machine-readable context: ```python application.log().info( "Invoice created", extra={"invoice_id": invoice_id, "customer_id": customer_id}, ) ``` When a JSON formatter is active, extra fields can be included by log processors depending on formatter configuration. Do not log secrets, access tokens, passwords, private keys, or personally identifiable information. ## JSON Formatting The generated formatter includes message, timestamp, level, path, and line number: ```yaml formatters: json: format: '%(message)s %(asctime)s %(levelname)s %(levelno)d %(pathname)s %(lineno)d' indent: null ``` Useful `LogRecord` fields include `message`, `asctime`, `levelname`, `levelno`, `pathname`, `lineno`, `name`, `module`, `funcName`, `process`, and `threadName`. ## File Handlers File-based handlers expect their parent directories to exist. The generated project includes `storage/logs/` for this reason. ```yaml single: formatter: json filename: 'storage/logs/app.log' ``` If you change the log path, create the directory before the application starts. ## SQLite Logging The `sqlite` channel writes records to a SQLite database file: ```yaml sqlite: filename: 'storage/logs/app.sqlite' table_name: logs ``` SQLite logging is useful for local inspection but is not a high-volume production logging system. For production, prefer console JSON logs and let the platform collect them. ## Testing Logs Tests can override logging with `NullLogger`: ```python from equipment.Log.NullLogger import NullLogger self.app.log.override(NullLogger()) ``` Tests that assert file logs should close handlers before reading or deleting files. This matters on Windows because open file handles can block cleanup. ## Troubleshooting No logs appear: Check `LOG_CHANNEL`, `LOG_LEVEL`, and whether the app is running from the expected base path. `LOG_CHANNEL=null` intentionally disables output. File log is not created: Confirm the parent directory exists and the process has write permission. Duplicate log lines appear: Check whether custom code has added handlers to Python loggers. Equipment clears handlers when creating its logger, but direct logger modifications can still create duplicates. JSON output is missing fields: Update `formatters.json.format` to include the fields you need. ## Guidance - Do not log secrets, credentials, tokens, or personally identifiable information. - Use `stack` locally when you want both console and file output. - Use JSON in production when logs are collected by another system. - Ensure directories such as `storage/logs` exist before file logging. - Use `extra` for IDs and stable attributes. - Keep log volume reasonable in scheduled jobs and queues. - Run logging handler tests before upgrading `python-json-logger` or `python_sqlite_log_handler`. --- ## Queue Equipment provides a queue abstraction with two drivers: `sync` and `redis`. Use queues when work should happen outside the current request, script, or scheduler loop. Common examples include sending emails, generating reports, importing files, processing uploads, retrying webhooks, rebuilding caches, and running expensive cleanup tasks. ## Configuration ```yaml queue: connection: ${QUEUE_CONNECTION:sync} connections: sync: redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379} db: ${REDIS_DB:0} username: ${REDIS_USERNAME:null} password: ${REDIS_PASSWORD:null} ``` ## Drivers | Driver | Behavior | Use Case | | --- | --- | --- | | `sync` | Runs work immediately in the current process. | Local development, deterministic tests, simple scripts. | | `redis` | Enqueues jobs into Redis through RQ. | Background workers, web requests that should return quickly, scheduled jobs. | ## Sync Driver ```python from app import app application = app() def send_email(address: str) -> None: application.log().info("Sending email", extra={"address": address}) application.queue().push(send_email, "user@example.com") ``` With the sync driver, `send_email` runs before `push` returns. This makes tests easy to reason about, but it is not real background processing. ## Redis Driver Set `QUEUE_CONNECTION=redis`, start Redis, and run a worker: ```bash python queues.py ``` On Windows PowerShell: ```powershell $env:QUEUE_CONNECTION = "redis" python queues.py ``` Push jobs from application code: ```python application.queue().push(send_email, "user@example.com") ``` Schedule a job for later: ```python from datetime import datetime, timedelta run_at = datetime.now() + timedelta(minutes=10) application.queue().push_at(run_at, send_email, "user@example.com") ``` ## Task Function Design RQ workers must be able to import queued functions. Prefer module-level functions: ```python # app/jobs/reports.py from app import app def rebuild_report(report_id: int) -> None: application = app() application.reports().rebuild(report_id) ``` Queue it by passing the function object: ```python from app.jobs.reports import rebuild_report application.queue().push(rebuild_report, 123) ``` Avoid lambdas, nested functions, open file handles, active database sessions, request objects, container instances, and large in-memory objects as queued arguments. Pass stable identifiers and load fresh data inside the worker. ## Idempotency Queued tasks may run more than once after retries, worker crashes, deployment restarts, or network interruptions. Design tasks so a repeat run is safe. Useful patterns: - check whether a record has already been processed; - use database uniqueness constraints; - write output to deterministic paths; - use idempotency keys for external APIs that support them; - log task start and finish with stable IDs. ## Error Handling `push()` returns `True` or `False` for enqueueing/execution at the queue layer. In sync mode, exceptions raised by the function are caught, logged, and result in `False`. In Redis mode, enqueueing errors are caught, but task failures happen in worker processes and are managed by RQ. Check worker logs for task exceptions. ## Queue From FastAPI ```python @web.post("/reports/{report_id}/rebuild") def rebuild(report_id: int) -> dict[str, str]: application.queue().push(rebuild_report, report_id) return {"status": "queued"} ``` With `sync`, this executes immediately. With `redis`, the response can return after enqueueing. ## Queue From Scheduler ```python self.schedule.every().hour.do(self.queue.push, rebuild_report, 123) ``` Schedulers should enqueue slow work instead of running it inline. ## Deployment Checklist - Redis is reachable from both producer processes and worker processes. - `QUEUE_CONNECTION=redis` is set where background behavior is expected. - Worker processes run `python queues.py`. - Task functions are importable from module scope. - Task arguments are serializable and small. - Logs include task IDs or domain IDs for debugging. ## Testing Queued Work Test business logic directly where possible. Test queue integration separately. ```python called = [] def job(value): called.append(value) self.assertTrue(self.app.queue().push(job, 42)) self.assertEqual([42], called) ``` Use `sync` in most tests. Run Redis-backed tests only when Redis is available in CI or local development. ## Troubleshooting Task runs immediately instead of in the background: `QUEUE_CONNECTION` is probably `sync`. Task never runs: Check Redis connectivity, worker process logs, and whether the function can be imported by the worker. Task works in sync mode but not Redis mode: Check serialization and importability. Pass IDs rather than objects. Delayed tasks do not run when expected: Confirm worker support for scheduled jobs and check server time assumptions. ## Guidance - Keep queued callables importable and stable. - Prefer small serializable arguments such as IDs. - Use idempotent task logic so retries are safe. - Use `sync` in tests unless specifically testing Redis integration. - Do not pass active database sessions, file handles, request objects, or dependency-injector providers as job payloads. --- ## Scheduler Equipment uses the `schedule` library for recurring tasks. The generated `scheduler.py` entry point creates the app and calls `app.scheduler().run()`. Use the scheduler for periodic application-level work: reports, cleanup, polling, cache refreshes, and enqueueing background jobs. The scheduler is a long-running process and should be started separately from `main.py`, `web.py`, and `queues.py`. ## Generated Scheduler Scheduled work is defined in `app/Scheduler.py`: ```python from equipment.Scheduler.Scheduler import Scheduler as Equipment class Scheduler(Equipment): def run(self) -> None: self.schedule.every(1).seconds.do( lambda: self.log.debug(self.inspiring.quote()) ) self.schedule.every(5).seconds.do( self.queue.push, _inspire, self.log, self.inspiring, ) super().run() ``` Run it with: ```bash python scheduler.py ``` Real applications should choose intervals that match the work being done. ## Common Patterns ```python def cleanup() -> None: pass class Scheduler(Equipment): def run(self) -> None: self.schedule.every(10).minutes.do(cleanup) self.schedule.every().day.at("03:00").do(cleanup) self.schedule.every().monday.do(cleanup) self.schedule.every().hour.do(cleanup) self.schedule.every(5).to(10).minutes.do(cleanup) super().run() ``` Always call `super().run()` after registering jobs. ## Queue Integration Long-running scheduled work should be pushed to the queue: ```python self.schedule.every().hour.do(self.queue.push, rebuild_report, report_id) ``` With `QUEUE_CONNECTION=sync`, this still runs immediately. With `QUEUE_CONNECTION=redis`, the work is handled by `queues.py`. ## Process Model The scheduler loop: 1. logs startup; 2. calls `self.schedule.run_pending()`; 3. sleeps briefly; 4. repeats until interrupted or `should_exit` is set. Run one scheduler process when a job must happen once globally. If multiple scheduler replicas are active, each replica may run or enqueue the same job. ## Scheduler-safe Jobs Good scheduler jobs are: - short; - idempotent; - logged; - exception-safe; - independent of terminal state; - safe to run again after a restart. For slow work, enqueue a task: ```python def enqueue_report_rebuild(queue, report_id: int) -> None: queue.push(rebuild_report, report_id) ``` ## Environment-specific Scheduling ```python class Scheduler(Equipment): def run(self) -> None: if self.config.app.env() != "production": self.log.info("Skipping production-only jobs") super().run() return self.schedule.every().day.at("03:00").do(self.queue.push, rebuild_report, 123) super().run() ``` If scheduling behavior grows, add `config/scheduler.yaml` and inject those values into the scheduler. ## Testing Scheduler Code Avoid tests that sleep. Patch `schedule.Scheduler.run_pending` or set `scheduler.should_exit = True` after one loop. Test the task function separately from schedule registration. ## Deployment Checklist - Decide whether the scheduler should run as one process or many. - Use Redis queues for long tasks. - Log job start and completion with stable IDs. - Document timezone assumptions. - Monitor the scheduler process like any other worker. - Avoid schedules that run more often than the task can complete. ## Troubleshooting Scheduled task never runs: Confirm `python scheduler.py` is running, the job is registered before `super().run()`, and the system clock/timezone matches the schedule. Scheduler blocks: A job is probably doing long-running work inline. Push that work to the queue. Task runs multiple times: Multiple scheduler processes may be active, or the schedule interval may be too frequent. ## Guidance - Keep scheduler jobs short or delegate to the queue. - Handle exceptions inside task functions. - Account for local machine time and timezone. - Use focused tests with patched sleep/run-pending behavior. - Avoid multiple scheduler replicas unless jobs are idempotent or externally locked. --- ## Storage Equipment provides a storage abstraction with local filesystem and S3-compatible drivers. Application code can call the same methods regardless of the configured disk. Use storage for application-managed files: generated reports, uploads, exports, temporary artifacts, cache files, and files that may move from local disk to S3 later. ## Configuration ```yaml storage: disk: ${FILESYSTEM_DISK:local} disks: local: path: storage/app s3: endpoint: ${S3_ENDPOINT} bucket: ${S3_BUCKET} access_key: ${S3_ACCESS_KEY} secret_key: ${S3_SECRET_KEY} region: ${S3_REGION:auto} prefix: ${S3_PREFIX:null} ``` The local driver uses `path`, for example `storage/app`. ## API | Method | Behavior | | --- | --- | | `path(file)` | Return the local absolute path or S3 key. | | `write(file, data)` | Write string data. Returns `True` or `False`. | | `read(file)` | Read string data. Raises `FileNotFoundError` when missing. | | `exists(file)` | Return whether the file exists. | | `remove(file)` | Delete a file. Returns `True` or `False`. | | `move(source, destination)` | Move or rename a file. Returns `True` or `False`. | | `list(path)` | Return files directly under a directory or prefix. | Pass relative paths. Avoid leading slashes, drive letters, and user-controlled `..` segments. ## Usage ```python from app import app application = app() storage = application.storage() storage.write("reports/today.txt", "ready") if storage.exists("reports/today.txt"): content = storage.read("reports/today.txt") application.log().info(content) storage.move("reports/today.txt", "reports/archive/today.txt") storage.remove("reports/archive/today.txt") ``` ## Structured Data Storage methods read and write strings. Serialize structured data explicitly: ```python payload = {"status": "ready", "count": 3} application.storage().write("reports/status.json", json.dumps(payload)) loaded = json.loads(application.storage().read("reports/status.json")) ``` For binary files, encode data before writing or extend the storage abstraction in your application. ## Local Driver The local driver stores files under `base_path / config.storage.disks.local.path`. In the generated project, that is `storage/app`. Local storage is a good default for: - local development; - tests; - single-machine scripts; - generated files that do not need to be shared across servers. Do not use local storage for horizontally scaled production apps unless every process sees the same shared filesystem. ## S3 Driver The S3 driver uses `boto3`. It supports an optional `prefix` and treats `None`, `null`, and empty values as no prefix. S3 storage is useful for: - user uploads; - generated exports; - shared files across workers and web servers; - environments without persistent local disks. Example environment: ```env FILESYSTEM_DISK=s3 S3_ENDPOINT=https://s3.example.com S3_BUCKET=my-app-files S3_ACCESS_KEY=... S3_SECRET_KEY=... S3_REGION=eu-west-1 S3_PREFIX=production ``` With `S3_PREFIX=production`, `write("reports/a.txt", "data")` writes to `production/reports/a.txt`. ## Path Safety Do not pass untrusted raw user input directly as a storage path. Normalize it or generate your own filenames: ```python safe_name = uploaded_filename.replace("/", "_").replace("\\", "_") path = f"uploads/{user_id}/{safe_name}" application.storage().write(path, content) ``` For stronger guarantees, generate filenames with UUIDs or database IDs. ## Error Handling `write`, `remove`, and `move` return booleans. Check them when failure matters: ```python if not application.storage().write(path, content): application.log().error("Could not write file", extra={"path": path}) raise RuntimeError("Storage write failed") ``` `read` raises `FileNotFoundError` when a file is missing. Handle that separately when missing files are expected. ## Atomicity And Concurrency The storage abstraction does not guarantee atomic writes, locks, version checks, or compare-and-swap behavior. If concurrent writes matter, add application-level coordination, database records, object versioning, or a queue. For local storage, `move()` uses filesystem rename semantics. For S3, `move()` is implemented as copy then delete. ## Testing Storage Use the local driver and a temporary base path for most tests. Use moto for S3 unit coverage. Keep real bucket tests separate from fast unit tests and protect them with explicit environment variables. Test both success and failure paths: - write then read; - missing read; - nested directory writes; - move into a nested path; - list files without listing subdirectories; - S3 prefix handling. ## Troubleshooting File is written to an unexpected directory: Check `FILESYSTEM_DISK`, `config/storage.yaml`, and the application base path. S3 key has an unexpected prefix: Check `S3_PREFIX`. Values `None`, `null`, and empty string mean no prefix. Local storage works but S3 does not: Check endpoint, region, bucket, credentials, and network access. Then verify credentials with a small S3 integration test. `FileNotFoundError` from `read()`: Check the active disk, base path, and relative file path. ## Guidance - Prefer `app.storage()` over direct `open()` calls for application-managed files. - Keep user input sanitized before using it as a storage path. - Avoid absolute paths in storage calls. - Serialize non-string data explicitly. - Design production storage around shared persistence when running multiple processes. - Run both local and S3 storage tests before upgrading `boto3`, `botocore`, or `moto`. --- ## Testing Equipment projects use the Python standard library `unittest` runner. The repository also uses `coverage` for test coverage reporting. The generated project includes a reusable `tests/TestCase.py` base class that creates Faker data and an application container for each test. The test philosophy is simple: verify user workflows and service behavior, not just implementation details. Equipment-generated apps should be easy to refactor because tests describe what the app does. ## Generated TestCase ```python from faker import Faker from app import app class TestCase(unittest.TestCase): def setUp(self): super().setUp() self.fake = Faker() self.app = app() self.app.config.app.env.from_value('testing') ``` The base class gives each test: - `self.fake`: a Faker instance for realistic test values; - `self.app`: an Equipment application container; - `APP_ENV` forced to `testing` inside config. You can extend this base class with application-specific helpers, such as creating database rows, disabling logs, or setting local storage paths. ## Write A Test ```python from tests.TestCase import TestCase class StorageTest(TestCase): def test_write_and_read_file(self): filename = "example.txt" content = self.fake.sentence() self.assertTrue(self.app.storage().write(filename, content)) self.assertEqual(content, self.app.storage().read(filename)) ``` Prefer tests that follow a real workflow: 1. arrange input and config; 2. call an application service or entry point; 3. assert the observable result; 4. assert important side effects such as files, logs, database rows, or queued calls. ## Service Test Example ```python from tests.TestCase import TestCase class ReportsTest(TestCase): def test_report_writes_to_storage(self): path = self.app.reports().write_daily_report("Ready") self.assertTrue(self.app.storage().exists(path)) self.assertEqual("Ready", self.app.storage().read(path)) ``` ## Mocking External Services Use `unittest.mock` for external services that should not run in unit tests: ```python from unittest.mock import Mock from tests.TestCase import TestCase class ReportsTest(TestCase): def test_report_uses_storage(self): storage = Mock() self.app.storage.override(storage) self.app.reports().write_daily_report("Ready") storage.write.assert_called_once() ``` Mock network calls, Redis calls, S3 calls, email providers, and payment providers unless the test is explicitly an integration test. ## Run Generated Project Tests ```bash python -m unittest discover -s tests ``` With coverage: ```bash python -m pip install .[dev] python -m coverage run -m unittest discover -s tests python -m coverage report ``` Run a single test module: ```bash python -m unittest tests.app.test_Inspire ``` Run one test class or method: ```bash python -m unittest tests.app.test_Inspire.TestInspire python -m unittest tests.app.test_Inspire.TestInspire.test_quote ``` ## Run Repository Tests From the Equipment repository root: ```bash python -m pip install -r requirements.txt python -m pip install coverage runtype faker python -m coverage run -m unittest discover -s tests python -m coverage report ``` ## What To Cover Before Dependency Upgrades - CLI dispatch for `equipment new` and `equipment compile`. - Project scaffolding from the real template with network calls mocked. - Template rendering and generated metadata. - File creation, ignored directories, and compile output layout. - Config loading from YAML and JSON. - Local storage and S3 storage behavior. - Database URL creation and session creation. - Logging handler setup. - Queue behavior for sync mode and Redis integration when Redis is available. ## Test Categories Unit tests: - service methods; - configuration parsing; - local storage behavior; - database URL generation; - queue sync behavior; - scheduler registration helpers. Integration tests: - database sessions against SQLite; - S3 behavior with moto; - generated project install/import behavior; - compile command output; - CLI command behavior with mocked network calls. External-service tests: - Redis worker behavior; - real S3 buckets; - MySQL or PostgreSQL drivers; - deployment-specific smoke tests. Keep external-service tests opt-in unless CI provisions those services. ## Cross-platform Testing Path handling, temporary directories, file cleanup, and compiled bytecode behavior can differ across Unix and Windows. When tests create temporary directories and call `os.chdir`, make sure cleanup changes back to the previous directory before deleting the temporary directory. Windows cannot delete the current working directory. Prefer `pathlib.Path` for test file paths and avoid hardcoded `/` or `\\` separators. ## Coverage Guidance Do not chase a number without context. Coverage is useful when it protects important workflows: - project generation; - generated metadata; - config loading; - local and S3 storage; - database URL/session creation; - logging handlers; - queue behavior; - scheduler loop behavior; - compile command output. If coverage reports include dependency internals or C-extension source paths, erase old coverage data and rerun the intended command: ```bash python -m coverage erase python -m coverage run -m unittest discover -s tests python -m coverage report ``` ## Current Practical Gaps - Redis integration needs a running Redis service. - S3 tests use moto for unit coverage and do not prove real cloud credentials. - MySQL and PostgreSQL tests skip unless optional drivers are installed. - Native Windows validation should still be run for batch scripts and path-sensitive changes. ## Guidance - Prefer workflow tests over implementation-only assertions. - Mock network access in `equipment new` tests. - Use temporary directories for filesystem tests. - Keep tests deterministic; use Faker for data variety, not for essential assertions. - Keep tests importable with `python -m unittest discover -s tests`. - Avoid sleeping tests; patch scheduler loops instead. - Close files and log handlers before deleting temporary directories, especially on Windows. --- ## Compilation The `equipment compile` command prepares a generated project for bytecode-based distribution. It compiles Python files to `.pyc` and copies runtime assets into an output directory. Compilation is optional. Use it when you want a deployment directory that contains Python bytecode and runtime assets but excludes tests and source `.py` files. ## Command ```bash equipment compile dist ``` Run it from the generated project root. ## What It Does - Walks the current directory. - Ignores `__pycache__`, `dist`, `tests`, `equipment`, and the selected output directory. - Compiles `.py` files into `.pyc` files under the output directory. - Copies runtime assets: `config`, `database`, `storage`, `.coveragerc`, `.editorconfig`, `.env`, `.env.example`, `.gitignore`, `pyproject.toml`, and `README.md`. The command is designed for generated projects. Run it from the generated project root, not from inside the `equipment` package repository unless you are intentionally testing the command. ## Example ```bash equipment compile dist cd dist python main.pyc ``` On Windows: ```bat equipment compile dist cd dist py -3.14 main.pyc ``` ## Output Shape ```text dist/ ├── app/ │ └── ... .pyc files ├── config/ ├── database/ ├── storage/ ├── main.pyc ├── queues.pyc ├── scheduler.pyc ├── web.pyc ├── README.md └── pyproject.toml ``` Tests and the vendored `equipment` package directory are intentionally excluded. ## Included And Excluded Content Included by default: - compiled `.pyc` files for application Python files; - `config/`; - `database/`; - `storage/`; - `.coveragerc`; - `.editorconfig`; - `.env`; - `.env.example`; - `.gitignore`; - `pyproject.toml`; - `README.md`. Excluded by default: - `tests/`; - `equipment/` when present inside the generated project; - `__pycache__/`; - `dist/`; - the selected output directory; - source `.py` files. ## Deployment Checklist After compiling: 1. `cd` into the output directory. 2. Run `python main.pyc`. 3. Run any deployment smoke script you maintain. 4. Confirm config files and `.env` values are appropriate for the target environment. 5. Confirm optional services such as Redis, database, and S3 are reachable. If the target environment differs from your build environment, build with the Python version and operating system you intend to run where possible. ## Cross-platform Notes - Use `python main.pyc` on Unix and Windows when `python` is on PATH. - Use `py -3.14 main.pyc` on Windows when selecting a specific interpreter. - Do not rely on executable file permissions in compiled output. - Keep paths inside application code platform-safe with `pathlib` or `os.path`. ## Limitations - Bytecode is not a security boundary. Treat `.pyc` files as packaging convenience, not source protection. - Compiled output should be tested before deployment. - Platform-specific bytecode should be built with the Python version and operating system you plan to run. - The command does not bundle third-party dependencies into the output directory. - The command does not replace proper packaging, containerization, or deployment automation. ## Troubleshooting `ModuleNotFoundError` in compiled output: Install project dependencies in the runtime environment. The compile command copies your project runtime assets, not every dependency from the environment. Config file missing: Confirm the file is in one of the included runtime asset paths or copy it as part of your deployment process. Compiled output includes stale files: Delete the output directory before compiling again. Compiled output runs locally but not on another machine: Check Python version, operating system, architecture, installed dependencies, and environment variables. ## Guidance - Compile into a clean output directory such as `dist` or `build/output`. - Do not compile into the source root. - Run `python main.pyc` from inside the output directory to verify imports and runtime assets. - Use `pathlib` in project code so compiled projects behave consistently on Windows and Unix. - Keep compile validation in CI if bytecode deployment is part of your release process. --- ## FastAPI Example Generated Equipment projects include `web.py`, a small FastAPI entry point that reads host and port from `config/web.yaml` and uses the generated application container. The example is intentionally minimal. It shows how to combine FastAPI with the generated `app()` container, not how to structure every production API. ## Dependencies The generated `pyproject.toml` includes: ```toml dependencies = [ "equipment>=1.0.0", "fastapi[standard]>=0.100.0,<1", "uvicorn[standard]>=0.30,<1", ] ``` Install the generated project before running the web example: ```bash python -m pip install . ``` ## Configuration `config/web.yaml`: ```yaml web: host: ${WEB_HOST:0.0.0.0} port: ${PORT:8000} ``` Set `WEB_HOST` or `PORT` in `.env` or the process environment to change runtime behavior. ## Generated `web.py` ```python from app import app from fastapi import FastAPI from fastapi.responses import HTMLResponse from uvicorn import Server, Config app = app() name = app.config.app.name() web = FastAPI(title=name) @web.get("/", response_class=HTMLResponse) async def landing() -> str: return f''' {name} {app.inspiring().quote()} ''' if __name__ == '__main__': server = Server(Config( app='web:web', host=str(app.config.web.host()), port=int(app.config.web.port()), )) server.run() ``` The generated file creates the application container once at module import time. For many small apps, that is enough. For larger apps, keep the container creation in one module and import routers that depend on services. ## Run ```bash python web.py ``` Then open the configured host and port, usually `http://127.0.0.1:8000` for local development. If `WEB_HOST=0.0.0.0`, the server listens on all interfaces. Browsers on the same machine should still use `127.0.0.1` or `localhost`. ## Extend Add routers or services in `app/`, then inject framework services through the generated `App` container. Keep route functions thin and move business logic into testable services. ## Add A Router Create a router module: ```python # app/routes/health.py from fastapi import APIRouter from app import app router = APIRouter() application = app() @router.get("/health") def health() -> dict[str, str]: return { "status": "ok", "app": application.config.app.name(), } ``` Include it in `web.py`: ```python from app.routes.health import router as health_router web.include_router(health_router) ``` ## Use Services In Routes Keep route handlers thin: ```python @web.post("/reports/{report_id}/rebuild") def rebuild_report(report_id: int) -> dict[str, str]: app.queue().push(rebuild_report_job, report_id) return {"status": "queued"} ``` Put business behavior in services registered in `app/__init__.py`, then call those services from routes or queued jobs. ## Configuration Per Environment Local `.env` example: ```env APP_ENV=local WEB_HOST=127.0.0.1 PORT=8000 LOG_CHANNEL=stack QUEUE_CONNECTION=sync ``` Container or PaaS example: ```env APP_ENV=production WEB_HOST=0.0.0.0 PORT=8000 LOG_CHANNEL=console QUEUE_CONNECTION=redis ``` ## Testing FastAPI Routes Use FastAPI's test client for HTTP behavior and Equipment's `TestCase` for service behavior: ```python from fastapi.testclient import TestClient from web import web class WebTest(TestCase): def test_health(self): client = TestClient(web) response = client.get("/health") self.assertEqual(200, response.status_code) self.assertEqual("ok", response.json()["status"]) ``` For service-level tests, call the service directly without HTTP. This keeps most tests fast and focused. ## Deployment Notes - The generated `web.py` runs Uvicorn directly for convenience. - Production deployments may use a process manager, container command, or platform-specific start command. - Keep secrets in environment variables, not in route modules. - Use `LOG_CHANNEL=console` in platforms that collect stdout. - Use `QUEUE_CONNECTION=redis` and a separate worker process for slow work. ## Troubleshooting Server starts but route cannot import `app`: Run from the generated project root or install the project with `python -m pip install .`. Port is already in use: Change `PORT` in `.env` or stop the process using the port. Queued route blocks instead of returning quickly: `QUEUE_CONNECTION` is probably `sync`. Use Redis for true background processing. ## Guidance - Keep web configuration in `config/web.yaml` or environment variables. - Do not hardcode secrets or deployment hostnames in `web.py`. - Use FastAPI's normal testing tools for HTTP behavior and Equipment's `TestCase` for service-level behavior. - Keep route handlers small and move business logic into services. - Use queues for slow work triggered by HTTP requests. --- ## CLI Reference Equipment exposes a command-line interface through the `equipment` console script. ```bash equipment --help ``` The current CLI has two commands: - `equipment new NAME` - `equipment compile DIST` ## `equipment new NAME` Create a new project from the maintained Equipment template. ```bash equipment new my-app ``` What it does: 1. Uses the current working directory as the parent path. 2. Downloads the Equipment GitHub archive from `https://github.com/rogervila/equipment/archive/refs/heads/main.zip`. 3. Extracts the `equipment-main/project` template. 4. Copies the template to `./NAME`. 5. Copies `.env.example` to `.env`. 6. Replaces `PROJECT_NAME` in the generated `pyproject.toml` with `NAME`. 7. Prompts before overwriting an existing directory. Example: ```bash equipment new billing-api cd billing-api python -m pip install . python main.py ``` Windows example: ```powershell equipment new billing-api cd billing-api py -3.14 -m pip install . py -3.14 main.py ``` ## `new` Command Constraints - Network access to GitHub is required. - The generated directory name is the name passed to the command. - Existing directories are not overwritten without confirmation. - The command creates a local `.env` file from `.env.example`. - The command does not ask which features to include. Remove unused generated files after creation. ## `new` Command Testing Guidance Tests should mock the GitHub download. Do not make unit tests depend on live network access. Important behavior to test: - project directory creation; - `.env` creation from `.env.example`; - `PROJECT_NAME` replacement in `pyproject.toml`; - skip behavior when overwrite is declined; - download failure behavior; - ignored files such as logs, SQLite files, bytecode, caches, and virtual environments. ## `equipment compile DIST` Compile a generated project into bytecode and runtime assets. ```bash equipment compile dist ``` What it does: 1. Walks the current directory. 2. Ignores `__pycache__`, `dist`, `tests`, `equipment`, and the selected output directory. 3. Compiles `.py` files to `.pyc` files under `DIST`. 4. Copies runtime assets into `DIST`. Runtime assets copied by default: - `config/` - `database/` - `storage/` - `.coveragerc` - `.editorconfig` - `.env` - `.env.example` - `.gitignore` - `pyproject.toml` - `README.md` Example: ```bash equipment compile dist cd dist python main.pyc ``` Windows example: ```powershell equipment compile dist cd dist py -3.14 main.pyc ``` ## `compile` Command Constraints - Run from the generated project root. - Compile into a clean output directory. - Do not compile into the source root. - The command does not bundle third-party dependencies. - Bytecode is not a security boundary. - Bytecode should be built with a compatible Python version for the target runtime. ## Exit And Error Behavior The CLI prints human-readable status messages. Command implementations catch broad exceptions, print a red error message, and return. Tests should verify observable results rather than relying only on printed output. ## Cross-platform Notes - Paths should be handled with `pathlib` or platform-safe APIs. - Tests should avoid assuming `/` path separators. - Windows cannot delete the current working directory, so tests that `chdir` into a temporary directory must change back before cleanup. - Do not assume executable permissions behave the same on Unix and Windows. --- ## Environment Variables Equipment projects use `.env` and process environment variables to specialize configuration. The generated config files use `${VARIABLE:default}` syntax. Keep `.env.example` as documentation. Keep real secrets in `.env`, CI secrets, a container orchestrator, or a deployment platform secret store. ## Application Variables | Variable | Default | Description | | --- | --- | --- | | `APP_NAME` | `Equipment` | Human-readable app name used in logs and examples. | | `APP_ENV` | `local` | Environment name. Common values: `local`, `testing`, `staging`, `production`. | Example: ```env APP_NAME=Billing API APP_ENV=local ``` ## Logging Variables | Variable | Default | Description | | --- | --- | --- | | `LOG_CHANNEL` | `stack` | Active logging channel. | | `LOG_LEVEL` | `debug` | Python log level. | Common values: ```env LOG_CHANNEL=stack LOG_LEVEL=debug ``` Production platform example: ```env LOG_CHANNEL=console LOG_LEVEL=info ``` Quiet test example: ```env LOG_CHANNEL=null LOG_LEVEL=critical ``` ## Database Variables | Variable | Default | Description | | --- | --- | --- | | `DB_CONNECTION` | `sqlite` | Active connection key. | | `DB_HOST` | `127.0.0.1` | MySQL/PostgreSQL host. | | `DB_PORT` | `3306` or `5432` | MySQL/PostgreSQL port. | | `DB_DATABASE` | connection-specific | SQLite file path or database name. | | `DB_USERNAME` | connection-specific | MySQL/PostgreSQL username. | | `DB_PASSWORD` | connection-specific | MySQL/PostgreSQL password. | | `DB_CHARSET` | `utf8mb4` | MySQL charset. | SQLite local example: ```env DB_CONNECTION=sqlite DB_DATABASE=database/database.sqlite ``` MySQL example: ```env DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=equipment DB_USERNAME=equipment DB_PASSWORD=equipment DB_CHARSET=utf8mb4 ``` PostgreSQL example: ```env DB_CONNECTION=postgresql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=equipment DB_USERNAME=equipment DB_PASSWORD=equipment ``` ## Queue Variables | Variable | Default | Description | | --- | --- | --- | | `QUEUE_CONNECTION` | `sync` | Active queue driver. | | `REDIS_HOST` | `127.0.0.1` | Redis host. | | `REDIS_PORT` | `6379` | Redis port. | | `REDIS_DB` | `0` | Redis database number. | | `REDIS_USERNAME` | `null` | Redis username when required. | | `REDIS_PASSWORD` | `null` | Redis password when required. | Local/test example: ```env QUEUE_CONNECTION=sync ``` Worker example: ```env QUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_DB=0 ``` ## Storage Variables | Variable | Default | Description | | --- | --- | --- | | `FILESYSTEM_DISK` | `local` | Active storage disk. | | `S3_ENDPOINT` | none | S3-compatible endpoint. | | `S3_BUCKET` | none | Bucket name. | | `S3_ACCESS_KEY` | none | Access key. | | `S3_SECRET_KEY` | none | Secret key. | | `S3_REGION` | `auto` | S3 region. | | `S3_PREFIX` | `null` | Optional object key prefix. | Local example: ```env FILESYSTEM_DISK=local ``` S3 example: ```env FILESYSTEM_DISK=s3 S3_ENDPOINT=https://s3.example.com S3_BUCKET=my-app-files S3_ACCESS_KEY=... S3_SECRET_KEY=... S3_REGION=eu-west-1 S3_PREFIX=production ``` ## Web Variables | Variable | Default | Description | | --- | --- | --- | | `WEB_HOST` | `0.0.0.0` | Host passed to Uvicorn. | | `PORT` | `8000` | Port passed to Uvicorn. | Local example: ```env WEB_HOST=127.0.0.1 PORT=8000 ``` Container/PaaS example: ```env WEB_HOST=0.0.0.0 PORT=8000 ``` ## Recommended `.env` Profiles Local script or web development: ```env APP_ENV=local LOG_CHANNEL=stack LOG_LEVEL=debug DB_CONNECTION=sqlite QUEUE_CONNECTION=sync FILESYSTEM_DISK=local WEB_HOST=127.0.0.1 PORT=8000 ``` Production web process: ```env APP_ENV=production LOG_CHANNEL=console LOG_LEVEL=info DB_CONNECTION=postgresql QUEUE_CONNECTION=redis FILESYSTEM_DISK=s3 WEB_HOST=0.0.0.0 PORT=8000 ``` Production worker process: ```env APP_ENV=production LOG_CHANNEL=console LOG_LEVEL=info DB_CONNECTION=postgresql QUEUE_CONNECTION=redis FILESYSTEM_DISK=s3 ``` ## Type Conversion Environment variables are text. Convert values before passing them to libraries that need numbers or booleans: ```python port = int(application.config.web.port()) enabled = str(application.config.feature.enabled()).lower() == "true" ``` ## Secret Handling Never commit real secrets to Git. Do not add secrets to `config/*.yaml`, `README.md`, tests, logs, or LLM docs. Keep `.env.example` realistic but fake. ## Troubleshooting Variable appears ignored: Confirm `.env` is in the project base path and the process starts from that directory. Also check whether the variable is already set in the shell or deployment platform. Variable has wrong type: Cast explicitly at the call site. Production uses local settings: Check deployment environment variables. `.env` files are often not present in production unless explicitly copied. --- ## Maintenance Guide This page describes how to keep Equipment and generated projects healthy over time. ## Supported Runtime Contract Equipment currently supports: - Python 3.12 - Python 3.13 - Python 3.14 - Windows - macOS - Linux When changing code, config, templates, or docs, preserve all supported Python versions unless there is a deliberate compatibility decision documented in the change. ## Repository Validation From the repository root: ```bash python -m pip install -r requirements.txt python -m pip install coverage runtype faker python -m coverage run -m unittest discover -s tests ``` For Python 3.14 specifically: ```bash python3.14 -m coverage run -m unittest discover -s tests ``` For Python 3.13 specifically: ```bash python3.13 -m coverage run -m unittest discover -s tests ``` ## Generated Project Validation After changing the template under `project/`, validate that the generated project still installs, tests, compiles, and runs: ```bash cd project python -m pip install . python -m coverage run -m unittest cp .env.example .env python -m equipment compile dist cd dist python main.pyc ``` On Windows, use the Python launcher if needed: ```powershell py -3.14 -m pip install . py -3.14 -m coverage run -m unittest copy .env.example .env py -3.14 -m equipment compile dist cd dist py -3.14 main.pyc ``` ## Website Validation The website is deployed on Vercel and uses npm: ```bash cd website npm ci npm run build ``` Keep `website/package-lock.json` committed when website dependencies change. Vercel uses the lockfile for deterministic installs. The hosted LLM files are generated during the Docusaurus build by `docusaurus-plugin-llms`: - `https://equipment-python.vercel.app/llms.txt` - `https://equipment-python.vercel.app/llms-full.txt` Do not hand-edit generated files in `website/build/`. Update `website/docs/` content and the plugin root content in `website/docusaurus.config.js` instead. ## Dependency Upgrade Policy Do not mix dependency upgrades with unrelated compatibility, docs, or test changes. Upgrade dependencies in a dedicated change so failures are easy to diagnose. Before upgrading: 1. Run the current test suite. 2. Note skipped tests and warnings. 3. Upgrade one dependency group at a time. 4. Run repository tests. 5. Run generated project validation. 6. Run website validation if website dependencies changed. 7. Update docs only when behavior, commands, or constraints changed. High-risk dependencies: - `dependency-injector`: affects container configuration and singleton providers. - `SQLAlchemy`: affects database URLs, engines, sessions, and ORM examples. - `boto3`, `botocore`, `moto`: affect S3 storage tests and behavior. - `redis`, `rq`: affect queue and worker behavior. - `python-json-logger`, `python_sqlite_log_handler`: affect logging handlers. - `click`: affects CLI behavior. - `schedule`: affects scheduler behavior. - Docusaurus packages: affect website builds and Vercel deployment. ## Template Change Checklist When editing `project/`: - Update generated tests if behavior changes. - Update website docs if commands, files, or config change. - Update website docs and the `docusaurus-plugin-llms` root content in `website/docusaurus.config.js` if architecture or constraints change. - Validate generated project install and tests. - Validate compile output if entry points or runtime assets change. - Keep Unix and Windows behavior in mind. ## Python Version Change Checklist When adding or removing Python support: - Update root `pyproject.toml` classifiers. - Update generated `project/pyproject.toml` classifiers and `requires-python` if needed. - Update GitHub Actions matrix. - Run tests on each supported interpreter available locally. - Update README, website docs, and the `docusaurus-plugin-llms` root content in `website/docusaurus.config.js`. - Document any dependency blocker instead of silently upgrading requirements. ## Cross-platform Checklist - Use `pathlib` or `os.path` for paths. - Avoid hardcoded `/` and `\\` in Python logic. - Avoid shell-specific generated project commands unless alternatives are documented. - Remember Windows cannot delete the current working directory. - Close files, log handlers, and database connections before cleanup. - Prefer `python -m ...` commands in docs. - Do not assume executable bits behave the same on Windows and Unix. ## Documentation Checklist When docs change: - Keep README, website docs, [llms.txt](https://equipment-python.vercel.app/llms.txt), and [llms-full.txt](https://equipment-python.vercel.app/llms-full.txt) consistent. The LLM files are generated from docs during `npm run build`. - Prefer copyable commands. - Include Windows notes when commands differ. - Mention required external services such as Redis, S3, MySQL, or PostgreSQL. - Avoid claiming pytest is the default; Equipment uses `unittest`. - Keep examples aligned with actual generated files. - Run `cd website && npm ci && npm run build`. ## Release Notes To Watch When preparing releases, watch for: - setuptools warnings about project metadata; - Python deprecations that become removals; - Docusaurus and Webpack compatibility changes; - dependency security advisories; - GitHub Actions image changes; - Vercel Node/npm changes; - Windows path and cleanup behavior. ## Known Practical Gaps - Redis integration requires a real Redis service for full validation. - S3 tests use moto; real S3-compatible services should be validated separately. - MySQL and PostgreSQL examples need optional drivers and database services. - Native Windows validation should be run for path, cleanup, shell, and batch changes. - Bytecode compile validation does not replace packaging or deployment tests.