encore-dev 2.0.0a16__tar.gz → 2.0.0b4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- encore_dev-2.0.0a16/README.md → encore_dev-2.0.0b4/PKG-INFO +201 -2
- encore_dev-2.0.0a16/PKG-INFO → encore_dev-2.0.0b4/README.md +181 -15
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/__init__.py +29 -6
- encore_dev-2.0.0b4/encore/_run.py +246 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/config/_build.py +4 -4
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/_native.pyi +184 -15
- encore_dev-2.0.0b4/encore/internal/reqfields.py +307 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/runtime.py +0 -5
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/pubsub/__init__.py +50 -6
- encore_dev-2.0.0b4/encore/reqctx/__init__.py +259 -0
- encore_dev-2.0.0b4/encore/reqctx/_json.py +77 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/secrets/__init__.py +6 -10
- encore_dev-2.0.0b4/encore/server/__init__.py +10 -0
- encore_dev-2.0.0b4/encore/server/_serve.py +475 -0
- encore_dev-2.0.0b4/encore/server/_span.py +100 -0
- encore_dev-2.0.0b4/encore/server/_tracing.py +212 -0
- encore_dev-2.0.0b4/encore/server/asgi.py +161 -0
- encore_dev-2.0.0b4/encore/server/wsgi.py +208 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/services/__init__.py +19 -2
- encore_dev-2.0.0b4/encore/services/_propagate.py +88 -0
- encore_dev-2.0.0b4/encore/services/aiohttp.py +75 -0
- encore_dev-2.0.0b4/encore/services/httpx.py +137 -0
- encore_dev-2.0.0b4/encore/services/niquests.py +93 -0
- encore_dev-2.0.0b4/encore/services/requests.py +88 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/postgres/__init__.py +41 -17
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_context.py +21 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_tracer.py +6 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/pyproject.toml +15 -3
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/pyproject.toml.orig +25 -3
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/config/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/config/_markers.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/cron/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/env/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/_loader.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/lazy.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/startup.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/logs/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/logs/_console.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/logs/_runtime.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/_instrument.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/_metrics.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/_provider.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/py.typed +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/shutdown/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/objects/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/_base.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/_responses.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/_values.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/bitmap.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/generic.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/geo.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/hash.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/json.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/list.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/pubsub.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/set.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/sorted_set.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/stream.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/string.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/__init__.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_encode.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_provider.py +0 -0
- {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_span.py +0 -0
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: encore-dev
|
|
3
|
+
Version: 2.0.0b4
|
|
4
|
+
Summary: Encore SDK for Python — infrastructure resources
|
|
5
|
+
Author: Encore
|
|
6
|
+
License: MPL-2.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Requires-Dist: opentelemetry-api>=1.27
|
|
9
|
+
Requires-Dist: typing-extensions>=4.12
|
|
10
|
+
Requires-Dist: aiohttp>=3.14.4 ; python_full_version >= '3.10' and extra == 'aiohttp'
|
|
11
|
+
Requires-Dist: httpx>=0.27 ; extra == 'httpx'
|
|
12
|
+
Requires-Dist: niquests>=3.19 ; extra == 'niquests'
|
|
13
|
+
Requires-Dist: requests>=2.34 ; python_full_version >= '3.10' and extra == 'requests'
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Provides-Extra: aiohttp
|
|
16
|
+
Provides-Extra: httpx
|
|
17
|
+
Provides-Extra: niquests
|
|
18
|
+
Provides-Extra: requests
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
1
21
|
# Encore SDK for Python
|
|
2
22
|
|
|
3
23
|
The Python SDK for [Encore](https://encore.dev) — **infrastructure resources only**.
|
|
@@ -58,6 +78,184 @@ Looking up a service's URL makes the calling service depend on it, which Encore
|
|
|
58
78
|
reads from the code: pass the name as a string literal, call `base_url` directly
|
|
59
79
|
rather than passing it around, and call it in a service.
|
|
60
80
|
|
|
81
|
+
### Calling services
|
|
82
|
+
|
|
83
|
+
To call a service, use the adapter for your HTTP client. Each adapter re-exports
|
|
84
|
+
`encore.services` and adds `client(service)`, a client whose relative URLs are
|
|
85
|
+
the service's: the async one where the library has both kinds, with
|
|
86
|
+
`sync_client(service)` for the other:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from encore.services import httpx as services
|
|
90
|
+
|
|
91
|
+
billing = services.client("billing")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def invoices():
|
|
95
|
+
response = await billing.get("/invoices")
|
|
96
|
+
return response.json()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Adapter | Install | `client` returns | `sync_client` returns |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `encore.services.httpx` | `encore-dev[httpx]` | `httpx.AsyncClient` | `httpx.Client` |
|
|
102
|
+
| `encore.services.requests` | `encore-dev[requests]` | `requests.Session` | |
|
|
103
|
+
| `encore.services.aiohttp` | `encore-dev[aiohttp]` | `aiohttp.ClientSession` | |
|
|
104
|
+
| `encore.services.niquests` | `encore-dev[niquests]` | `niquests.AsyncSession` | `niquests.Session` |
|
|
105
|
+
|
|
106
|
+
The `requests` and `aiohttp` extras need Python 3.10: on 3.9 they install
|
|
107
|
+
nothing, as their releases for 3.9 have known vulnerabilities, and the adapters
|
|
108
|
+
use the client the app installed.
|
|
109
|
+
|
|
110
|
+
A generated client takes an `httpx.Client` as its `http_client`, so give it
|
|
111
|
+
`sync_client`. Their other arguments go to the client's constructor, as keywords. The same
|
|
112
|
+
rules as `base_url` apply to the service's name. aiohttp joins relative URLs as
|
|
113
|
+
links are, so write them without the leading slash there: `get("invoices")`;
|
|
114
|
+
and it creates a session only in a running event loop, so call its `client` in
|
|
115
|
+
a coroutine, such as a lifespan handler.
|
|
116
|
+
|
|
117
|
+
The clients send the Request Context of the calls made with them to the app's
|
|
118
|
+
services, and never to anything else, redirects included. A client you build
|
|
119
|
+
yourself can do the same with the adapter's hook: `Transport` and
|
|
120
|
+
`AsyncTransport` for httpx, the requests `Adapter`, the aiohttp `middleware`, or the
|
|
121
|
+
niquests `pre_request` hook.
|
|
122
|
+
|
|
123
|
+
## Request Context
|
|
124
|
+
|
|
125
|
+
Request Context fields are typed values that travel with a request to every
|
|
126
|
+
service it reaches, such as the user it is made for. Declare a field once, at
|
|
127
|
+
module scope, with its name and the type of its value:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from dataclasses import dataclass
|
|
131
|
+
|
|
132
|
+
from encore import reqctx
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class User:
|
|
137
|
+
id: str
|
|
138
|
+
admin: bool = False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
current_user = reqctx.Field[User]("user")
|
|
142
|
+
tenant = reqctx.Field[str]("tenant", record=True)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Read a field while handling a request, and set or clear one for the calls made
|
|
146
|
+
in a block:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
user = current_user.get() # None when not set
|
|
150
|
+
|
|
151
|
+
with current_user.set(User(id="system")):
|
|
152
|
+
await billing.get("/invoices")
|
|
153
|
+
|
|
154
|
+
with tenant.unset():
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Reference a field declared elsewhere, in any language, by name and with its
|
|
159
|
+
value's type: `current_user = reqctx.Field[User].named("user")`. Encore checks
|
|
160
|
+
that the types agree.
|
|
161
|
+
|
|
162
|
+
- `pubsub=True` also carries the field on the Pub/Sub messages published with
|
|
163
|
+
it set, into the handlers that receive them.
|
|
164
|
+
- `record=True` records the value on the request's spans, as
|
|
165
|
+
`encore.ctx.<name>`. Use it for identifiers worth finding traces by, never
|
|
166
|
+
for secrets.
|
|
167
|
+
|
|
168
|
+
A request keeps every field it carries, including ones the service never
|
|
169
|
+
declares, and passes them on to the services it calls through the
|
|
170
|
+
`encore.services` clients. Fields are never sent to anything else. A field's
|
|
171
|
+
value may encode to at most 4 KiB (1 KiB with `pubsub`), and a request's fields
|
|
172
|
+
to at most 8 KiB in all: setting a value beyond that raises `ValueError`.
|
|
173
|
+
|
|
174
|
+
## Running the service
|
|
175
|
+
|
|
176
|
+
`encore.run(app)` runs the service: it serves an ASGI app (FastAPI, Starlette,
|
|
177
|
+
...) with Encore's server, built on hyper, Rust's HTTP library, and handles its
|
|
178
|
+
lifecycle.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import encore
|
|
182
|
+
from fastapi import FastAPI
|
|
183
|
+
|
|
184
|
+
app = FastAPI()
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
encore.run(app)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **Startup**: it binds the address Encore assigns the service, runs the app's
|
|
191
|
+
lifespan startup, starts listening, and starts Encore's Pub/Sub
|
|
192
|
+
subscriptions. A failed startup exits with 1.
|
|
193
|
+
- **Requests**: HTTP/1.1 and HTTP/2 (h2c). Each request has its Request Context
|
|
194
|
+
and a span continuing its trace, with no OpenTelemetry setup in the app. The
|
|
195
|
+
server records the span itself, from the request's arrival to its response,
|
|
196
|
+
named after the route the app matched; the app's code reaches it as
|
|
197
|
+
`trace.get_current_span()`.
|
|
198
|
+
- **Shutdown**: on SIGTERM or SIGINT it stops accepting connections and taking
|
|
199
|
+
Pub/Sub messages, lets the requests in flight finish, runs the app's lifespan
|
|
200
|
+
shutdown, and lets Encore clean up the resources the app used, all within
|
|
201
|
+
Encore's shutdown deadline. A request waiting for its client to go away, as a
|
|
202
|
+
stream of events does, is told it did. A second Ctrl-C stops waiting.
|
|
203
|
+
|
|
204
|
+
A service without an HTTP API, as one that only handles Pub/Sub messages, calls
|
|
205
|
+
`encore.run()`: the same, without the server.
|
|
206
|
+
|
|
207
|
+
- `workers=N` runs N event loops on threads, sharing the app. It needs
|
|
208
|
+
free-threaded Python with the GIL disabled (`python3.14t`), where they run in
|
|
209
|
+
parallel. Each worker runs the app's lifespan with its own `state`, so
|
|
210
|
+
resources bound to an event loop stay on theirs.
|
|
211
|
+
- `max_concurrency` caps the requests the app handles at once.
|
|
212
|
+
- `read_timeout` (30 seconds) bounds how long a client may take to send a
|
|
213
|
+
request's headers, a body of up to 64 KiB (answered with 408 when it takes
|
|
214
|
+
longer), or each part of a longer one (after which its request reads as its
|
|
215
|
+
client gone).
|
|
216
|
+
- `tracing=False` takes only the Request Context.
|
|
217
|
+
- WebSockets are not supported yet.
|
|
218
|
+
|
|
219
|
+
A WSGI app (Flask, Django's WSGI application) runs through a WSGI-to-ASGI
|
|
220
|
+
adapter, such as `a2wsgi`'s, which calls it on a thread pool in the request's
|
|
221
|
+
context, so its code reads the Request Context and its calls carry it on:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import encore
|
|
225
|
+
from a2wsgi import WSGIMiddleware
|
|
226
|
+
from flask import Flask
|
|
227
|
+
|
|
228
|
+
app = Flask(__name__)
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
encore.run(WSGIMiddleware(app))
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Its spans are not named after its routes: WSGI frameworks don't say which one
|
|
235
|
+
they matched.
|
|
236
|
+
|
|
237
|
+
FastAPI records spans of its own once a tracer provider is set, as `run` sets
|
|
238
|
+
Encore's: a second server span for each request, and spans of dependencies,
|
|
239
|
+
endpoints and serialization. `run` turns them off for its requests; pass
|
|
240
|
+
`server_spans=False` to leave the request's span, and those, to FastAPI.
|
|
241
|
+
|
|
242
|
+
An app served by a server of its own adds Encore's middleware instead, for the
|
|
243
|
+
same Request Context and tracing:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from encore.server import asgi, wsgi
|
|
247
|
+
|
|
248
|
+
app.add_middleware(asgi.Middleware) # FastAPI, Starlette
|
|
249
|
+
app = wsgi.Middleware(app) # Flask, Django's WSGI application
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Both make Encore's tracer provider OpenTelemetry's global one, unless the app
|
|
253
|
+
set its own. The middleware works beside OpenTelemetry's own: inside it, as
|
|
254
|
+
`FastAPIInstrumentor` arranges, the request's span is that one's; outside it,
|
|
255
|
+
OpenTelemetry's records an internal span under Encore's. The ASGI middleware
|
|
256
|
+
names the span after the route FastAPI or Starlette matched; WSGI frameworks
|
|
257
|
+
don't say which route they matched.
|
|
258
|
+
|
|
61
259
|
## Postgres queries and handles
|
|
62
260
|
|
|
63
261
|
Declare `postgres.Database("orders", migrations="./migrations")` at module scope.
|
|
@@ -65,7 +263,8 @@ Declare `postgres.Database("orders", migrations="./migrations")` at module scope
|
|
|
65
263
|
reference a database declared elsewhere.
|
|
66
264
|
|
|
67
265
|
`query(sql, *params)` returns a `Cursor`. Use `async for` to stream rows,
|
|
68
|
-
`await query.one()` to fetch one row
|
|
266
|
+
`await query.one()` to fetch exactly one row (raising `EncoreError` on no rows or
|
|
267
|
+
more than one), `await query.first()` to fetch the first row or `None`, or
|
|
69
268
|
`await query.all()` to collect all rows in a list. Each query can be consumed
|
|
70
269
|
once. Reusing it or mixing helpers with iteration raises `EncoreError`, including
|
|
71
270
|
after partial iteration or an error. Helpers close their cursors automatically.
|
|
@@ -77,7 +276,7 @@ dictionaries keyed by column name. Values are bound with `$1`, `$2`, etc.
|
|
|
77
276
|
|
|
78
277
|
```python
|
|
79
278
|
async def find_order(database: postgres.Queryable, order_id: int):
|
|
80
|
-
return await database.query("SELECT * FROM orders WHERE id = $1", order_id).
|
|
279
|
+
return await database.query("SELECT * FROM orders WHERE id = $1", order_id).first()
|
|
81
280
|
|
|
82
281
|
|
|
83
282
|
async def handle_order(order_id: int):
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: encore-dev
|
|
3
|
-
Version: 2.0.0a16
|
|
4
|
-
Summary: Encore SDK for Python — infrastructure resources
|
|
5
|
-
Author: Encore
|
|
6
|
-
License: MPL-2.0
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Requires-Dist: opentelemetry-api>=1.27
|
|
9
|
-
Requires-Dist: opentelemetry-semantic-conventions>=0.62b1
|
|
10
|
-
Requires-Dist: typing-extensions>=4.12
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
1
|
# Encore SDK for Python
|
|
15
2
|
|
|
16
3
|
The Python SDK for [Encore](https://encore.dev) — **infrastructure resources only**.
|
|
@@ -71,6 +58,184 @@ Looking up a service's URL makes the calling service depend on it, which Encore
|
|
|
71
58
|
reads from the code: pass the name as a string literal, call `base_url` directly
|
|
72
59
|
rather than passing it around, and call it in a service.
|
|
73
60
|
|
|
61
|
+
### Calling services
|
|
62
|
+
|
|
63
|
+
To call a service, use the adapter for your HTTP client. Each adapter re-exports
|
|
64
|
+
`encore.services` and adds `client(service)`, a client whose relative URLs are
|
|
65
|
+
the service's: the async one where the library has both kinds, with
|
|
66
|
+
`sync_client(service)` for the other:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from encore.services import httpx as services
|
|
70
|
+
|
|
71
|
+
billing = services.client("billing")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def invoices():
|
|
75
|
+
response = await billing.get("/invoices")
|
|
76
|
+
return response.json()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Adapter | Install | `client` returns | `sync_client` returns |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `encore.services.httpx` | `encore-dev[httpx]` | `httpx.AsyncClient` | `httpx.Client` |
|
|
82
|
+
| `encore.services.requests` | `encore-dev[requests]` | `requests.Session` | |
|
|
83
|
+
| `encore.services.aiohttp` | `encore-dev[aiohttp]` | `aiohttp.ClientSession` | |
|
|
84
|
+
| `encore.services.niquests` | `encore-dev[niquests]` | `niquests.AsyncSession` | `niquests.Session` |
|
|
85
|
+
|
|
86
|
+
The `requests` and `aiohttp` extras need Python 3.10: on 3.9 they install
|
|
87
|
+
nothing, as their releases for 3.9 have known vulnerabilities, and the adapters
|
|
88
|
+
use the client the app installed.
|
|
89
|
+
|
|
90
|
+
A generated client takes an `httpx.Client` as its `http_client`, so give it
|
|
91
|
+
`sync_client`. Their other arguments go to the client's constructor, as keywords. The same
|
|
92
|
+
rules as `base_url` apply to the service's name. aiohttp joins relative URLs as
|
|
93
|
+
links are, so write them without the leading slash there: `get("invoices")`;
|
|
94
|
+
and it creates a session only in a running event loop, so call its `client` in
|
|
95
|
+
a coroutine, such as a lifespan handler.
|
|
96
|
+
|
|
97
|
+
The clients send the Request Context of the calls made with them to the app's
|
|
98
|
+
services, and never to anything else, redirects included. A client you build
|
|
99
|
+
yourself can do the same with the adapter's hook: `Transport` and
|
|
100
|
+
`AsyncTransport` for httpx, the requests `Adapter`, the aiohttp `middleware`, or the
|
|
101
|
+
niquests `pre_request` hook.
|
|
102
|
+
|
|
103
|
+
## Request Context
|
|
104
|
+
|
|
105
|
+
Request Context fields are typed values that travel with a request to every
|
|
106
|
+
service it reaches, such as the user it is made for. Declare a field once, at
|
|
107
|
+
module scope, with its name and the type of its value:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from dataclasses import dataclass
|
|
111
|
+
|
|
112
|
+
from encore import reqctx
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class User:
|
|
117
|
+
id: str
|
|
118
|
+
admin: bool = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
current_user = reqctx.Field[User]("user")
|
|
122
|
+
tenant = reqctx.Field[str]("tenant", record=True)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Read a field while handling a request, and set or clear one for the calls made
|
|
126
|
+
in a block:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
user = current_user.get() # None when not set
|
|
130
|
+
|
|
131
|
+
with current_user.set(User(id="system")):
|
|
132
|
+
await billing.get("/invoices")
|
|
133
|
+
|
|
134
|
+
with tenant.unset():
|
|
135
|
+
...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Reference a field declared elsewhere, in any language, by name and with its
|
|
139
|
+
value's type: `current_user = reqctx.Field[User].named("user")`. Encore checks
|
|
140
|
+
that the types agree.
|
|
141
|
+
|
|
142
|
+
- `pubsub=True` also carries the field on the Pub/Sub messages published with
|
|
143
|
+
it set, into the handlers that receive them.
|
|
144
|
+
- `record=True` records the value on the request's spans, as
|
|
145
|
+
`encore.ctx.<name>`. Use it for identifiers worth finding traces by, never
|
|
146
|
+
for secrets.
|
|
147
|
+
|
|
148
|
+
A request keeps every field it carries, including ones the service never
|
|
149
|
+
declares, and passes them on to the services it calls through the
|
|
150
|
+
`encore.services` clients. Fields are never sent to anything else. A field's
|
|
151
|
+
value may encode to at most 4 KiB (1 KiB with `pubsub`), and a request's fields
|
|
152
|
+
to at most 8 KiB in all: setting a value beyond that raises `ValueError`.
|
|
153
|
+
|
|
154
|
+
## Running the service
|
|
155
|
+
|
|
156
|
+
`encore.run(app)` runs the service: it serves an ASGI app (FastAPI, Starlette,
|
|
157
|
+
...) with Encore's server, built on hyper, Rust's HTTP library, and handles its
|
|
158
|
+
lifecycle.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import encore
|
|
162
|
+
from fastapi import FastAPI
|
|
163
|
+
|
|
164
|
+
app = FastAPI()
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
encore.run(app)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- **Startup**: it binds the address Encore assigns the service, runs the app's
|
|
171
|
+
lifespan startup, starts listening, and starts Encore's Pub/Sub
|
|
172
|
+
subscriptions. A failed startup exits with 1.
|
|
173
|
+
- **Requests**: HTTP/1.1 and HTTP/2 (h2c). Each request has its Request Context
|
|
174
|
+
and a span continuing its trace, with no OpenTelemetry setup in the app. The
|
|
175
|
+
server records the span itself, from the request's arrival to its response,
|
|
176
|
+
named after the route the app matched; the app's code reaches it as
|
|
177
|
+
`trace.get_current_span()`.
|
|
178
|
+
- **Shutdown**: on SIGTERM or SIGINT it stops accepting connections and taking
|
|
179
|
+
Pub/Sub messages, lets the requests in flight finish, runs the app's lifespan
|
|
180
|
+
shutdown, and lets Encore clean up the resources the app used, all within
|
|
181
|
+
Encore's shutdown deadline. A request waiting for its client to go away, as a
|
|
182
|
+
stream of events does, is told it did. A second Ctrl-C stops waiting.
|
|
183
|
+
|
|
184
|
+
A service without an HTTP API, as one that only handles Pub/Sub messages, calls
|
|
185
|
+
`encore.run()`: the same, without the server.
|
|
186
|
+
|
|
187
|
+
- `workers=N` runs N event loops on threads, sharing the app. It needs
|
|
188
|
+
free-threaded Python with the GIL disabled (`python3.14t`), where they run in
|
|
189
|
+
parallel. Each worker runs the app's lifespan with its own `state`, so
|
|
190
|
+
resources bound to an event loop stay on theirs.
|
|
191
|
+
- `max_concurrency` caps the requests the app handles at once.
|
|
192
|
+
- `read_timeout` (30 seconds) bounds how long a client may take to send a
|
|
193
|
+
request's headers, a body of up to 64 KiB (answered with 408 when it takes
|
|
194
|
+
longer), or each part of a longer one (after which its request reads as its
|
|
195
|
+
client gone).
|
|
196
|
+
- `tracing=False` takes only the Request Context.
|
|
197
|
+
- WebSockets are not supported yet.
|
|
198
|
+
|
|
199
|
+
A WSGI app (Flask, Django's WSGI application) runs through a WSGI-to-ASGI
|
|
200
|
+
adapter, such as `a2wsgi`'s, which calls it on a thread pool in the request's
|
|
201
|
+
context, so its code reads the Request Context and its calls carry it on:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
import encore
|
|
205
|
+
from a2wsgi import WSGIMiddleware
|
|
206
|
+
from flask import Flask
|
|
207
|
+
|
|
208
|
+
app = Flask(__name__)
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
encore.run(WSGIMiddleware(app))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Its spans are not named after its routes: WSGI frameworks don't say which one
|
|
215
|
+
they matched.
|
|
216
|
+
|
|
217
|
+
FastAPI records spans of its own once a tracer provider is set, as `run` sets
|
|
218
|
+
Encore's: a second server span for each request, and spans of dependencies,
|
|
219
|
+
endpoints and serialization. `run` turns them off for its requests; pass
|
|
220
|
+
`server_spans=False` to leave the request's span, and those, to FastAPI.
|
|
221
|
+
|
|
222
|
+
An app served by a server of its own adds Encore's middleware instead, for the
|
|
223
|
+
same Request Context and tracing:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from encore.server import asgi, wsgi
|
|
227
|
+
|
|
228
|
+
app.add_middleware(asgi.Middleware) # FastAPI, Starlette
|
|
229
|
+
app = wsgi.Middleware(app) # Flask, Django's WSGI application
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Both make Encore's tracer provider OpenTelemetry's global one, unless the app
|
|
233
|
+
set its own. The middleware works beside OpenTelemetry's own: inside it, as
|
|
234
|
+
`FastAPIInstrumentor` arranges, the request's span is that one's; outside it,
|
|
235
|
+
OpenTelemetry's records an internal span under Encore's. The ASGI middleware
|
|
236
|
+
names the span after the route FastAPI or Starlette matched; WSGI frameworks
|
|
237
|
+
don't say which route they matched.
|
|
238
|
+
|
|
74
239
|
## Postgres queries and handles
|
|
75
240
|
|
|
76
241
|
Declare `postgres.Database("orders", migrations="./migrations")` at module scope.
|
|
@@ -78,7 +243,8 @@ Declare `postgres.Database("orders", migrations="./migrations")` at module scope
|
|
|
78
243
|
reference a database declared elsewhere.
|
|
79
244
|
|
|
80
245
|
`query(sql, *params)` returns a `Cursor`. Use `async for` to stream rows,
|
|
81
|
-
`await query.one()` to fetch one row
|
|
246
|
+
`await query.one()` to fetch exactly one row (raising `EncoreError` on no rows or
|
|
247
|
+
more than one), `await query.first()` to fetch the first row or `None`, or
|
|
82
248
|
`await query.all()` to collect all rows in a list. Each query can be consumed
|
|
83
249
|
once. Reusing it or mixing helpers with iteration raises `EncoreError`, including
|
|
84
250
|
after partial iteration or an error. Helpers close their cursors automatically.
|
|
@@ -90,7 +256,7 @@ dictionaries keyed by column name. Values are bound with `$1`, `$2`, etc.
|
|
|
90
256
|
|
|
91
257
|
```python
|
|
92
258
|
async def find_order(database: postgres.Queryable, order_id: int):
|
|
93
|
-
return await database.query("SELECT * FROM orders WHERE id = $1", order_id).
|
|
259
|
+
return await database.query("SELECT * FROM orders WHERE id = $1", order_id).first()
|
|
94
260
|
|
|
95
261
|
|
|
96
262
|
async def handle_order(order_id: int):
|
|
@@ -8,22 +8,44 @@ PyO3) connects them to the provisioned infrastructure at run time.
|
|
|
8
8
|
Services are declared by the language-agnostic ``encore.service.json`` file, not in
|
|
9
9
|
Python — this SDK is infrastructure-only.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Run the service with ``encore.run(app)``: it serves the ASGI app where Encore
|
|
12
|
+
assigns the service, starts the Pub/Sub subscriptions, and shuts down gracefully
|
|
13
|
+
with Encore. A service without an HTTP API calls ``encore.run()``.
|
|
14
14
|
|
|
15
|
+
import encore
|
|
15
16
|
from encore import pubsub, secrets
|
|
16
17
|
from encore.storage import postgres
|
|
18
|
+
from fastapi import FastAPI
|
|
17
19
|
|
|
18
20
|
db = postgres.Database("orders", migrations="./migrations")
|
|
19
21
|
events = pubsub.Topic("order-events")
|
|
20
22
|
api_key = secrets.Secret("ApiKey")
|
|
23
|
+
|
|
24
|
+
app = FastAPI()
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
encore.run(app)
|
|
28
|
+
|
|
29
|
+
An app served by a server of its own instead calls ``await encore.start()`` once
|
|
30
|
+
from its startup hook, to start the subscriptions.
|
|
21
31
|
"""
|
|
22
32
|
|
|
23
33
|
from __future__ import annotations
|
|
24
34
|
|
|
25
|
-
from . import
|
|
26
|
-
|
|
35
|
+
from . import (
|
|
36
|
+
config,
|
|
37
|
+
cron,
|
|
38
|
+
env,
|
|
39
|
+
metrics,
|
|
40
|
+
pubsub,
|
|
41
|
+
reqctx,
|
|
42
|
+
secrets,
|
|
43
|
+
services,
|
|
44
|
+
shutdown,
|
|
45
|
+
storage,
|
|
46
|
+
)
|
|
47
|
+
from ._run import run
|
|
48
|
+
from .internal.runtime import EncoreError, version
|
|
27
49
|
from .internal.startup import start
|
|
28
50
|
from .env import app_meta
|
|
29
51
|
|
|
@@ -34,12 +56,13 @@ __all__ = [
|
|
|
34
56
|
"cron",
|
|
35
57
|
"env",
|
|
36
58
|
"metrics",
|
|
59
|
+
"reqctx",
|
|
37
60
|
"secrets",
|
|
38
61
|
"services",
|
|
39
62
|
"shutdown",
|
|
40
63
|
"EncoreError",
|
|
41
64
|
"app_meta",
|
|
42
|
-
"
|
|
65
|
+
"run",
|
|
43
66
|
"start",
|
|
44
67
|
"version",
|
|
45
68
|
]
|