Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5f91700
feat(engine): add nest/engine/types.py — HttpMethod re-export and End…
itay-dar-lmnd May 19, 2026
922dd65
test(engine): strengthen test_types.py — meaningful Endpoint check an…
itay-dar-lmnd May 19, 2026
5308736
feat(engine): add ParamSpec dataclass — neutral parameter source desc…
itay-dar-lmnd May 19, 2026
b1de9a1
feat(engine): add RouteSpec dataclass — neutral route descriptor
itay-dar-lmnd May 19, 2026
f334b57
feat(engine): add framework-neutral ExecutionContext
itay-dar-lmnd May 19, 2026
40dfd87
feat(engine): add AbstractHttpAdapter — NestJS-inspired engine contract
itay-dar-lmnd May 19, 2026
07d5224
fix(engine): replace custom metaclass with standard abc.ABC in Abstra…
itay-dar-lmnd May 19, 2026
86b492a
fix(engine): use standard abc.ABC — replace metaclass workaround with…
itay-dar-lmnd May 19, 2026
824629c
feat(engine): expose public exports from nest.engine package
itay-dar-lmnd May 19, 2026
1e24420
feat(http): add nest/http facade — Request/Response/Depends/HTTPExcep…
itay-dar-lmnd May 19, 2026
5361dd3
test(engine): scaffold conformance test directory for adapter-paramet…
itay-dar-lmnd May 19, 2026
1fa757b
feat(test-app): add PR-1 smoke test app — 23 tests covering engine co…
itay-dar-lmnd May 19, 2026
4e03022
fix(decorators): resolve future-annotations in param wrappers and fil…
itay-dar-lmnd May 19, 2026
ee91620
chore: remove test-app from package — keep as local-only dev sandbox
itay-dar-lmnd May 24, 2026
cb08103
chore: untrack .a5c/ agent runtime cache — add to .gitignore
itay-dar-lmnd May 24, 2026
bbad1f0
chore: untrack docs/plans/ — local planning notes, not part of publis…
itay-dar-lmnd May 24, 2026
3d81762
feat(engine): land PRs 2+3+4 — FastAPIAdapter, cutover, and docs
itay-dar-lmnd May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,12 @@ htmlcov/

# Miscellaneous
.DS_Store

# Local dev sandbox — not part of the package
test-app/

# Agent runtime cache/logs — local only
.a5c/

# Local planning notes — not part of the published docs
docs/plans/
146 changes: 146 additions & 0 deletions docs/engine/fastapi_adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# FastAPI Adapter

The default and currently only engine. Wraps a FastAPI application and exposes
the `AbstractHttpAdapter` contract.

## Construction

Three forms, all equivalent in spirit but differing in explicitness:

```python
from nest.core import PyNestFactory
from nest.engines.fastapi import FastAPIAdapter

# 1. Implicit — PyNestFactory creates a FastAPIAdapter() with no kwargs.
app = PyNestFactory.create(AppModule)

# 2. Kwarg passthrough — kwargs flow through to FastAPI(...).
app = PyNestFactory.create(
AppModule,
title="My API",
version="1.0.0",
docs_url="/docs",
redoc_url=None,
)

# 3. Explicit adapter — full control, with the option to pre-build the FastAPI instance.
app = PyNestFactory.create(
AppModule,
adapter=FastAPIAdapter(title="My API", version="1.0.0"),
)

# 4. Bring your own FastAPI instance
from fastapi import FastAPI
existing = FastAPI(title="Pre-existing", lifespan=my_lifespan)
app = PyNestFactory.create(
AppModule,
adapter=FastAPIAdapter(instance=existing),
)
```

Passing `adapter=` and additional kwargs in the same call raises `TypeError` —
configure the adapter directly instead.

## Accessing the underlying FastAPI app

For uvicorn, Hypercorn, or any ASGI runner:

```python
app = PyNestFactory.create(AppModule)
asgi_app = app.get_http_server() # returns the FastAPI instance
```

The backward-compat shim `app.http_server` is still available but deprecated;
prefer `app.get_http_server()` or `app.adapter.get_http_server()`.

## FastAPI-specific features in phase 1

Two PyNest features remain coupled to FastAPI in phase 1 and will only work
under the FastAPI engine until phase 2 lands:

### 1. Guards with `security_scheme`

The `BaseGuard.security_scheme` attribute integrates with FastAPI's
`fastapi.security.SecurityBase` system to populate the OpenAPI `securitySchemes`
section and render the "Authorize" button in Swagger UI. Other engines have
their own security primitives and will need adapter-level reimplementation.

```python
from fastapi.security import APIKeyHeader
from nest.core.decorators.guards import BaseGuard

class ApiKeyGuard(BaseGuard):
security_scheme = APIKeyHeader(name="X-API-Key")

def can_activate(self, request, credentials=None) -> bool:
return credentials == "expected-key"
```

A basic guard without `security_scheme` (just `can_activate(request, credentials)`)
will be portable to other engines once their guard pipeline lands.

### 2. WebSocket gateways

`@WebSocketGateway` uses Starlette's `WebSocket` class via FastAPI's
`add_api_websocket_route`. Other engines will get their own WebSocket pipeline
later; until then, gateways register only under `FastAPIAdapter`.

## Translating PyNest → FastAPI

When `adapter.add_route(spec)` runs on the FastAPI adapter:

| `RouteSpec` field | FastAPI translation |
|----------------------|-------------------------------------------|
| `method` + `path` | `add_api_route(path=..., methods=[...])` |
| `endpoint` | wrapped with `bind_params(...)` and `_wrap_with_filters(...)` |
| `params` | introspected from endpoint signature; each `ParamSpec` becomes a `Depends(...)` wrapping `Body/Query/Path/Header` |
| `guards` | each guard's `.as_dependency()` → `dependencies=[...]` |
| `filters` | endpoint wrapped to route exceptions through filters |
| `status_code` | `add_api_route(status_code=...)` |
| `tags` | `add_api_route(tags=[...])` |
| `extra` | merged into `add_api_route(**spec.extra)` |

`extra` is the escape hatch for FastAPI-only kwargs that PyNest core doesn't
model (e.g. `response_model_exclude_none`, `deprecated`, `openapi_extra`).

## What's not changed vs. 0.6

Everything user-facing in 0.6 still works:

- All controller decorators (`@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`, `@Head`, `@Options`)
- All param decorators (`@Body`, `@Query`, `@Param`, `@Headers`, `@Req`, `@Res`, `@Ip`, `@HostParam`)
- `createParamDecorator(factory)` for custom param sources
- All guards (`BaseGuard`, `@UseGuards`, `security_scheme`)
- All exception filters (`@Catch`, `@UseFilters`, `app.use_global_filters(...)`)
- Lifecycle hooks (`OnApplicationBootstrap`, `OnApplicationShutdown`, `OnModuleInit`, `OnModuleDestroy`)
- WebSocket gateways (`@WebSocketGateway`, `@SubscribeMessage`, `MessageBody`, `ConnectedSocket`)
- ORM/ODM providers
- `app.use(MiddlewareClass, **opts)`, `app.use_global_filters(...)`, `app.enable_shutdown_hooks()`

## Common questions

**Q: I have an existing FastAPI app I want to mount PyNest into. Can I?**

Yes — pass it as `instance=`:

```python
fastapi_app = FastAPI(...) # your existing app with its own routes
fastapi_app.include_router(legacy_router)

app = PyNestFactory.create(
AppModule,
adapter=FastAPIAdapter(instance=fastapi_app),
)
# fastapi_app now has PyNest routes registered alongside legacy routes
```

**Q: How do I access the FastAPI dependency injection from PyNest controllers?**

`from nest.http import Depends` re-exports `fastapi.Depends`. You can use both
PyNest's `@Body`/`@Query` and FastAPI's `Depends(...)` in the same handler.

**Q: Does PyNest add overhead?**

The translation happens once at startup (during route registration). At
runtime, each request is handled by FastAPI directly. There is no per-request
PyNest overhead beyond what FastAPI itself does.
183 changes: 183 additions & 0 deletions docs/engine/migration_0.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Migrating to PyNest 0.7

PyNest 0.7 introduces the engine adapter architecture. **Every 0.6 application
continues to work without code changes.** This page documents what's new,
what's deprecated, and what's unchanged.

## TL;DR

```python
# 0.6 code — still works in 0.7
from nest.core import PyNestFactory
app = PyNestFactory.create(AppModule, title="My API")
asgi = app.get_http_server()
```

No required changes. New optional features are additive.

## What's new

### 1. Explicit adapter parameter

```python
from nest.engines.fastapi import FastAPIAdapter

app = PyNestFactory.create(
AppModule,
adapter=FastAPIAdapter(title="My API", version="1.0.0"),
)
```

Useful when you want to:
- Pre-configure FastAPI with non-trivial settings
- Inject an existing FastAPI app: `FastAPIAdapter(instance=existing_app)`
- Prepare for swapping engines later

### 2. `nest.http` import facade

Recommended for new code:

```python
# 0.7+ recommended
from nest.http import Request, Response, Depends, HTTPException
```

Under the FastAPI default these are identical to `from fastapi import …`. When
you switch to a non-FastAPI engine later, only the imports change.

The old imports still work without warning in 0.7.

### 3. `nest.engine.*` public API

The contract layer is public and documented. You can:
- Build custom adapters by subclassing `AbstractHttpAdapter`
- Construct `RouteSpec` / `ParamSpec` instances for testing
- Use the `ExecutionContext` for custom param decorators

### 4. Adapter-conformance test suite

`tests/test_engine/conformance/` parametrizes every adapter test against
every registered engine. Adding a new engine = adding one line and making
the suite green.

## What's deprecated (still works)

The following are kept for 0.6 compatibility and will be removed in **1.0**.

| Deprecated | Replacement | Reason |
|------------|-------------|--------|
| `app.http_server` (attribute) | `app.get_http_server()` / `app.adapter.get_http_server()` | Becomes ambiguous under non-FastAPI engines |
| `from fastapi import Request, Response, Depends` in PyNest code | `from nest.http import …` | Engine-neutral import path |
| `PyNestApp(container, fastapi_instance)` direct construction with a raw FastAPI | `PyNestApp(container, FastAPIAdapter(instance=fastapi_instance))` | Type uniformity |
| `RoutesResolver(container, fastapi_instance)` direct construction with a raw FastAPI | `RoutesResolver(container, FastAPIAdapter(instance=fastapi_instance))` | Same |

Calling the deprecated forms continues to work — the constructor accepts
either an adapter or a raw FastAPI instance and wraps as needed.

There are **no deprecation warnings emitted in 0.7.0**. Warnings start in 0.7.1
so 0.7.0 itself is a quiet upgrade.

## What's unchanged

Everything user-facing:

- All controller decorators (`@Get`/`@Post`/`@Put`/`@Patch`/`@Delete`/`@Head`/`@Options`)
- All param decorators (`@Body`/`@Query`/`@Param`/`@Headers`/`@Req`/`@Res`/`@Ip`/`@HostParam`)
- `createParamDecorator(factory)` and `ExecutionContext`
- `BaseGuard`, `@UseGuards`, `security_scheme` — full FastAPI security integration preserved
- `@Catch`, `@UseFilters`, `app.use_global_filters(...)`
- `OnApplicationBootstrap`, `OnApplicationShutdown`, `OnModuleInit`, `OnModuleDestroy`
- `@WebSocketGateway`, `@SubscribeMessage`, `MessageBody`, `ConnectedSocket`
- ORM / ODM provider system
- `@Module(imports=, controllers=, providers=, exports=)`
- All `@Injectable` semantics (singleton, scoped, etc.)
- CLI: `pynest generate application`, `pynest generate resource`
- `app.use(MiddlewareClass, **opts)`
- `app.enable_shutdown_hooks(signals=...)`
- `await app.close()`

## What's NOT yet portable to non-FastAPI engines (phase 1 limitation)

These work fine with the default FastAPI engine but are explicitly documented
as `engines.fastapi`-only until phase 2:

1. **Guards with `security_scheme`** — uses `fastapi.security.SecurityBase`
directly. A guard that only implements `can_activate(request, credentials)`
(no `security_scheme`) will be portable.

2. **WebSocket gateways** — uses Starlette's `WebSocket` class. Litestar has a
similar abstraction; Robyn has a different one. The neutral WebSocket
protocol comes in 0.8.

If you stay on FastAPI, both work exactly as in 0.6.

## Bug fixes in 0.7 (carried over from late-0.6 development)

These were fixed during the 0.7 development cycle and apply automatically:

1. **`from __future__ import annotations` in controllers** — previously broke
`@Body` Pydantic parsing because annotations were strings. Now resolved via
`typing.get_type_hints()` in both input parsing and response serialization.

2. **Pipe `ValueError` propagation** — `ValueError`/`TypeError` raised from a
pipe's `transform()` now becomes `HTTPException(422)` instead of a 500
that breaks the ASGI transport.

## Concrete migration examples

### Example 1: A standard app

```python
# Before (0.6) — UNCHANGED in 0.7
from nest.core import PyNestFactory
from src.app_module import AppModule

app = PyNestFactory.create(AppModule, title="My API", version="1.0.0")
asgi_app = app.get_http_server()
```

No changes needed. The implicit FastAPIAdapter is used.

### Example 2: Pre-built FastAPI instance

```python
# Before (0.6)
from fastapi import FastAPI
custom = FastAPI(title="...", lifespan=my_lifespan)
# Old PyNest had no clean way to do this; people often patched .__init__

# After (0.7) — clean
from nest.engines.fastapi import FastAPIAdapter
app = PyNestFactory.create(
AppModule,
adapter=FastAPIAdapter(instance=custom),
)
```

### Example 3: Engine-neutral imports

```python
# Old controller
from fastapi import Request

@Controller("/users")
class UserController:
@Get("/{id}")
def show(self, id: int = Param("id"), request: Request = Req()):
return {"ip": request.client.host}

# New controller (functionally identical, future-proof)
from nest.http import Request

@Controller("/users")
class UserController:
@Get("/{id}")
def show(self, id: int = Param("id"), request: Request = Req()):
return {"ip": request.client.host}
```

## Got questions?

- **Open an issue** in the [PyNest GitHub repo](https://github.com/PythonNest/PyNest).
- See [Engine Overview](overview.md) for architecture.
- See [FastAPI Adapter](fastapi_adapter.md) for engine-specific reference.
Loading
Loading