Metadata-Version: 2.3
Name: nyxa
Version: 0.1.0
Summary: Batteries-included DX for Python API backends on FastAPI — convention-based CLI and runtime bases
Keywords: fastapi,artisan,cli,scaffold,backend,api,nyxa
Author: Al-Amin Islam Nerob
Author-email: Al-Amin Islam Nerob <alamin@aincoder.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Typing :: Typed
Requires-Dist: fastapi>=0.115.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: typer>=0.12.0
Requires-Dist: uvicorn>=0.30.0
Requires-Python: >=3.11
Project-URL: Homepage, https://nyxadev.com
Project-URL: Repository, https://github.com/NyxaDev/nyxa
Project-URL: Documentation, https://nyxadev.com
Description-Content-Type: text/markdown

# nyxa

Batteries-included DX for **Python API backends** on FastAPI.

**Nyxa** is pronounced *Nik-sah* (`/ˈnɪksə/`). Site: [nyxadev.com](https://nyxadev.com) · Repo: [NyxaDev/nyxa](https://github.com/NyxaDev/nyxa).

Nyxa gives you a convention-based CLI (`nyxa`, with an `artisan` alias), project layout, central `routes.py` + controllers, and small runtime bases (`AppError`, abort helpers, `register`, events). It is **backend/API only** — no server-side templates, Vite, or frontend stack.

> Long-term plans: see the workspace [`roadmap/`](../../roadmap/).

---

## Install

```bash
uv add nyxa
# or
pip install nyxa
```

The import and CLI stay **`nyxa`**:

```python
from nyxa import AppError, Route, register
```

---

## Quick start

```bash
uvx --from nyxa nyxa new myapp
cd myapp
uv sync
uv run dev
```

Interactive mode (TTY) prompts for database, auth, and optional packs. For CI:

```bash
nyxa new myapp --database sqlite --auth none --no-input
```

Cheat sheet: [`docs/cheat-sheet.md`](../../docs/cheat-sheet.md).
Adapter upgrades: [`docs/upgrading.md`](../../docs/upgrading.md).

### CORS and logging

Optional; off by default. Configure in `nyxa.toml` or env — applied inside `register()`:

```toml
[cors]
origins = ["http://localhost:3000"]

[logging]
json = true
level = "INFO"
```

```bash
CORS_ORIGINS=http://localhost:3000
NYXA_LOG_JSON=1
```

Or call `configure_cors(app, origins=[...])` / `configure_logging()` manually.

Scaffold into the current directory:

```bash
mkdir myapp && cd myapp
uvx --from nyxa nyxa init
```

Monorepo / unpublished local checkout:

```bash
uv run nyxa new myapp --nyxa-path /path/to/packages/nyxa
```

---

## Routing

Define routes in `src/{app}/routes.py`. Nyxa **compiles** them at startup into normal FastAPI endpoints (no Route stack on the request hot path).

| What you need | Nyxa |
| --- | --- |
| Map a path to a controller action | `Route.get("/users", [UserController, "index"])` |
| Group routes under a prefix | `with Route.prefix("/api"):` |
| Name a route | `.name("users.index")` |
| RESTful resource routes | `Route.resource("posts", PostController)` |
| List compiled routes | `nyxa route:list` |

```python
# src/myapp/routes.py
from nyxa.routing import Route
from myapp.http.controllers.user_controller import UserController

with Route.prefix("/api"):
    Route.resource("users", UserController)
```

---

## Stable CLI (0.1)

Entry points: `nyxa`, `artisan` (alias), and `dev`.

Generated scaffolds include **Google-style docstrings** (`Args` / `Returns`) on modules, classes, and functions.

| Command | Purpose |
| --- | --- |
| `nyxa new` / `nyxa init [name]` | Scaffold a new API app (`--database`, `--auth`, `--no-input`, `--queue`/`--cache`/`--mail`/`--storage`, `--force`, `--nyxa-path`) |
| `nyxa make:controller` | Controller + auto route; shorts `-a`/`-m`/`-r`/`-R`/`-e`/`-s` |
| `nyxa make:exc` | Error + handler; nested paths (`user/NotFound`); status heuristic + `--status` |
| `nyxa make:req` / `make:res` | FormRequest / response models |
| `nyxa make:policy` | Policy class (`nyxa-auth`) |
| `nyxa make:middleware` | Middleware class |
| `nyxa make:provider` | Service provider (`register` / `boot`) |
| `nyxa make:service` | Service class |
| `nyxa make:dependency` | FastAPI dependency |
| `nyxa make:schema` / `make:enum` / `make:config` | Schema, enum, settings |
| `nyxa make:event` / `make:listener` / `make:job` | Events, listeners (`--event` required), jobs (`Job` class when `nyxa-queue` installed) |
| `nyxa queue work` | Run a job once in-process (`nyxa-queue`) |
| `nyxa cache clear` | Flush cache store (`nyxa-cache`) |
| `nyxa mail test` | Send sample mail via configured mailer (`nyxa-mail`) |
| `nyxa make:test` | Pytest stub |
| `nyxa make:resource` | Full CRUD slice + `Route.resource` stub |
| `nyxa route:list` / `list:routes` | List routes (method, URI, name, action) |
| `dev` | Run uvicorn using `nyxa.toml` `[dev]` |

---

## Stable public API (0.1)

Supported imports for application code:

```python
from nyxa import (
    AppError,
    Controller,
    EventDispatcher,
    ForbiddenError,
    FormRequest,
    Route,
    ServiceProvider,
    abort,
    bind,
    check,
    config,
    context,
    current_user,
    dispatcher,
    env,
    guest,
    no_content,
    not_found,
    register,
    route,
    settings,
    success,
    user,
)
```

| Symbol | Role |
| --- | --- |
| `AppError` | Base application error (override via `[bases].error`); default JSON handler on `register` |
| `UnauthorizedError` / `ForbiddenError` / `NotFoundError` / … | Core HTTP-mapped `AppError` subclasses (401–429) |
| `abort` / `unauthorized` / `forbidden` / `not_found` / … | Raise helpers (`NoReturn`) for common exits |
| `success` / `created` / `accepted` / `no_content` / `json_response` | JSON envelope / empty-body helpers |
| `context` / `ContextMiddleware` | Request-scoped key/value bag (auto-wired by `register`) |
| `user` / `current_user` / `guest` / `check` | Auth stubs reading context (filled by `nyxa-auth`) |
| `env` / `config` / `settings` | Process env + BaseSettings under `config/` (`model_dump` + typed `settings(stem)`) |
| `Route` / `Controller` | Routing DSL + controller base (constructor `Depends` OK) |
| `route(name, **params)` | Reverse URL helper from compiled named routes |
| `FormRequest` | Pydantic request base (`authorize()` → `ForbiddenError` when falsy) |
| `ServiceProvider` | App boot hooks (`register` / `boot`) discovered under `providers/` |
| `bind` / `singleton` / `instance` / `make` / `resolve` / `inject` / `has` | Lean service container (flushed on `register`) |
| `ForbiddenError` | HTTP 403 `AppError` subclass |
| `register(app)` | Default AppError handler, custom handlers, middleware, context, container flush, providers, `routes.py`, listeners |
| `dispatcher` / `EventDispatcher` | In-process events |

Everything else under `nyxa.*` (`templates`, `generate`, `make`, `config` internals, …) is **private** and may change without notice.

See also [`API.md`](API.md).

---

## Config (`nyxa.toml`, optional)

All keys optional. Defaults:

```toml
app = "myapp"       # required in practice — set by `init` / `new`
src = "src"

[dev]
module = "myapp.main:app"   # default: "{app}.main:app"
host = "0.0.0.0"
port = 8000
reload = true

[paths]
errors = "http/exception/errors"
handlers = "http/exception/handlers"
requests = "http/requests"
responses = "http/responses"
middleware = "http/middleware"
controllers = "http/controllers"
dependencies = "http/dependencies"
services = "services"
schemas = "schemas"
enums = "domain/enums"
config = "config"
events = "events"
listeners = "listeners"
jobs = "jobs"
providers = "providers"
tests = "tests"             # relative to project root

[middleware]
# global = ["timing"]       # optional Kernel-style list (module stems); request-inbound order
# When unset, all http/middleware/*/Middleware exports are auto-discovered (alphabetical).

[providers]
# boot = ["app_service_provider"]  # optional ordered list (module stems under paths.providers)
# When unset, all providers/*/Provider exports are auto-discovered (alphabetical).

[env]
file = ".env"               # primary dotenv under project root
# environment = "local"     # also load .env.{environment}; else NYXA_ENV / APP_ENV

[bases]
error = "nyxa.errors:AppError"
```

Fallback: `[tool.nyxa] app = "..."` in `pyproject.toml`.

### Middleware (two concepts)

| Mechanism | Role |
| --- | --- |
| `Route.middleware(...)` / `Route.group(..., middleware=[...])` | FastAPI **`Depends`** on routes (per-route / group) |
| Discovered `Middleware` export under `http/middleware/` | Global **Starlette** `BaseHTTPMiddleware` stack via `register` |

Use `[middleware] global = ["timing", ...]` for an explicit Kernel-style order (first entry is outermost / sees the request first). Omit `global` to auto-discover all modules alphabetically.

### Service providers

App boot hooks under `providers/`. Each module exports `Provider` (a `ServiceProvider` subclass) with optional `register` / `boot` methods. `register(app)` calls **all** `register` hooks, then **all** `boot` hooks, before loading `routes.py`.

```bash
uv run nyxa make:provider AppService
```

```toml
[providers]
boot = ["app_service_provider"]  # optional; omit for alphabetical discovery
```

### Service container

Lean bind/resolve for app-scoped services. Bind in a provider; inject via FastAPI:

```python
from nyxa import singleton, inject

# in ServiceProvider.register:
singleton(UserService)

# in controller __init__:
service: Annotated[UserService, Depends(inject(UserService))]
```

Also: `bind`, `instance`, `make` / `resolve`, `has`. The global `container` is flushed at the start of each `register()`.

### Environment (`.env`)

`register(app)` loads dotenv files into the process environment:

1. Primary file from `[env] file` (default `.env`)
2. Optional `.env.{environment}` when `[env] environment` is set, else `NYXA_ENV`, else `APP_ENV`

Among files, the environment-specific file overrides the primary for shared keys. Existing OS/CI variables are **never** overridden. Missing files are no-ops. `init` / `new` write `.env.example` as a template — copy to `.env` (and optionally `.env.local`, etc.) locally (gitignored).

```toml
[env]
file = ".env"
environment = "local"   # loads .env then .env.local
```

---

## Out of scope (for now)

- Database / migrations — **Phase 2** (`nyxa-db`) — Done
- Auth / policies — **Phase 3** (`nyxa-auth`) — **Done** (JWT, sessions, personal tokens, Google OAuth)
- Queues, cache, mail, storage — **Phase 4 Done** (`nyxa-queue`, `nyxa-cache`, `nyxa-mail`, `nyxa-storage`)
- Frontend scaffolding — never (API-only)

---

## Tests

```bash
# from workspace root
uv sync --group dev
uv run pytest packages/nyxa/tests
```

---

## Publish (maintainers)

### GitHub Release (recommended)

[`.github/workflows/publish-nyxa.yml`](../../.github/workflows/publish-nyxa.yml) publishes core `nyxa` and all seven `nyxa-*` packs from one Release tag, using PyPI Trusted Publishing (OIDC, no API token secret).

**One-time setup**

1. In GitHub → **Settings → Environments**, create one environment per package: `pypi-nyxa`, `pypi-nyxa-db`, `pypi-nyxa-auth`, `pypi-nyxa-queue`, `pypi-nyxa-cache`, `pypi-nyxa-mail`, `pypi-nyxa-storage`, `pypi-nyxa-postgres`.
2. On PyPI, add a Trusted Publisher for each package (GitHub tab):

| PyPI project name | Owner | Repository name | Workflow name | Environment name |
| --- | --- | --- | --- | --- |
| `nyxa` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa` |
| `nyxa-db` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-db` |
| `nyxa-auth` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-auth` |
| `nyxa-queue` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-queue` |
| `nyxa-cache` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-cache` |
| `nyxa-mail` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-mail` |
| `nyxa-storage` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-storage` |
| `nyxa-postgres` | `NyxaDev` | `Nyxa` | `publish-nyxa.yml` | `pypi-nyxa-postgres` |

For a project that doesn't exist yet, register it as a *pending* publisher at [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/). PyPI allows 3 pending publishers per account at a time, and each slot frees up once its project is created. Pending publishers must also be unique on owner/repo/workflow/environment, which is why every package has its own environment.

**Each release**

```bash
gh release create v0.1.1 --target master --title "Nyxa v0.1.1" --notes "…"
gh run watch
```

The tag (leading `v` stripped) becomes the version of every `packages/nyxa*/pyproject.toml`. The workflow builds all eight packages once, then runs one publish job per package in its own environment. A failed job can be re-run with `gh run rerun <run-id> --failed`; files already on PyPI are skipped.

### Verify a build locally

From the workspace root, build exactly as CI does, then install the wheels into a scratch environment:

```bash
for p in nyxa nyxa-db nyxa-auth nyxa-queue nyxa-cache nyxa-mail nyxa-storage nyxa-postgres; do
  uv build --package "$p" --no-sources --out-dir /tmp/nyxa-dist
done
uvx twine check /tmp/nyxa-dist/*
uvx --from /tmp/nyxa-dist/nyxa-0.1.0-py3-none-any.whl nyxa new /tmp/demoapp --no-input
# resolve the scaffold against local wheels before they are on PyPI:
cd /tmp/demoapp && uv sync --find-links /tmp/nyxa-dist
```

---

## Roadmap

Workspace plans: [`roadmap/README.md`](../../roadmap/README.md).
