encore-dev 2.0.0b6__tar.gz → 2.0.0b7__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 (71) hide show
  1. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/PKG-INFO +1 -1
  2. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/_run.py +9 -0
  3. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/_native.pyi +15 -1
  4. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/runtime.py +24 -22
  5. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/startup.py +27 -1
  6. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/pubsub/__init__.py +10 -3
  7. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/pyproject.toml +1 -1
  8. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/pyproject.toml.orig +1 -1
  9. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/README.md +0 -0
  10. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/__init__.py +0 -0
  11. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/config/__init__.py +0 -0
  12. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/config/_build.py +0 -0
  13. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/config/_markers.py +0 -0
  14. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/cron/__init__.py +0 -0
  15. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/env/__init__.py +0 -0
  16. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/errs/__init__.py +0 -0
  17. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/gateway/__init__.py +0 -0
  18. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/gateway/_serve.py +0 -0
  19. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/gateway/match.py +0 -0
  20. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/__init__.py +0 -0
  21. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/_loader.py +0 -0
  22. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/lazy.py +0 -0
  23. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/internal/reqfields.py +0 -0
  24. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/logs/__init__.py +0 -0
  25. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/logs/_console.py +0 -0
  26. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/logs/_runtime.py +0 -0
  27. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/metrics/__init__.py +0 -0
  28. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/metrics/_instrument.py +0 -0
  29. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/metrics/_metrics.py +0 -0
  30. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/metrics/_provider.py +0 -0
  31. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/py.typed +0 -0
  32. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/reqctx/__init__.py +0 -0
  33. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/reqctx/_json.py +0 -0
  34. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/secrets/__init__.py +0 -0
  35. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/server/__init__.py +0 -0
  36. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/server/_serve.py +0 -0
  37. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/server/_span.py +0 -0
  38. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/server/_tracing.py +0 -0
  39. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/server/asgi.py +0 -0
  40. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/server/wsgi.py +0 -0
  41. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/services/__init__.py +0 -0
  42. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/services/_propagate.py +0 -0
  43. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/services/aiohttp.py +0 -0
  44. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/services/httpx.py +0 -0
  45. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/services/niquests.py +0 -0
  46. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/services/requests.py +0 -0
  47. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/shutdown/__init__.py +0 -0
  48. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/__init__.py +0 -0
  49. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/objects/__init__.py +0 -0
  50. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/postgres/__init__.py +0 -0
  51. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/__init__.py +0 -0
  52. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/_base.py +0 -0
  53. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/_responses.py +0 -0
  54. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/_values.py +0 -0
  55. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/bitmap.py +0 -0
  56. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/generic.py +0 -0
  57. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/geo.py +0 -0
  58. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/hash.py +0 -0
  59. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/json.py +0 -0
  60. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/list.py +0 -0
  61. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/pubsub.py +0 -0
  62. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/set.py +0 -0
  63. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/sorted_set.py +0 -0
  64. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/stream.py +0 -0
  65. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/storage/redis/string.py +0 -0
  66. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/tracing/__init__.py +0 -0
  67. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/tracing/_context.py +0 -0
  68. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/tracing/_encode.py +0 -0
  69. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/tracing/_provider.py +0 -0
  70. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/encore/tracing/_span.py +0 -0
  71. {encore_dev-2.0.0b6 → encore_dev-2.0.0b7}/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.0b6
3
+ Version: 2.0.0b7
4
4
  Summary: Encore SDK for Python — infrastructure resources
5
5
  Author: Encore
6
6
  License: MPL-2.0
@@ -153,6 +153,7 @@ async def _lifecycle(http: Any) -> int:
153
153
 
154
154
  async def _shutdown(http: Any, force: asyncio.Event) -> int:
155
155
  from . import shutdown
156
+ from .internal.startup import stopped as subscriptions_stopped
156
157
 
157
158
  # Cooperative: Encore cleans up the resources the app uses once the app
158
159
  # says it is done (its requests answered, its lifespan shut down), or at
@@ -195,9 +196,17 @@ async def _shutdown(http: Any, force: asyncio.Event) -> int:
195
196
  await http.join(1.0)
196
197
 
197
198
  result = await shutdown.completed()
199
+ # Before the loop closes: the subscriptions' consumers ended as the
200
+ # shutdown began.
201
+ await subscriptions_stopped(_CONSUMERS_STOP)
198
202
  return result.code()
199
203
 
200
204
 
205
+ #: How long the subscriptions' consumers may take to end once shutdown has
206
+ #: completed.
207
+ _CONSUMERS_STOP = 1.0
208
+
209
+
201
210
  #: How long after the first signal the same one again is a new one, not the
202
211
  #: first delivered twice.
203
212
  _REPEAT_SECS = 1.0
@@ -734,7 +734,8 @@ class PubSubSubscription:
734
734
  """
735
735
  Consume messages on the calling asyncio loop until subscription shutdown.
736
736
  Handler completion acknowledges a message; exceptions propagate to the core
737
- so the provider can apply its retry policy.
737
+ so the provider can apply its retry policy. Subscribing again moves the
738
+ handler to the calling loop: the subscription has one handler.
738
739
  """
739
740
 
740
741
  @final
@@ -1030,6 +1031,12 @@ class Runtime:
1030
1031
  """
1031
1032
  The version of the Encore runtime in use.
1032
1033
  """
1034
+ def wait_completed(self, /) -> "ShutdownResult":
1035
+ """
1036
+ Wait for cleanup to finish, as `completed` does, blocking this thread
1037
+ rather than awaiting: for the exit, which runs no event loop and
1038
+ leaves the runtime nothing to complete. Call begin_shutdown first.
1039
+ """
1033
1040
 
1034
1041
  @final
1035
1042
  class Secret:
@@ -1108,6 +1115,13 @@ def encode_log(level: int, time_unix_nanos: int, message: str, *, logger: str |N
1108
1115
  runtime.
1109
1116
  """
1110
1117
 
1118
+ def exiting(timeout: float) -> "None":
1119
+ """
1120
+ Says the interpreter is exiting: from now on the runtime's threads don't
1121
+ enter Python. Waits up to `timeout` seconds for those in it to leave.
1122
+ The last thing the SDK does at exit.
1123
+ """
1124
+
1111
1125
  def flush_runtime_log(timeout: float) -> "int":
1112
1126
  """
1113
1127
  Waits, up to `timeout` seconds, for the handler to take the lines queued
@@ -30,16 +30,24 @@ _native.set_runtime_log_handler(_handle_runtime_line)
30
30
  #: How long the runtime's queued lines may take to reach ``logging`` at exit.
31
31
  _RUNTIME_LOG_FLUSH = 1.0
32
32
 
33
+ #: How long the runtime's threads in Python may take to leave it at exit.
34
+ _LEAVE_PYTHON = 1.0
33
35
 
34
- def _flush_runtime_log() -> None:
35
- """Hand the runtime's queued lines to ``logging`` before exit."""
36
+
37
+ def _exit() -> None:
38
+ """Hand the runtime's queued lines to ``logging``, then leave Python be.
39
+
40
+ After this, the runtime's threads don't enter Python: one that did as
41
+ the interpreter finalizes could crash it.
42
+ """
36
43
  lost = _native.flush_runtime_log(_RUNTIME_LOG_FLUSH)
37
44
  if lost:
38
45
  print(f"encore: lost {lost} of the runtime's log lines", file=sys.stderr)
46
+ _native.exiting(_LEAVE_PYTHON)
39
47
 
40
48
 
41
49
  # Registered first, so it runs after the exit handlers registered later.
42
- atexit.register(_flush_runtime_log)
50
+ atexit.register(_exit)
43
51
 
44
52
  # Instrumentations read this at import; Encore records stable semconv (ADR 0004).
45
53
  os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "http")
@@ -62,28 +70,22 @@ def _shutdown() -> bool:
62
70
  global _shutdown_ok
63
71
  if _shutdown_ok is not None:
64
72
  return _shutdown_ok
65
- import asyncio
66
-
67
- from .. import shutdown
68
-
73
+ # Imports nothing, and runs no event loop: it runs at exit, and in the
74
+ # SIGTERM handler, which may interrupt an import.
69
75
  if threading.current_thread() is threading.main_thread():
70
76
  # A repeated SIGTERM must not interrupt the flush.
71
77
  signal.signal(signal.SIGTERM, signal.SIG_IGN)
72
- shutdown.begin()
73
- result: list[bool] = []
74
-
75
- def wait() -> None:
76
- try:
77
- done = asyncio.run(shutdown.completed())
78
- result.append(done is shutdown.ShutdownResult.SUCCESS)
79
- except Exception as exc: # noqa: BLE001
80
- print(f"encore: shutdown failed: {exc}", file=sys.stderr)
81
-
82
- # Off this thread, which may be running an event loop.
83
- waiter = threading.Thread(target=wait, name="encore-shutdown")
84
- waiter.start()
85
- waiter.join()
86
- _shutdown_ok = bool(result and result[0])
78
+ _runtime.begin_shutdown()
79
+ try:
80
+ result = _runtime.wait_completed()
81
+ except EncoreError as exc:
82
+ print(f"encore: shutdown failed: {exc}", file=sys.stderr)
83
+ result = None
84
+ if result == _native.ShutdownResult.Failed:
85
+ print(
86
+ "encore: shutdown failed: Encore resource cleanup failed", file=sys.stderr
87
+ )
88
+ _shutdown_ok = result == _native.ShutdownResult.Success
87
89
  return _shutdown_ok
88
90
 
89
91
 
@@ -1,5 +1,7 @@
1
1
  """Application-loop startup for registered Encore resources."""
2
2
 
3
+ import asyncio
4
+
3
5
  from .. import pubsub
4
6
  from .runtime import runtime
5
7
 
@@ -10,7 +12,9 @@ async def start() -> None:
10
12
  Start all registered subscriptions and return. Import resource declarations
11
13
  before calling this from your async main or framework startup hook.
12
14
  Repeated calls on the same loop reuse consumers; later calls also start newly
13
- imported subscriptions. This does not own the loop or install signal handlers.
15
+ imported subscriptions. Once the loop they were started on has closed, as
16
+ a test client's does after each lifespan, a call on another moves them
17
+ there. This does not own the loop or install signal handlers.
14
18
 
15
19
  Shortly after, the runtime warns of each subscription the service hosts
16
20
  that has no handler, such as one declared in a module never imported.
@@ -28,3 +32,25 @@ async def start() -> None:
28
32
  subscription._start()
29
33
 
30
34
  runtime().subscriptions_started()
35
+
36
+
37
+ async def stopped(timeout: float) -> None:
38
+ """Wait for this loop's subscription consumers to end, up to ``timeout``.
39
+
40
+ They end once Encore's shutdown begins, each resolved from a runtime
41
+ thread. Awaited before the loop closes, so their completions run on it,
42
+ and those still pending at ``timeout`` are cancelled.
43
+ """
44
+ loop = asyncio.get_running_loop()
45
+ consumers = {
46
+ consumer
47
+ for subscription in pubsub._subscriptions
48
+ if (consumer := subscription._consumer) is not None
49
+ and consumer.get_loop() is loop
50
+ and not consumer.done()
51
+ }
52
+ if not consumers:
53
+ return
54
+ _, pending = await asyncio.wait(consumers, timeout=timeout)
55
+ for consumer in pending:
56
+ consumer.cancel()
@@ -366,7 +366,7 @@ def _handler_context(message: _native.PubSubMessage) -> context_api.Context:
366
366
 
367
367
 
368
368
  class SubscriptionLoopError(RuntimeError):
369
- """The subscription belongs to another asyncio loop."""
369
+ """The subscription runs on another asyncio loop, which is still open."""
370
370
 
371
371
 
372
372
  class Subscription(Generic[_T]):
@@ -375,6 +375,10 @@ class Subscription(Generic[_T]):
375
375
  Await encore.start() to begin consumption on the application's asyncio
376
376
  loop. The explicit type argument selects payload decoding; omitting it
377
377
  uses ordinary JSON decoding.
378
+
379
+ It runs on one loop at a time. Once that loop has closed, as a test
380
+ client's closes after its lifespan, encore.start() on another moves it
381
+ there.
378
382
  """
379
383
 
380
384
  def __init__(
@@ -446,14 +450,17 @@ class Subscription(Generic[_T]):
446
450
 
447
451
  def _check_loop(self) -> None:
448
452
  if self._consumer is not None:
449
- if self._consumer.get_loop() is not asyncio.get_running_loop():
453
+ loop = self._consumer.get_loop()
454
+ if loop is not asyncio.get_running_loop() and not loop.is_closed():
450
455
  raise SubscriptionLoopError(
451
456
  "subscription already started on a different asyncio loop"
452
457
  )
453
458
 
454
459
  def _start(self) -> asyncio.Future[None]:
455
460
  self._check_loop()
456
- if self._consumer is None:
461
+ # Started anew where the loop it ran on has closed: the native
462
+ # subscription moves its handler to this one.
463
+ if self._consumer is None or self._consumer.get_loop().is_closed():
457
464
  self._consumer = asyncio.ensure_future(self._handle.get().subscribe())
458
465
  self._consumer.add_done_callback(self._consumer_done)
459
466
  return self._consumer
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "encore-dev"
7
- version = "2.0.0b6"
7
+ version = "2.0.0b7"
8
8
  description = "Encore SDK for Python — infrastructure resources"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "encore-dev"
7
- version = "2.0.0b6"
7
+ version = "2.0.0b7"
8
8
  description = "Encore SDK for Python — infrastructure resources"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes