encore-dev 2.0.0a7__py3-none-any.whl

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 (44) hide show
  1. encore/__init__.py +43 -0
  2. encore/cron/__init__.py +50 -0
  3. encore/internal/__init__.py +1 -0
  4. encore/internal/_loader.py +79 -0
  5. encore/internal/_native.pyi +734 -0
  6. encore/internal/lazy.py +35 -0
  7. encore/internal/runtime.py +133 -0
  8. encore/internal/startup.py +24 -0
  9. encore/logs/__init__.py +19 -0
  10. encore/logs/_console.py +155 -0
  11. encore/logs/_runtime.py +56 -0
  12. encore/meta/__init__.py +228 -0
  13. encore/metrics/__init__.py +390 -0
  14. encore/pubsub/__init__.py +425 -0
  15. encore/py.typed +0 -0
  16. encore/secrets/__init__.py +139 -0
  17. encore/shutdown/__init__.py +164 -0
  18. encore/storage/__init__.py +5 -0
  19. encore/storage/objects/__init__.py +396 -0
  20. encore/storage/postgres/__init__.py +737 -0
  21. encore/storage/redis/__init__.py +200 -0
  22. encore/storage/redis/_base.py +42 -0
  23. encore/storage/redis/_responses.py +150 -0
  24. encore/storage/redis/_values.py +189 -0
  25. encore/storage/redis/bitmap.py +142 -0
  26. encore/storage/redis/generic.py +367 -0
  27. encore/storage/redis/geo.py +508 -0
  28. encore/storage/redis/hash.py +504 -0
  29. encore/storage/redis/json.py +614 -0
  30. encore/storage/redis/list.py +511 -0
  31. encore/storage/redis/pubsub.py +276 -0
  32. encore/storage/redis/set.py +570 -0
  33. encore/storage/redis/sorted_set.py +753 -0
  34. encore/storage/redis/stream.py +773 -0
  35. encore/storage/redis/string.py +654 -0
  36. encore/tracing/__init__.py +19 -0
  37. encore/tracing/_context.py +27 -0
  38. encore/tracing/_encode.py +351 -0
  39. encore/tracing/_provider.py +62 -0
  40. encore/tracing/_span.py +226 -0
  41. encore/tracing/_tracer.py +93 -0
  42. encore_dev-2.0.0a7.dist-info/METADATA +791 -0
  43. encore_dev-2.0.0a7.dist-info/RECORD +44 -0
  44. encore_dev-2.0.0a7.dist-info/WHEEL +4 -0
@@ -0,0 +1,425 @@
1
+ """Pub/Sub topics and subscriptions.
2
+
3
+ Typical usage example:
4
+
5
+ from encore import pubsub
6
+
7
+ orders = pubsub.Topic(
8
+ "orders",
9
+ exactly_once=True,
10
+ )
11
+
12
+ async def place_order(order):
13
+ await orders.publish({"id": order.id})
14
+
15
+ ordered = pubsub.OrderedTopic(
16
+ "ordered-orders",
17
+ exactly_once=True,
18
+ )
19
+
20
+ async def place_order_in_sequence(order):
21
+ await ordered.publish({"id": order.id}, ordering_key=str(order.id))
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ from dataclasses import dataclass
28
+ from datetime import datetime
29
+ import json
30
+ import types
31
+ from typing import (
32
+ Any,
33
+ Awaitable,
34
+ Callable,
35
+ Generic,
36
+ ForwardRef,
37
+ TYPE_CHECKING,
38
+ TypeVar,
39
+ Union,
40
+ get_args,
41
+ get_origin,
42
+ )
43
+
44
+ from ..internal.lazy import Lazy
45
+ from ..internal.runtime import _native, runtime
46
+ from ..tracing._context import active_ctx_bytes
47
+
48
+ try:
49
+ from pydantic import BaseModel
50
+ except ImportError:
51
+ BaseModel = None
52
+
53
+
54
+ _UNION_ORIGINS = (Union, getattr(types, "UnionType", Union))
55
+ _json_ref = ForwardRef("JSON", module=__name__)
56
+ # Union syntax keeps these aliases compatible with Python 3.9.
57
+
58
+ if TYPE_CHECKING:
59
+ JSON = Union[dict[str, "JSON"], list["JSON"], str, int, float, bool, None]
60
+
61
+ from pydantic import BaseModel as _PydanticBaseModel
62
+
63
+ Payload = Union[_PydanticBaseModel, JSON, bytes]
64
+ else:
65
+ # Resolve the recursive alias in this module, even when a handler imports
66
+ # pubsub under an alias or imports Message without importing JSON directly.
67
+ JSON = Union[dict[str, _json_ref], list[_json_ref], str, int, float, bool, None]
68
+ Payload = (
69
+ Union[BaseModel, JSON, bytes] if BaseModel is not None else Union[JSON, bytes]
70
+ )
71
+
72
+ _T = TypeVar("_T", bound=Payload)
73
+
74
+
75
+ def _validate_payload_type(payload_type: Any) -> None:
76
+ """Check decoder support, without validating individual JSON values."""
77
+ if payload_type is bytes or (
78
+ BaseModel is not None
79
+ and isinstance(payload_type, type)
80
+ and issubclass(payload_type, BaseModel)
81
+ ):
82
+ return
83
+
84
+ pending = [payload_type]
85
+ seen: set[int] = set()
86
+ while pending:
87
+ current = pending.pop()
88
+ if id(current) in seen:
89
+ continue
90
+ seen.add(id(current))
91
+ if current in (Any, str, int, float, bool, type(None), None, list, dict):
92
+ continue
93
+ # Only our recursive JSON alias is supported, not arbitrary forward refs.
94
+ if current is _json_ref:
95
+ continue
96
+ origin, args = get_origin(current), get_args(current)
97
+ if origin in _UNION_ORIGINS or origin is list:
98
+ pending.extend(args)
99
+ elif origin is dict and (not args or args[0] in (str, Any)):
100
+ pending.extend(args[1:])
101
+ else:
102
+ raise TypeError(
103
+ f"Unsupported Pub/Sub payload type {payload_type!r}. Use JSON "
104
+ "types, bytes, or a top-level Pydantic v2 model. "
105
+ "Use Subscription[bytes] for custom decoding. "
106
+ "Pass types directly rather than as string forward references."
107
+ )
108
+
109
+
110
+ def _decode_message_data(payload_type: Any, data: bytes) -> Payload:
111
+ """Decode using the subscription's validated type argument.
112
+
113
+ Bytes pass through unchanged. Pydantic v2 models validate their own JSON.
114
+ Other supported annotations use ordinary JSON decoding; nested annotations
115
+ do not coerce or validate JSON values. Use a model for field validation.
116
+ """
117
+ if payload_type is bytes:
118
+ return data
119
+
120
+ if (
121
+ BaseModel is not None
122
+ and isinstance(payload_type, type)
123
+ and issubclass(payload_type, BaseModel)
124
+ ):
125
+ try:
126
+ return payload_type.model_validate_json(data)
127
+ except ValueError as error:
128
+ raise ValueError(
129
+ f"Cannot decode Pub/Sub message as {payload_type.__name__}. "
130
+ f"Update the published payload or the model fields. {error}"
131
+ ) from error
132
+
133
+ try:
134
+ value = json.loads(data)
135
+ except json.JSONDecodeError as error:
136
+ raise ValueError(
137
+ f"Cannot decode Pub/Sub message as JSON at line {error.lineno}, "
138
+ f"column {error.colno}: {error.msg}. Publish valid JSON or use "
139
+ "Subscription[bytes] to handle the raw payload."
140
+ ) from error
141
+ except UnicodeDecodeError as error:
142
+ raise ValueError(
143
+ "Cannot decode Pub/Sub message: invalid text encoding. "
144
+ "Publish JSON encoded as UTF-8, UTF-16, or UTF-32, or use "
145
+ "Subscription[bytes] to handle the raw payload."
146
+ ) from error
147
+
148
+ return value
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class Message(Generic[_T]):
153
+ """A delivered message with its decoded payload and provider metadata.
154
+
155
+ Attributes:
156
+ data: The payload decoded according to the subscription's type argument.
157
+ raw_data: The original payload bytes.
158
+ attributes: Message attributes supplied alongside the payload.
159
+ id: The provider-assigned message identifier.
160
+ delivery_attempt: The delivery attempt number, starting at one.
161
+ publish_time: The publication time as a UTC datetime.
162
+ ordering_key: The ordering key, if supplied.
163
+ """
164
+
165
+ data: _T
166
+ raw_data: bytes
167
+ attributes: dict[str, str]
168
+ id: str
169
+ delivery_attempt: int
170
+ publish_time: datetime
171
+ ordering_key: str | None
172
+
173
+ @classmethod
174
+ def _from_native(cls, message: _native.PubSubMessage, data: _T) -> Message:
175
+ return cls(
176
+ data=data,
177
+ raw_data=message.data,
178
+ attributes=message.attributes,
179
+ id=message.id,
180
+ delivery_attempt=message.delivery_attempt,
181
+ publish_time=message.publish_time,
182
+ ordering_key=message.ordering_key,
183
+ )
184
+
185
+
186
+ def _serialize_payload(value: Any) -> bytes:
187
+ if isinstance(value, bytes):
188
+ return value
189
+
190
+ if BaseModel is not None and isinstance(value, BaseModel):
191
+ return value.model_dump_json().encode("utf-8")
192
+
193
+ try:
194
+ return json.dumps(value).encode("utf-8")
195
+ except (TypeError, ValueError) as error:
196
+ raise TypeError(
197
+ "Cannot publish Pub/Sub message. Use JSON values, bytes, or a top-level "
198
+ "Pydantic v2 model. Dataclasses and models nested in plain JSON containers "
199
+ "are not supported."
200
+ ) from error
201
+
202
+
203
+ class Topic(Generic[_T]):
204
+ """A pub/sub topic declared at module scope."""
205
+
206
+ def __init__(
207
+ self,
208
+ name: str,
209
+ *,
210
+ exactly_once: bool = False,
211
+ ) -> None:
212
+ """Declare a pub/sub topic.
213
+
214
+ Args:
215
+ name: The topic's resource name.
216
+ exactly_once: Whether to request exactly-once delivery instead of
217
+ the default at-least-once delivery.
218
+ """
219
+ self._name = name
220
+ self._exactly_once = exactly_once
221
+ self._handle = Lazy(lambda: runtime().pubsub_topic(name))
222
+
223
+ @property
224
+ def _impl(self) -> _native.PubSubTopic:
225
+ return self._handle.get()
226
+
227
+ @classmethod
228
+ def named(cls, name: str) -> Topic[_T]:
229
+ """Reference an existing topic declared by another service.
230
+
231
+ The declaring service may use another language. Construct Topic
232
+ directly to declare a new topic.
233
+
234
+ Args:
235
+ name: The topic name as a string literal for static resolution.
236
+
237
+ Returns:
238
+ A reference to the configured topic.
239
+ """
240
+ return cls(name)
241
+
242
+ async def publish(
243
+ self, message: _T, attributes: dict[str, str] | None = None
244
+ ) -> str:
245
+ """Publish a message and return its provider-assigned identifier.
246
+
247
+ Args:
248
+ message: A JSON value, bytes, or a top-level Pydantic v2 model.
249
+ attributes: Optional string metadata to send alongside the payload.
250
+
251
+ Returns:
252
+ The published message identifier.
253
+
254
+ Raises:
255
+ TypeError: The payload cannot be serialized as a supported type.
256
+ """
257
+ return await self._impl.publish(
258
+ _serialize_payload(message), attributes, source=active_ctx_bytes()
259
+ )
260
+
261
+
262
+ class OrderedTopic(Generic[_T]):
263
+ """A ordered pub/sub topic declared at module scope."""
264
+
265
+ def __init__(
266
+ self,
267
+ name: str,
268
+ *,
269
+ exactly_once: bool = False,
270
+ ) -> None:
271
+ """Declare a ordered pub/sub topic.
272
+
273
+ Args:
274
+ name: The topic's resource name.
275
+ exactly_once: Whether to request exactly-once delivery instead of
276
+ the default at-least-once delivery.
277
+ """
278
+ self._name = name
279
+ self._exactly_once = exactly_once
280
+ self._handle = Lazy(lambda: runtime().pubsub_topic(name))
281
+
282
+ @property
283
+ def _impl(self) -> _native.PubSubTopic:
284
+ return self._handle.get()
285
+
286
+ @classmethod
287
+ def named(cls, name: str) -> OrderedTopic[_T]:
288
+ """Reference an existing topic declared by another service.
289
+
290
+ The declaring service may use another language. Construct OrderedTopic
291
+ directly to declare a new topic.
292
+
293
+ Args:
294
+ name: The topic name as a string literal for static resolution.
295
+
296
+ Returns:
297
+ A reference to the configured topic.
298
+ """
299
+ return cls(name)
300
+
301
+ async def publish(
302
+ self, message: _T, ordering_key: str, attributes: dict[str, str] | None = None
303
+ ) -> str:
304
+ """Publish a message and return its provider-assigned identifier.
305
+
306
+ Args:
307
+ message: A JSON value, bytes, or a top-level Pydantic v2 model.
308
+ ordering_key: The key grouping messages for ordered delivery.
309
+ attributes: Optional string metadata to send alongside the payload.
310
+
311
+ Returns:
312
+ The published message identifier.
313
+
314
+ Raises:
315
+ TypeError: The payload cannot be serialized as a supported type.
316
+ """
317
+ return await self._impl.publish(
318
+ _serialize_payload(message),
319
+ attributes,
320
+ source=active_ctx_bytes(),
321
+ ordering_key=ordering_key,
322
+ )
323
+
324
+
325
+ _subscriptions: list[Subscription] = []
326
+
327
+
328
+ class SubscriptionLoopError(RuntimeError):
329
+ """The subscription belongs to another asyncio loop."""
330
+
331
+
332
+ class Subscription(Generic[_T]):
333
+ """A subscription and async handler declared at module scope.
334
+
335
+ Await encore.start() to begin consumption on the application's asyncio
336
+ loop. The explicit type argument selects payload decoding; omitting it
337
+ uses ordinary JSON decoding.
338
+ """
339
+
340
+ def __init__(
341
+ self,
342
+ topic: Topic[_T] | OrderedTopic[_T],
343
+ name: str,
344
+ *,
345
+ handler: Callable[[Message[_T]], Awaitable[None]],
346
+ ack_deadline: str | None = None,
347
+ message_retention: str | None = None,
348
+ min_backoff: str | None = None,
349
+ max_backoff: str | None = None,
350
+ max_retries: int | None = None,
351
+ max_concurrency: int | None = None,
352
+ ) -> None:
353
+ """Declare a subscription and register its handler for startup.
354
+
355
+ Timing and retry options configure provisioning through Encore's parser.
356
+ None leaves an option at its platform default.
357
+
358
+ Args:
359
+ topic: The topic to subscribe to.
360
+ name: The subscription name within the topic.
361
+ handler: An async callable receiving each decoded Message.
362
+ ack_deadline: The acknowledgement deadline as a duration string.
363
+ message_retention: The message retention period as a duration string.
364
+ min_backoff: The minimum retry delay as a duration string.
365
+ max_backoff: The maximum retry delay as a duration string.
366
+ max_retries: The maximum number of delivery retries.
367
+ max_concurrency: The maximum number of concurrent handler calls.
368
+
369
+ Raises:
370
+ TypeError: The handler is not callable.
371
+ """
372
+ if not callable(handler):
373
+ raise TypeError(
374
+ "Subscription handler must be callable. "
375
+ "Pass an async function, for example handler=handle_order, "
376
+ "rather than calling it with handler=handle_order()."
377
+ )
378
+ self._topic = topic
379
+ self._name = name
380
+ self._handler = handler
381
+ self._handle = Lazy(self._resolve)
382
+ self._consumer: asyncio.Future[None] | None = None
383
+ _subscriptions.append(self)
384
+
385
+ def _resolve(self) -> _native.PubSubSubscription:
386
+ # Generic instance metadata is available after construction, at startup.
387
+ args = get_args(getattr(self, "__orig_class__", None))
388
+ tp = args[0] if args else JSON
389
+ _validate_payload_type(tp)
390
+
391
+ async def dispatch(message: _native.PubSubMessage) -> None:
392
+ data = _decode_message_data(tp, message.data)
393
+ await self._handler(Message._from_native(message, data))
394
+
395
+ return runtime().pubsub_subscription(
396
+ _native.PubSubSubscriptionConfig(
397
+ topic_name=self._topic._name,
398
+ subscription_name=self._name,
399
+ handler=dispatch,
400
+ )
401
+ )
402
+
403
+ def _check_loop(self) -> None:
404
+ if self._consumer is not None:
405
+ if self._consumer.get_loop() is not asyncio.get_running_loop():
406
+ raise SubscriptionLoopError(
407
+ "subscription already started on a different asyncio loop"
408
+ )
409
+
410
+ def _start(self) -> asyncio.Future[None]:
411
+ self._check_loop()
412
+ if self._consumer is None:
413
+ self._consumer = asyncio.ensure_future(self._handle.get().subscribe())
414
+ self._consumer.add_done_callback(self._consumer_done)
415
+ return self._consumer
416
+
417
+ def _consumer_done(self, consumer: asyncio.Future[None]) -> None:
418
+ if not consumer.cancelled() and (error := consumer.exception()) is not None:
419
+ consumer.get_loop().call_exception_handler(
420
+ {
421
+ "message": f"Encore subscription {self._topic._name}/{self._name} failed",
422
+ "exception": error,
423
+ "future": consumer,
424
+ }
425
+ )
encore/py.typed ADDED
File without changes
@@ -0,0 +1,139 @@
1
+ """Typed application secrets declared at module scope.
2
+
3
+ Secret[T] belongs to its declaring service. SharedSecret[T] declares a
4
+ shared secret with tracked usage. GlobalSecret[T] is available to every
5
+ service without usage tracking. Each exposes its current value through
6
+ value and requires an explicit str or bytes type argument.
7
+
8
+ Service-private secrets have a separate namespace from shared and global
9
+ secrets. SharedSecret and GlobalSecret use the same namespace.
10
+
11
+ Typical usage example:
12
+ from encore.secrets import Secret, SharedSecret
13
+
14
+ shared_api_key = SharedSecret[str]("api_key")
15
+ api_key = Secret[str]("api_key")
16
+ """
17
+
18
+ from __future__ import annotations
19
+ from typing import Callable, Generic, Optional, TypeVar, cast, get_args
20
+
21
+ from ..internal.lazy import Lazy
22
+ from ..internal.runtime import _native, runtime
23
+
24
+ _T = TypeVar("_T", str, bytes)
25
+
26
+
27
+ class _SecretValue(Generic[_T]):
28
+ def __init__(
29
+ self, name: str, load: Callable[[str], Optional[_native.Secret]]
30
+ ) -> None:
31
+ self._name = name
32
+ self._handle = Lazy(lambda: load(name))
33
+
34
+ def read(self, value_type: type[str] | type[bytes]) -> _T:
35
+ if value_type is not str and value_type is not bytes:
36
+ raise TypeError(
37
+ f"For {type(self).__name__}[T](name) T must be str or bytes"
38
+ )
39
+ secret = self._handle.get()
40
+ if secret is None:
41
+ raise KeyError(f"secret {self._name!r} is not configured")
42
+ raw = secret.get_bytes()
43
+
44
+ if value_type is bytes:
45
+ return cast(_T, raw)
46
+
47
+ return cast(_T, raw.decode("utf-8"))
48
+
49
+
50
+ class _Secret(Generic[_T]):
51
+ """Common public behavior, backed by a scope-specific lookup."""
52
+
53
+ def __init__(
54
+ self, name: str, load: Callable[[str], Optional[_native.Secret]]
55
+ ) -> None:
56
+ self._name = name
57
+ self._value: _SecretValue[_T] = _SecretValue(name, load)
58
+
59
+ @property
60
+ def name(self) -> str:
61
+ """The original name, without an internal namespace prefix."""
62
+ return self._name
63
+
64
+ @property
65
+ def value(self) -> _T:
66
+ """The secret value decoded using the constructor type argument.
67
+
68
+ A variable annotation alone does not supply the runtime type.
69
+
70
+ Raises:
71
+ TypeError: The constructor type argument is missing or is neither
72
+ str nor bytes.
73
+ KeyError: The secret is not configured.
74
+ UnicodeDecodeError: A str secret contains invalid UTF-8.
75
+ """
76
+ args = get_args(getattr(self, "__orig_class__", None))
77
+ if not args:
78
+ raise TypeError(f"Use {type(self).__name__}[T](name) with an explicit type")
79
+ return self._value.read(args[0])
80
+
81
+ def __repr__(self) -> str:
82
+ return f"{type(self).__name__}({self._name!r}: *********)"
83
+
84
+
85
+ class Secret(_Secret[_T]):
86
+ """A secret belonging only to the service that declares it.
87
+
88
+ The type parameter must be specified explicitly.
89
+
90
+ Example:
91
+ secret = Secret[str]("ApiKey")
92
+ """
93
+
94
+ def __init__(self, name: str) -> None:
95
+ """Declare a service-private secret at module scope.
96
+
97
+ Args:
98
+ name: The configured secret name. Instantiate with an explicit str
99
+ or bytes type argument, such as Secret[str]("ApiKey").
100
+ """
101
+ super().__init__(name, lambda key: runtime().secret(key))
102
+
103
+
104
+ class SharedSecret(_Secret[_T]):
105
+ """A shared secret with tracked usage; may be declared outside services.
106
+
107
+ The type parameter must be specified explicitly.
108
+
109
+ Example:
110
+ secret = SharedSecret[str]("ApiKey")
111
+ """
112
+
113
+ def __init__(self, name: str) -> None:
114
+ """Declare a shared secret at module scope.
115
+
116
+ Args:
117
+ name: The configured secret name. Instantiate with an explicit str
118
+ or bytes type argument, such as SharedSecret[str]("ApiKey").
119
+ """
120
+ super().__init__(name, lambda key: runtime().shared_secret(key))
121
+
122
+
123
+ class GlobalSecret(_Secret[_T]):
124
+ """A global secret available to every service without usage tracking.
125
+
126
+ The type parameter must be specified explicitly.
127
+
128
+ Example:
129
+ secret = GlobalSecret[str]("ApiKey")
130
+ """
131
+
132
+ def __init__(self, name: str) -> None:
133
+ """Declare a global secret at module scope.
134
+
135
+ Args:
136
+ name: The configured secret name. Instantiate with an explicit str
137
+ or bytes type argument, such as GlobalSecret[str]("ApiKey").
138
+ """
139
+ super().__init__(name, lambda key: runtime().shared_secret(key))