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.
Files changed (67) hide show
  1. encore_dev-2.0.0a16/README.md → encore_dev-2.0.0b4/PKG-INFO +201 -2
  2. encore_dev-2.0.0a16/PKG-INFO → encore_dev-2.0.0b4/README.md +181 -15
  3. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/__init__.py +29 -6
  4. encore_dev-2.0.0b4/encore/_run.py +246 -0
  5. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/config/_build.py +4 -4
  6. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/_native.pyi +184 -15
  7. encore_dev-2.0.0b4/encore/internal/reqfields.py +307 -0
  8. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/runtime.py +0 -5
  9. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/pubsub/__init__.py +50 -6
  10. encore_dev-2.0.0b4/encore/reqctx/__init__.py +259 -0
  11. encore_dev-2.0.0b4/encore/reqctx/_json.py +77 -0
  12. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/secrets/__init__.py +6 -10
  13. encore_dev-2.0.0b4/encore/server/__init__.py +10 -0
  14. encore_dev-2.0.0b4/encore/server/_serve.py +475 -0
  15. encore_dev-2.0.0b4/encore/server/_span.py +100 -0
  16. encore_dev-2.0.0b4/encore/server/_tracing.py +212 -0
  17. encore_dev-2.0.0b4/encore/server/asgi.py +161 -0
  18. encore_dev-2.0.0b4/encore/server/wsgi.py +208 -0
  19. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/services/__init__.py +19 -2
  20. encore_dev-2.0.0b4/encore/services/_propagate.py +88 -0
  21. encore_dev-2.0.0b4/encore/services/aiohttp.py +75 -0
  22. encore_dev-2.0.0b4/encore/services/httpx.py +137 -0
  23. encore_dev-2.0.0b4/encore/services/niquests.py +93 -0
  24. encore_dev-2.0.0b4/encore/services/requests.py +88 -0
  25. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/postgres/__init__.py +41 -17
  26. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_context.py +21 -0
  27. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_tracer.py +6 -0
  28. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/pyproject.toml +15 -3
  29. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/pyproject.toml.orig +25 -3
  30. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/config/__init__.py +0 -0
  31. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/config/_markers.py +0 -0
  32. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/cron/__init__.py +0 -0
  33. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/env/__init__.py +0 -0
  34. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/__init__.py +0 -0
  35. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/_loader.py +0 -0
  36. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/lazy.py +0 -0
  37. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/internal/startup.py +0 -0
  38. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/logs/__init__.py +0 -0
  39. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/logs/_console.py +0 -0
  40. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/logs/_runtime.py +0 -0
  41. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/__init__.py +0 -0
  42. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/_instrument.py +0 -0
  43. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/_metrics.py +0 -0
  44. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/metrics/_provider.py +0 -0
  45. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/py.typed +0 -0
  46. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/shutdown/__init__.py +0 -0
  47. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/__init__.py +0 -0
  48. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/objects/__init__.py +0 -0
  49. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/__init__.py +0 -0
  50. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/_base.py +0 -0
  51. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/_responses.py +0 -0
  52. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/_values.py +0 -0
  53. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/bitmap.py +0 -0
  54. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/generic.py +0 -0
  55. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/geo.py +0 -0
  56. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/hash.py +0 -0
  57. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/json.py +0 -0
  58. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/list.py +0 -0
  59. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/pubsub.py +0 -0
  60. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/set.py +0 -0
  61. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/sorted_set.py +0 -0
  62. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/stream.py +0 -0
  63. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/storage/redis/string.py +0 -0
  64. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/__init__.py +0 -0
  65. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_encode.py +0 -0
  66. {encore_dev-2.0.0a16 → encore_dev-2.0.0b4}/encore/tracing/_provider.py +0 -0
  67. {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 or `None` (rejecting multiple rows), or
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).one()
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 or `None` (rejecting multiple rows), or
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).one()
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
- After importing resource declarations, call ``await encore.start()`` once from
12
- your async main or framework startup hook. This starts all registered subscription
13
- handlers on the application's loop and returns so the application can serve.
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 config, cron, env, metrics, pubsub, secrets, services, shutdown, storage
26
- from .internal.runtime import EncoreError, run_forever, version
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
- "run_forever",
65
+ "run",
43
66
  "start",
44
67
  "version",
45
68
  ]