encore-dev 2.0.0b4__tar.gz → 2.0.0b5__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.0b4 → encore_dev-2.0.0b5}/PKG-INFO +95 -3
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/README.md +94 -2
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/__init__.py +4 -0
- encore_dev-2.0.0b5/encore/errs/__init__.py +90 -0
- encore_dev-2.0.0b5/encore/gateway/__init__.py +990 -0
- encore_dev-2.0.0b5/encore/gateway/_serve.py +615 -0
- encore_dev-2.0.0b5/encore/gateway/match.py +270 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/_loader.py +39 -9
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/_native.pyi +210 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/reqfields.py +54 -24
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/pubsub/__init__.py +3 -3
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/reqctx/__init__.py +7 -9
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/secrets/__init__.py +55 -1
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/services/__init__.py +5 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/postgres/__init__.py +7 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/pyproject.toml +1 -1
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/pyproject.toml.orig +1 -1
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/_run.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/config/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/config/_build.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/config/_markers.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/cron/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/env/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/lazy.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/runtime.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/internal/startup.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/logs/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/logs/_console.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/logs/_runtime.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/metrics/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/metrics/_instrument.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/metrics/_metrics.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/metrics/_provider.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/py.typed +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/reqctx/_json.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/server/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/server/_serve.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/server/_span.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/server/_tracing.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/server/asgi.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/server/wsgi.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/services/_propagate.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/services/aiohttp.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/services/httpx.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/services/niquests.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/services/requests.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/shutdown/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/objects/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/_base.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/_responses.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/_values.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/bitmap.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/generic.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/geo.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/hash.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/json.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/list.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/pubsub.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/set.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/sorted_set.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/stream.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/storage/redis/string.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/tracing/__init__.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/tracing/_context.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/tracing/_encode.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/tracing/_provider.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/tracing/_span.py +0 -0
- {encore_dev-2.0.0b4 → encore_dev-2.0.0b5}/encore/tracing/_tracer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: encore-dev
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b5
|
|
4
4
|
Summary: Encore SDK for Python — infrastructure resources
|
|
5
5
|
Author: Encore
|
|
6
6
|
License: MPL-2.0
|
|
@@ -60,6 +60,29 @@ async def place_order(order):
|
|
|
60
60
|
await events.publish({"id": order.id})
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Secrets
|
|
64
|
+
|
|
65
|
+
`secrets.Secret[T]("Name")` belongs to the service that declares it.
|
|
66
|
+
`secrets.SharedSecret[T]("Name")` is one secret for the whole app, given to the
|
|
67
|
+
services that read it, and `secrets.GlobalSecret[T]("Name")` one that every
|
|
68
|
+
service is given. `T` is `str` or `bytes`, and `value` reads the secret decoded
|
|
69
|
+
as that.
|
|
70
|
+
|
|
71
|
+
A shared secret is declared once in the app. Another Python service can't
|
|
72
|
+
import the declaring service's modules, so it references the secret by name
|
|
73
|
+
instead:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from encore.secrets import SharedSecret
|
|
77
|
+
|
|
78
|
+
support_email = SharedSecret[str].named("SupportEmail")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Reading `value` makes the service one the secret is given to. The declaration
|
|
82
|
+
can be in any language, and the type argument is how this service decodes the
|
|
83
|
+
value, whatever the declaration's. Encore reports a reference to a name that
|
|
84
|
+
isn't declared as a shared secret.
|
|
85
|
+
|
|
63
86
|
## Service URLs
|
|
64
87
|
|
|
65
88
|
Use `base_url` to look up a service's configured base URL:
|
|
@@ -139,7 +162,7 @@ class User:
|
|
|
139
162
|
|
|
140
163
|
|
|
141
164
|
current_user = reqctx.Field[User]("user")
|
|
142
|
-
tenant = reqctx.Field[str]("tenant",
|
|
165
|
+
tenant = reqctx.Field[str]("tenant", trace=True)
|
|
143
166
|
```
|
|
144
167
|
|
|
145
168
|
Read a field while handling a request, and set or clear one for the calls made
|
|
@@ -161,7 +184,7 @@ that the types agree.
|
|
|
161
184
|
|
|
162
185
|
- `pubsub=True` also carries the field on the Pub/Sub messages published with
|
|
163
186
|
it set, into the handlers that receive them.
|
|
164
|
-
- `
|
|
187
|
+
- `trace=True` puts the value on the request's spans, as
|
|
165
188
|
`encore.ctx.<name>`. Use it for identifiers worth finding traces by, never
|
|
166
189
|
for secrets.
|
|
167
190
|
|
|
@@ -274,6 +297,16 @@ Entering the context does not execute SQL. Exiting it closes the query.
|
|
|
274
297
|
`exec(sql, *params)` returns an `ExecResult` with `rows_affected`. Rows are
|
|
275
298
|
dictionaries keyed by column name. Values are bound with `$1`, `$2`, etc.
|
|
276
299
|
|
|
300
|
+
Column values have the Python types asyncpg and psycopg use. `numeric` is a
|
|
301
|
+
`decimal.Decimal` with the column's scale, so a `numeric(10,2)` holding 7 is
|
|
302
|
+
`Decimal('7.00')`; `NaN` and `±Infinity` are `Decimal('NaN')` and
|
|
303
|
+
`Decimal('Infinity')`. `timestamptz` is an aware `datetime.datetime` in UTC,
|
|
304
|
+
`timestamp` a naive one, and `date` and `time` are `datetime.date` and
|
|
305
|
+
`datetime.time`. `bytea` is `bytes`, arrays are lists of their elements' types,
|
|
306
|
+
JSON is decoded, and `uuid`, `inet` and `cidr` are strings. Parameters accept
|
|
307
|
+
the same types; a naive `datetime` bound to a `timestamptz` is taken as UTC,
|
|
308
|
+
and an aware one may use any `tzinfo`, `zoneinfo.ZoneInfo` included.
|
|
309
|
+
|
|
277
310
|
```python
|
|
278
311
|
async def find_order(database: postgres.Queryable, order_id: int):
|
|
279
312
|
return await database.query("SELECT * FROM orders WHERE id = $1", order_id).first()
|
|
@@ -355,6 +388,65 @@ in a top-level Pydantic model and pass that class directly. Original bytes remai
|
|
|
355
388
|
available as `message.raw_data`. Payloads are decoded once before the handler is
|
|
356
389
|
called; decoding errors propagate for normal retry handling.
|
|
357
390
|
|
|
391
|
+
## Gateway Middleware
|
|
392
|
+
|
|
393
|
+
A service with a `gateway` section in its `encore.service.json` hosts the app's
|
|
394
|
+
gateway, and its program serves it with Middleware: code that authenticates,
|
|
395
|
+
limits, logs or meters the public requests the gateway serves. Encore runs it on
|
|
396
|
+
the gateway variant of the native runtime.
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
import asyncio
|
|
400
|
+
from dataclasses import dataclass
|
|
401
|
+
|
|
402
|
+
import encore
|
|
403
|
+
from encore import gateway, reqctx
|
|
404
|
+
from encore.errs import ErrCode
|
|
405
|
+
from encore.gateway import match
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@dataclass
|
|
409
|
+
class User:
|
|
410
|
+
id: str
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
current_user = reqctx.Field[User]("user")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class Authenticate(gateway.Middleware):
|
|
417
|
+
match = match.path_prefix("/api")
|
|
418
|
+
|
|
419
|
+
async def on_request(self, req: gateway.Request) -> None:
|
|
420
|
+
user = await verify(req.header("authorization"))
|
|
421
|
+
if user is None:
|
|
422
|
+
raise gateway.reject(ErrCode.UNAUTHENTICATED, "missing token")
|
|
423
|
+
req.set(current_user, user)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
async def main() -> None:
|
|
427
|
+
await encore.start()
|
|
428
|
+
await gateway.serve(Authenticate())
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
asyncio.run(main())
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
A Middleware is called at the hooks it defines (`on_request`, `on_request_body`,
|
|
435
|
+
`on_response`, `on_response_body`, `on_complete`), only for the requests its
|
|
436
|
+
`match` matches: Encore checks that before a request reaches Python. A hook may
|
|
437
|
+
read any header, query parameter or cookie; each reaches Python only as the hook
|
|
438
|
+
reads it. Encore checks a subclass when it is defined, so a misspelt hook raises
|
|
439
|
+
at once.
|
|
440
|
+
|
|
441
|
+
`on_request` sets the request's Request Context fields with
|
|
442
|
+
`req.set(field, value)`: the services the request reaches read them with
|
|
443
|
+
`field.get()`, and no client can set them. Every hook runs in the request's
|
|
444
|
+
trace and context, so it reads them with `field.get()` too, or `req.get(field)`,
|
|
445
|
+
and the calls it makes to services carry both.
|
|
446
|
+
|
|
447
|
+
`gateway.serve` handles SIGTERM while it serves, so hooks keep running while the
|
|
448
|
+
gateway drains, and returns once the shutdown has completed.
|
|
449
|
+
|
|
358
450
|
## Application-owned shutdown
|
|
359
451
|
|
|
360
452
|
Without a hook, Encore shuts down non-cooperatively as the interpreter exits:
|
|
@@ -40,6 +40,29 @@ async def place_order(order):
|
|
|
40
40
|
await events.publish({"id": order.id})
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Secrets
|
|
44
|
+
|
|
45
|
+
`secrets.Secret[T]("Name")` belongs to the service that declares it.
|
|
46
|
+
`secrets.SharedSecret[T]("Name")` is one secret for the whole app, given to the
|
|
47
|
+
services that read it, and `secrets.GlobalSecret[T]("Name")` one that every
|
|
48
|
+
service is given. `T` is `str` or `bytes`, and `value` reads the secret decoded
|
|
49
|
+
as that.
|
|
50
|
+
|
|
51
|
+
A shared secret is declared once in the app. Another Python service can't
|
|
52
|
+
import the declaring service's modules, so it references the secret by name
|
|
53
|
+
instead:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from encore.secrets import SharedSecret
|
|
57
|
+
|
|
58
|
+
support_email = SharedSecret[str].named("SupportEmail")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Reading `value` makes the service one the secret is given to. The declaration
|
|
62
|
+
can be in any language, and the type argument is how this service decodes the
|
|
63
|
+
value, whatever the declaration's. Encore reports a reference to a name that
|
|
64
|
+
isn't declared as a shared secret.
|
|
65
|
+
|
|
43
66
|
## Service URLs
|
|
44
67
|
|
|
45
68
|
Use `base_url` to look up a service's configured base URL:
|
|
@@ -119,7 +142,7 @@ class User:
|
|
|
119
142
|
|
|
120
143
|
|
|
121
144
|
current_user = reqctx.Field[User]("user")
|
|
122
|
-
tenant = reqctx.Field[str]("tenant",
|
|
145
|
+
tenant = reqctx.Field[str]("tenant", trace=True)
|
|
123
146
|
```
|
|
124
147
|
|
|
125
148
|
Read a field while handling a request, and set or clear one for the calls made
|
|
@@ -141,7 +164,7 @@ that the types agree.
|
|
|
141
164
|
|
|
142
165
|
- `pubsub=True` also carries the field on the Pub/Sub messages published with
|
|
143
166
|
it set, into the handlers that receive them.
|
|
144
|
-
- `
|
|
167
|
+
- `trace=True` puts the value on the request's spans, as
|
|
145
168
|
`encore.ctx.<name>`. Use it for identifiers worth finding traces by, never
|
|
146
169
|
for secrets.
|
|
147
170
|
|
|
@@ -254,6 +277,16 @@ Entering the context does not execute SQL. Exiting it closes the query.
|
|
|
254
277
|
`exec(sql, *params)` returns an `ExecResult` with `rows_affected`. Rows are
|
|
255
278
|
dictionaries keyed by column name. Values are bound with `$1`, `$2`, etc.
|
|
256
279
|
|
|
280
|
+
Column values have the Python types asyncpg and psycopg use. `numeric` is a
|
|
281
|
+
`decimal.Decimal` with the column's scale, so a `numeric(10,2)` holding 7 is
|
|
282
|
+
`Decimal('7.00')`; `NaN` and `±Infinity` are `Decimal('NaN')` and
|
|
283
|
+
`Decimal('Infinity')`. `timestamptz` is an aware `datetime.datetime` in UTC,
|
|
284
|
+
`timestamp` a naive one, and `date` and `time` are `datetime.date` and
|
|
285
|
+
`datetime.time`. `bytea` is `bytes`, arrays are lists of their elements' types,
|
|
286
|
+
JSON is decoded, and `uuid`, `inet` and `cidr` are strings. Parameters accept
|
|
287
|
+
the same types; a naive `datetime` bound to a `timestamptz` is taken as UTC,
|
|
288
|
+
and an aware one may use any `tzinfo`, `zoneinfo.ZoneInfo` included.
|
|
289
|
+
|
|
257
290
|
```python
|
|
258
291
|
async def find_order(database: postgres.Queryable, order_id: int):
|
|
259
292
|
return await database.query("SELECT * FROM orders WHERE id = $1", order_id).first()
|
|
@@ -335,6 +368,65 @@ in a top-level Pydantic model and pass that class directly. Original bytes remai
|
|
|
335
368
|
available as `message.raw_data`. Payloads are decoded once before the handler is
|
|
336
369
|
called; decoding errors propagate for normal retry handling.
|
|
337
370
|
|
|
371
|
+
## Gateway Middleware
|
|
372
|
+
|
|
373
|
+
A service with a `gateway` section in its `encore.service.json` hosts the app's
|
|
374
|
+
gateway, and its program serves it with Middleware: code that authenticates,
|
|
375
|
+
limits, logs or meters the public requests the gateway serves. Encore runs it on
|
|
376
|
+
the gateway variant of the native runtime.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
import asyncio
|
|
380
|
+
from dataclasses import dataclass
|
|
381
|
+
|
|
382
|
+
import encore
|
|
383
|
+
from encore import gateway, reqctx
|
|
384
|
+
from encore.errs import ErrCode
|
|
385
|
+
from encore.gateway import match
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@dataclass
|
|
389
|
+
class User:
|
|
390
|
+
id: str
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
current_user = reqctx.Field[User]("user")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class Authenticate(gateway.Middleware):
|
|
397
|
+
match = match.path_prefix("/api")
|
|
398
|
+
|
|
399
|
+
async def on_request(self, req: gateway.Request) -> None:
|
|
400
|
+
user = await verify(req.header("authorization"))
|
|
401
|
+
if user is None:
|
|
402
|
+
raise gateway.reject(ErrCode.UNAUTHENTICATED, "missing token")
|
|
403
|
+
req.set(current_user, user)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def main() -> None:
|
|
407
|
+
await encore.start()
|
|
408
|
+
await gateway.serve(Authenticate())
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
asyncio.run(main())
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
A Middleware is called at the hooks it defines (`on_request`, `on_request_body`,
|
|
415
|
+
`on_response`, `on_response_body`, `on_complete`), only for the requests its
|
|
416
|
+
`match` matches: Encore checks that before a request reaches Python. A hook may
|
|
417
|
+
read any header, query parameter or cookie; each reaches Python only as the hook
|
|
418
|
+
reads it. Encore checks a subclass when it is defined, so a misspelt hook raises
|
|
419
|
+
at once.
|
|
420
|
+
|
|
421
|
+
`on_request` sets the request's Request Context fields with
|
|
422
|
+
`req.set(field, value)`: the services the request reaches read them with
|
|
423
|
+
`field.get()`, and no client can set them. Every hook runs in the request's
|
|
424
|
+
trace and context, so it reads them with `field.get()` too, or `req.get(field)`,
|
|
425
|
+
and the calls it makes to services carry both.
|
|
426
|
+
|
|
427
|
+
`gateway.serve` handles SIGTERM while it serves, so hooks keep running while the
|
|
428
|
+
gateway drains, and returns once the shutdown has completed.
|
|
429
|
+
|
|
338
430
|
## Application-owned shutdown
|
|
339
431
|
|
|
340
432
|
Without a hook, Encore shuts down non-cooperatively as the interpreter exits:
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Encore's error codes, and the error a request is answered with.
|
|
2
|
+
|
|
3
|
+
An [APIError] is answered as Encore answers an error: with its code's HTTP
|
|
4
|
+
status, and a JSON body Encore's clients read as the error::
|
|
5
|
+
|
|
6
|
+
{"code": "unauthenticated", "message": "missing token", "details": null}
|
|
7
|
+
|
|
8
|
+
Typical usage example, in a gateway's Middleware::
|
|
9
|
+
|
|
10
|
+
from encore.errs import APIError, ErrCode
|
|
11
|
+
|
|
12
|
+
async def on_request(self, req):
|
|
13
|
+
if req.header("authorization") is None:
|
|
14
|
+
raise APIError(ErrCode.UNAUTHENTICATED, "missing token")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
__all__ = ["APIError", "ErrCode"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ErrCode(Enum):
|
|
25
|
+
"""An Encore error code, and the HTTP status it answers with.
|
|
26
|
+
|
|
27
|
+
The values are the codes as Encore's error bodies spell them.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
CANCELED = "canceled"
|
|
31
|
+
UNKNOWN = "unknown"
|
|
32
|
+
INVALID_ARGUMENT = "invalid_argument"
|
|
33
|
+
DEADLINE_EXCEEDED = "deadline_exceeded"
|
|
34
|
+
NOT_FOUND = "not_found"
|
|
35
|
+
ALREADY_EXISTS = "already_exists"
|
|
36
|
+
PERMISSION_DENIED = "permission_denied"
|
|
37
|
+
RESOURCE_EXHAUSTED = "resource_exhausted"
|
|
38
|
+
FAILED_PRECONDITION = "failed_precondition"
|
|
39
|
+
ABORTED = "aborted"
|
|
40
|
+
OUT_OF_RANGE = "out_of_range"
|
|
41
|
+
UNIMPLEMENTED = "unimplemented"
|
|
42
|
+
INTERNAL = "internal"
|
|
43
|
+
UNAVAILABLE = "unavailable"
|
|
44
|
+
DATA_LOSS = "data_loss"
|
|
45
|
+
UNAUTHENTICATED = "unauthenticated"
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def http_status(self) -> int:
|
|
49
|
+
"""The HTTP status the code answers with.
|
|
50
|
+
|
|
51
|
+
401 for ``UNAUTHENTICATED``, 404 for ``NOT_FOUND``, and so on.
|
|
52
|
+
"""
|
|
53
|
+
return _HTTP_STATUS[self]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_HTTP_STATUS = {
|
|
57
|
+
ErrCode.CANCELED: 499,
|
|
58
|
+
ErrCode.UNKNOWN: 500,
|
|
59
|
+
ErrCode.INVALID_ARGUMENT: 400,
|
|
60
|
+
ErrCode.DEADLINE_EXCEEDED: 504,
|
|
61
|
+
ErrCode.NOT_FOUND: 404,
|
|
62
|
+
ErrCode.ALREADY_EXISTS: 409,
|
|
63
|
+
ErrCode.PERMISSION_DENIED: 403,
|
|
64
|
+
ErrCode.RESOURCE_EXHAUSTED: 429,
|
|
65
|
+
ErrCode.FAILED_PRECONDITION: 400,
|
|
66
|
+
ErrCode.ABORTED: 409,
|
|
67
|
+
ErrCode.OUT_OF_RANGE: 400,
|
|
68
|
+
ErrCode.UNIMPLEMENTED: 501,
|
|
69
|
+
ErrCode.INTERNAL: 500,
|
|
70
|
+
ErrCode.UNAVAILABLE: 503,
|
|
71
|
+
ErrCode.DATA_LOSS: 500,
|
|
72
|
+
ErrCode.UNAUTHENTICATED: 401,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class APIError(Exception):
|
|
77
|
+
"""An error with an Encore error code, answered as Encore answers one.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
code: The error's code.
|
|
81
|
+
message: What went wrong, for the client.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, code: ErrCode, message: str) -> None:
|
|
85
|
+
"""Make an error with ``code`` and ``message``."""
|
|
86
|
+
if not isinstance(code, ErrCode):
|
|
87
|
+
raise TypeError(f"code must be an ErrCode, not {type(code).__name__}")
|
|
88
|
+
super().__init__(f"{code.value}: {message}")
|
|
89
|
+
self.code = code
|
|
90
|
+
self.message = message
|