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.
- encore/__init__.py +43 -0
- encore/cron/__init__.py +50 -0
- encore/internal/__init__.py +1 -0
- encore/internal/_loader.py +79 -0
- encore/internal/_native.pyi +734 -0
- encore/internal/lazy.py +35 -0
- encore/internal/runtime.py +133 -0
- encore/internal/startup.py +24 -0
- encore/logs/__init__.py +19 -0
- encore/logs/_console.py +155 -0
- encore/logs/_runtime.py +56 -0
- encore/meta/__init__.py +228 -0
- encore/metrics/__init__.py +390 -0
- encore/pubsub/__init__.py +425 -0
- encore/py.typed +0 -0
- encore/secrets/__init__.py +139 -0
- encore/shutdown/__init__.py +164 -0
- encore/storage/__init__.py +5 -0
- encore/storage/objects/__init__.py +396 -0
- encore/storage/postgres/__init__.py +737 -0
- encore/storage/redis/__init__.py +200 -0
- encore/storage/redis/_base.py +42 -0
- encore/storage/redis/_responses.py +150 -0
- encore/storage/redis/_values.py +189 -0
- encore/storage/redis/bitmap.py +142 -0
- encore/storage/redis/generic.py +367 -0
- encore/storage/redis/geo.py +508 -0
- encore/storage/redis/hash.py +504 -0
- encore/storage/redis/json.py +614 -0
- encore/storage/redis/list.py +511 -0
- encore/storage/redis/pubsub.py +276 -0
- encore/storage/redis/set.py +570 -0
- encore/storage/redis/sorted_set.py +753 -0
- encore/storage/redis/stream.py +773 -0
- encore/storage/redis/string.py +654 -0
- encore/tracing/__init__.py +19 -0
- encore/tracing/_context.py +27 -0
- encore/tracing/_encode.py +351 -0
- encore/tracing/_provider.py +62 -0
- encore/tracing/_span.py +226 -0
- encore/tracing/_tracer.py +93 -0
- encore_dev-2.0.0a7.dist-info/METADATA +791 -0
- encore_dev-2.0.0a7.dist-info/RECORD +44 -0
- encore_dev-2.0.0a7.dist-info/WHEEL +4 -0
encore/internal/lazy.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Thread-safe initialization for runtime resource handles."""
|
|
2
|
+
|
|
3
|
+
from threading import Lock
|
|
4
|
+
from typing import Callable, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Lazy(Generic[T]):
|
|
10
|
+
"""A thread-safe cache of the first successful factory result.
|
|
11
|
+
|
|
12
|
+
None is cached like any other result. Failed initialization can be retried.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, factory: Callable[[], T]) -> None:
|
|
16
|
+
"""Initialize an unresolved value.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
factory: A callable invoked on first access and retried if it raises.
|
|
20
|
+
"""
|
|
21
|
+
self._factory = factory
|
|
22
|
+
self._value: tuple[T] | None = None
|
|
23
|
+
self._lock = Lock()
|
|
24
|
+
|
|
25
|
+
def get(self) -> T:
|
|
26
|
+
# The cached tuple is published only after initialization and never replaced.
|
|
27
|
+
"""Return the cached value, initializing it once under a lock if needed."""
|
|
28
|
+
if (value := self._value) is not None:
|
|
29
|
+
return value[0]
|
|
30
|
+
|
|
31
|
+
with self._lock:
|
|
32
|
+
# Another caller may have initialized the value while we waited.
|
|
33
|
+
if self._value is None:
|
|
34
|
+
self._value = (self._factory(),)
|
|
35
|
+
return self._value[0]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""The process-wide runtime singleton over the native extension.
|
|
2
|
+
|
|
3
|
+
This is the Python analogue of ``sdk/ts/infra/internal/runtime/mod.ts``: the
|
|
4
|
+
native module wraps ``encore_sdk_core`` over PyO3, and the rest of the ``encore``
|
|
5
|
+
package builds an ergonomic API on top of the runtime handle.
|
|
6
|
+
|
|
7
|
+
The runtime is initialized synchronously when this module is imported, matching
|
|
8
|
+
the TypeScript SDK. Encore must supply runtime configuration before application
|
|
9
|
+
imports; initialization errors fail startup before requests are served.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import atexit
|
|
15
|
+
import json
|
|
16
|
+
import signal
|
|
17
|
+
import sys
|
|
18
|
+
import threading
|
|
19
|
+
|
|
20
|
+
from ._loader import _native, _runtime_relative_path # noqa: F401
|
|
21
|
+
|
|
22
|
+
# The native error type, re-exported for callers that want to catch it.
|
|
23
|
+
EncoreError = _native.EncoreError
|
|
24
|
+
|
|
25
|
+
from ..logs._runtime import handle_line as _handle_runtime_line # noqa: E402
|
|
26
|
+
|
|
27
|
+
_native.set_runtime_log_handler(_handle_runtime_line)
|
|
28
|
+
|
|
29
|
+
#: How long the runtime's queued lines may take to reach ``logging`` at exit.
|
|
30
|
+
_RUNTIME_LOG_FLUSH = 1.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _flush_runtime_log() -> None:
|
|
34
|
+
"""Hand the runtime's queued lines to ``logging`` before exit."""
|
|
35
|
+
lost = _native.flush_runtime_log(_RUNTIME_LOG_FLUSH)
|
|
36
|
+
if lost:
|
|
37
|
+
print(f"encore: lost {lost} of the runtime's log lines", file=sys.stderr)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Registered first, so it runs after the exit handlers registered later.
|
|
41
|
+
atexit.register(_flush_runtime_log)
|
|
42
|
+
|
|
43
|
+
_runtime = _native.Runtime()
|
|
44
|
+
|
|
45
|
+
from ..tracing._provider import install as _install_tracing # noqa: E402
|
|
46
|
+
|
|
47
|
+
_telemetry = _runtime.telemetry()
|
|
48
|
+
_install_tracing(_telemetry)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
#: The outcome of the shutdown at exit, once run: whether it completed in time.
|
|
52
|
+
_shutdown_ok: bool | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _shutdown() -> bool:
|
|
56
|
+
"""Drain infrastructure and flush telemetry once.
|
|
57
|
+
|
|
58
|
+
Wait for an existing application-initiated shutdown if one has begun.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Whether shutdown completed within its deadline.
|
|
62
|
+
"""
|
|
63
|
+
global _shutdown_ok
|
|
64
|
+
if _shutdown_ok is not None:
|
|
65
|
+
return _shutdown_ok
|
|
66
|
+
import asyncio
|
|
67
|
+
|
|
68
|
+
from .. import shutdown
|
|
69
|
+
|
|
70
|
+
if threading.current_thread() is threading.main_thread():
|
|
71
|
+
# A repeated SIGTERM must not interrupt the flush.
|
|
72
|
+
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
|
73
|
+
shutdown.begin()
|
|
74
|
+
result: list[bool] = []
|
|
75
|
+
|
|
76
|
+
def wait() -> None:
|
|
77
|
+
try:
|
|
78
|
+
done = asyncio.run(shutdown.completed())
|
|
79
|
+
result.append(done is shutdown.ShutdownResult.SUCCESS)
|
|
80
|
+
except Exception as exc: # noqa: BLE001
|
|
81
|
+
print(f"encore: shutdown failed: {exc}", file=sys.stderr)
|
|
82
|
+
|
|
83
|
+
# Off this thread, which may be running an event loop.
|
|
84
|
+
waiter = threading.Thread(target=wait, name="encore-shutdown")
|
|
85
|
+
waiter.start()
|
|
86
|
+
waiter.join()
|
|
87
|
+
_shutdown_ok = bool(result and result[0])
|
|
88
|
+
return _shutdown_ok
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _shutdown_at_exit() -> None:
|
|
92
|
+
_shutdown()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _exit_on_sigterm(signum: int, frame: object) -> None:
|
|
96
|
+
"""Exit with 0 if shutdown completes in time, or 1 otherwise."""
|
|
97
|
+
raise SystemExit(0 if _shutdown() else 1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _testing() -> bool:
|
|
101
|
+
"""Whether the process runs tests, where the test runner owns SIGTERM."""
|
|
102
|
+
meta = json.loads(_runtime.app_meta_json())
|
|
103
|
+
return (meta.get("environment") or {}).get("type") == "test"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
atexit.register(_shutdown_at_exit)
|
|
107
|
+
# SIGTERM's default action skips atexit. Servers that handle it themselves
|
|
108
|
+
# replace this handler, and re-raise the signal after restoring it.
|
|
109
|
+
if (
|
|
110
|
+
threading.current_thread() is threading.main_thread()
|
|
111
|
+
and signal.getsignal(signal.SIGTERM) is signal.SIG_DFL
|
|
112
|
+
and not _testing()
|
|
113
|
+
):
|
|
114
|
+
signal.signal(signal.SIGTERM, _exit_on_sigterm)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def runtime() -> _native.Runtime:
|
|
118
|
+
"""Return the process-wide core runtime.
|
|
119
|
+
|
|
120
|
+
The runtime is initialized from the ambient ENCORE_* environment when
|
|
121
|
+
this module is imported. Resources are resolved by name using its config.
|
|
122
|
+
"""
|
|
123
|
+
return _runtime
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_forever() -> None:
|
|
127
|
+
"""Start draining infrastructure (subscriptions, metrics) and keep serving."""
|
|
128
|
+
runtime().run_forever()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def version() -> str:
|
|
132
|
+
"""The version of the Encore runtime in use."""
|
|
133
|
+
return _native.Runtime.version()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Application-loop startup for registered Encore resources."""
|
|
2
|
+
|
|
3
|
+
from .. import pubsub
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def start() -> None:
|
|
7
|
+
"""The single Encore startup entrypoint for an application's asyncio loop.
|
|
8
|
+
|
|
9
|
+
Start all registered subscriptions and return. Import resource declarations
|
|
10
|
+
before calling this from your async main or framework startup hook.
|
|
11
|
+
Repeated calls on the same loop reuse consumers; later calls also start newly
|
|
12
|
+
imported subscriptions. This does not own the loop or install signal handlers.
|
|
13
|
+
"""
|
|
14
|
+
subscriptions = list(pubsub._subscriptions)
|
|
15
|
+
|
|
16
|
+
# First loop, verify that all subscriptions are available
|
|
17
|
+
for subscription in subscriptions:
|
|
18
|
+
subscription._check_loop()
|
|
19
|
+
subscription._handle.get()
|
|
20
|
+
|
|
21
|
+
# Start all subscriptions
|
|
22
|
+
# This way no partial startup is possible
|
|
23
|
+
for subscription in subscriptions:
|
|
24
|
+
subscription._start()
|
encore/logs/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Log records in the Encore log format.
|
|
2
|
+
|
|
3
|
+
:class:`ConsoleHandler` is a ``logging`` handler writing each record as one
|
|
4
|
+
line in the Encore log format to stderr, which Encore's log collector reads;
|
|
5
|
+
put it on the root logger::
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from encore.logs import ConsoleHandler
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(level=logging.INFO, handlers=[ConsoleHandler()])
|
|
12
|
+
|
|
13
|
+
A record handled while a span is current carries its trace and span ids.
|
|
14
|
+
The runtime's own lines log under the ``encore.runtime`` logger.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ._console import ConsoleHandler
|
|
18
|
+
|
|
19
|
+
__all__ = ["ConsoleHandler"]
|
encore/logs/_console.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""The ``logging`` handler printing the Encore log format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import math
|
|
8
|
+
import traceback
|
|
9
|
+
from typing import Any, Optional, TextIO
|
|
10
|
+
|
|
11
|
+
from opentelemetry import trace
|
|
12
|
+
|
|
13
|
+
from ..internal._loader import _native
|
|
14
|
+
|
|
15
|
+
#: The record attribute holding the ``SpanContext`` a record belongs to, over
|
|
16
|
+
#: the current span's.
|
|
17
|
+
SPAN_CONTEXT_ATTR = "_encore_span_context"
|
|
18
|
+
|
|
19
|
+
#: The record attribute holding the runtime module a line of the runtime's
|
|
20
|
+
#: own comes from.
|
|
21
|
+
RUNTIME_TARGET_ATTR = "_encore_runtime_target"
|
|
22
|
+
|
|
23
|
+
#: Attributes every ``LogRecord`` has; the rest come from ``extra``.
|
|
24
|
+
_STANDARD = frozenset(
|
|
25
|
+
logging.LogRecord("", 0, "", 0, "", (), None).__dict__.keys()
|
|
26
|
+
| {"message", "asctime", "taskName", SPAN_CONTEXT_ATTR, RUNTIME_TARGET_ATTR}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConsoleHandler(logging.StreamHandler):
|
|
31
|
+
"""A logging handler that writes one Encore-formatted line per record.
|
|
32
|
+
|
|
33
|
+
Records are written to stream, or sys.stderr by default. Each line carries
|
|
34
|
+
the time, level,
|
|
35
|
+
message, logger name, the caller as ``code.*``, ``exc_info`` as
|
|
36
|
+
``exception.*``, the ``extra`` attributes, and the ids of the span current
|
|
37
|
+
when the record is handled, with its sampled flag. The SDK core encodes
|
|
38
|
+
it; it needs the native library, not the runtime. Formatters are not
|
|
39
|
+
used.
|
|
40
|
+
|
|
41
|
+
An attribute named like a reserved key (``message``, ``trace_id``, …)
|
|
42
|
+
with a value valid for it is that field, in place of the record's; one
|
|
43
|
+
with an invalid value, or named like a key of the record's caller or
|
|
44
|
+
exception, is written as ``raw.<key>``.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self, stream: Optional[TextIO] = None, level: int | str = logging.NOTSET
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize an Encore console log handler.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
stream: The text stream to write to, or None to use sys.stderr.
|
|
54
|
+
level: The minimum logging level as a number or logging level name.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(stream)
|
|
57
|
+
self.setLevel(level)
|
|
58
|
+
|
|
59
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
60
|
+
"""Return the record as one Encore log line without a trailing newline.
|
|
61
|
+
|
|
62
|
+
Custom logging formatters are ignored.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
record: The log record to encode.
|
|
66
|
+
"""
|
|
67
|
+
return encode(record)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def encode(record: logging.LogRecord) -> str:
|
|
71
|
+
"""Return a record as one Encore log line without a trailing newline.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
record: The record to encode. An explicit span context takes precedence
|
|
75
|
+
over the current OpenTelemetry span.
|
|
76
|
+
"""
|
|
77
|
+
sc = getattr(record, SPAN_CONTEXT_ATTR, None)
|
|
78
|
+
if sc is None:
|
|
79
|
+
sc = trace.get_current_span().get_span_context()
|
|
80
|
+
span = None
|
|
81
|
+
if sc.is_valid:
|
|
82
|
+
span = (
|
|
83
|
+
sc.trace_id.to_bytes(16, "big")
|
|
84
|
+
+ sc.span_id.to_bytes(8, "big")
|
|
85
|
+
+ bytes((sc.trace_flags,))
|
|
86
|
+
)
|
|
87
|
+
exception_type = exception_message = exception_stacktrace = None
|
|
88
|
+
exc = record.exc_info
|
|
89
|
+
if exc and exc[0] is not None:
|
|
90
|
+
exception_type = _type_name(exc[0])
|
|
91
|
+
exception_message = str(exc[1])
|
|
92
|
+
exception_stacktrace = "".join(traceback.format_exception(*exc)).rstrip("\n")
|
|
93
|
+
return _native.encode_log(
|
|
94
|
+
_severity(record.levelno),
|
|
95
|
+
round(record.created * 1_000_000) * 1000,
|
|
96
|
+
record.getMessage(),
|
|
97
|
+
logger=record.name or None,
|
|
98
|
+
span_ctx=span,
|
|
99
|
+
exception_type=exception_type,
|
|
100
|
+
exception_message=exception_message,
|
|
101
|
+
exception_stacktrace=exception_stacktrace,
|
|
102
|
+
code_function=record.funcName or None,
|
|
103
|
+
code_file=record.pathname or None,
|
|
104
|
+
code_line=record.lineno or 0,
|
|
105
|
+
runtime_target=getattr(record, RUNTIME_TARGET_ATTR, None),
|
|
106
|
+
attrs_json=json.dumps(
|
|
107
|
+
_attributes(record), separators=(",", ":"), allow_nan=False
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _severity(levelno: int) -> int:
|
|
113
|
+
"""The OpenTelemetry severity number of the range ``levelno`` is in."""
|
|
114
|
+
if levelno < logging.DEBUG:
|
|
115
|
+
return 1
|
|
116
|
+
if levelno < logging.INFO:
|
|
117
|
+
return 5
|
|
118
|
+
if levelno < logging.WARNING:
|
|
119
|
+
return 9
|
|
120
|
+
if levelno < logging.ERROR:
|
|
121
|
+
return 13
|
|
122
|
+
if levelno < logging.CRITICAL:
|
|
123
|
+
return 17
|
|
124
|
+
return 21
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _attributes(record: logging.LogRecord) -> dict[str, Any]:
|
|
128
|
+
"""The ``extra`` attributes."""
|
|
129
|
+
return {
|
|
130
|
+
key: _json(value)
|
|
131
|
+
for key, value in record.__dict__.items()
|
|
132
|
+
if key not in _STANDARD
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _type_name(t: type) -> str:
|
|
137
|
+
if t.__module__ == "builtins":
|
|
138
|
+
return t.__qualname__
|
|
139
|
+
return f"{t.__module__}.{t.__qualname__}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _json(v: object) -> Any:
|
|
143
|
+
"""Return JSON-compatible values, converting unsupported values to strings.
|
|
144
|
+
|
|
145
|
+
Non-finite floats are also converted to strings.
|
|
146
|
+
"""
|
|
147
|
+
if v is None or isinstance(v, (str, bool, int)):
|
|
148
|
+
return v
|
|
149
|
+
if isinstance(v, float):
|
|
150
|
+
return v if math.isfinite(v) else str(v)
|
|
151
|
+
if isinstance(v, (list, tuple)):
|
|
152
|
+
return [_json(x) for x in v]
|
|
153
|
+
if isinstance(v, dict):
|
|
154
|
+
return {str(k): _json(x) for k, x in v.items()}
|
|
155
|
+
return str(v)
|
encore/logs/_runtime.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The runtime's own log lines as ``logging`` records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from opentelemetry.trace import SpanContext, TraceFlags
|
|
10
|
+
|
|
11
|
+
from ..internal._loader import _native
|
|
12
|
+
from ._console import RUNTIME_TARGET_ATTR, SPAN_CONTEXT_ATTR
|
|
13
|
+
|
|
14
|
+
#: The runtime's lines log under ``encore.runtime``, their target an
|
|
15
|
+
#: attribute.
|
|
16
|
+
LOGGER: str = _native.RUNTIME_LOGGER
|
|
17
|
+
|
|
18
|
+
#: The runtime's TRACE; ``logging`` has none.
|
|
19
|
+
TRACE = 5
|
|
20
|
+
|
|
21
|
+
# The runtime's levels: 1 ERROR to 5 TRACE.
|
|
22
|
+
_LEVELS = (logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG, TRACE)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def handle_line(
|
|
26
|
+
level: int,
|
|
27
|
+
target: str,
|
|
28
|
+
message: str,
|
|
29
|
+
fields_json: str,
|
|
30
|
+
span_ctx: Optional[bytes],
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Log one runtime line from the native log-handler callback.
|
|
33
|
+
|
|
34
|
+
Records use the encore.runtime logger. The target is stored in the
|
|
35
|
+
RUNTIME_TARGET_ATTR record attribute and fields become record attributes.
|
|
36
|
+
Fields colliding with LogRecord attributes use a raw.<name> key. The
|
|
37
|
+
25-byte span_ctx supplies the record's span.
|
|
38
|
+
"""
|
|
39
|
+
levelno = _LEVELS[min(max(level, 1), 5) - 1]
|
|
40
|
+
logger = logging.getLogger(LOGGER)
|
|
41
|
+
if not logger.isEnabledFor(levelno):
|
|
42
|
+
return
|
|
43
|
+
record = logger.makeRecord(LOGGER, levelno, "", 0, message, (), None)
|
|
44
|
+
for key, value in json.loads(fields_json).items():
|
|
45
|
+
while key in record.__dict__ or key in ("message", "asctime"):
|
|
46
|
+
key = "raw." + key
|
|
47
|
+
record.__dict__[key] = value
|
|
48
|
+
record.__dict__[RUNTIME_TARGET_ATTR] = target
|
|
49
|
+
if span_ctx:
|
|
50
|
+
record.__dict__[SPAN_CONTEXT_ATTR] = SpanContext(
|
|
51
|
+
int.from_bytes(span_ctx[:16], "big"),
|
|
52
|
+
int.from_bytes(span_ctx[16:24], "big"),
|
|
53
|
+
is_remote=False,
|
|
54
|
+
trace_flags=TraceFlags(span_ctx[24]),
|
|
55
|
+
)
|
|
56
|
+
logger.handle(record)
|
encore/meta/__init__.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Metadata for the running application and its API gateways.
|
|
2
|
+
|
|
3
|
+
Typical usage example:
|
|
4
|
+
|
|
5
|
+
from encore import meta
|
|
6
|
+
|
|
7
|
+
md = meta.app_meta()
|
|
8
|
+
if md.environment.type == "production":
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
# An app can declare several gateways, so look one up by name.
|
|
12
|
+
gw = md.gateways.get("api")
|
|
13
|
+
if gw is not None:
|
|
14
|
+
print("api is at", gw.base_url)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
from ..internal.runtime import runtime
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"app_meta",
|
|
29
|
+
"AppMeta",
|
|
30
|
+
"GatewayMeta",
|
|
31
|
+
"EnvironmentMeta",
|
|
32
|
+
"BuildMeta",
|
|
33
|
+
"DeployMeta",
|
|
34
|
+
"HostedService",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class GatewayMeta:
|
|
40
|
+
"""An API gateway serving the application.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
name: The name of the gateway, as declared by the application.
|
|
44
|
+
base_url: The base URL which can be used to call the API of this running
|
|
45
|
+
application through this gateway.
|
|
46
|
+
|
|
47
|
+
For local development it is ``http://localhost:<port>``, typically
|
|
48
|
+
``http://localhost:4000``.
|
|
49
|
+
|
|
50
|
+
If a custom domain is used for this environment it is returned here,
|
|
51
|
+
but note that changes only take effect at the time of deployment
|
|
52
|
+
while custom domains can be updated at any time.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
|
|
57
|
+
base_url: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class EnvironmentMeta:
|
|
62
|
+
"""The environment the application is running in.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
name: The name of the environment. For local development it is
|
|
66
|
+
``"local"``.
|
|
67
|
+
type: The type of environment: ``"production"``, ``"development"``,
|
|
68
|
+
``"ephemeral"`` or ``"test"``. For local development it is
|
|
69
|
+
``"development"``.
|
|
70
|
+
cloud: The cloud the environment runs on: ``"aws"``, ``"gcp"``,
|
|
71
|
+
``"azure"``, ``"encore"`` or ``"local"``. For local development it
|
|
72
|
+
is ``"local"``.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
|
|
77
|
+
type: str
|
|
78
|
+
|
|
79
|
+
cloud: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class BuildMeta:
|
|
84
|
+
"""The build that formed the running application.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
revision: The VCS revision the deployed application was built from.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
revision: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class HostedService:
|
|
95
|
+
"""A service hosted by this deployment.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
name: The name of the service.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
name: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class DeployMeta:
|
|
106
|
+
"""This deployment of the application.
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
id: The unique id of the deployment. Generated by the Encore Platform.
|
|
110
|
+
time: The time the Encore Platform deployed this build to the
|
|
111
|
+
environment.
|
|
112
|
+
hosted_services: The services hosted by this deployment.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
id: str
|
|
116
|
+
|
|
117
|
+
time: datetime
|
|
118
|
+
|
|
119
|
+
hosted_services: List[HostedService] = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class AppMeta:
|
|
124
|
+
"""Metadata about the running Encore application.
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
app_id: The Encore application ID. Empty if the application is not
|
|
128
|
+
linked to the Encore platform; run ``encore app link`` to link it.
|
|
129
|
+
app_slug: The application slug — the human-readable identifier shown in
|
|
130
|
+
the Encore Cloud dashboard and used by the CLI. Empty if the
|
|
131
|
+
application is not linked to the Encore platform.
|
|
132
|
+
|
|
133
|
+
Distinct from :attr:`app_id`: the id is opaque and stable, the slug
|
|
134
|
+
is readable and may be renamed. Locally the two are the same.
|
|
135
|
+
gateways: The API gateways that serve this application, keyed by gateway
|
|
136
|
+
name.
|
|
137
|
+
|
|
138
|
+
An application can declare several gateways, so there is no single
|
|
139
|
+
base URL for the API — look up the gateway you mean by name. A
|
|
140
|
+
process that serves no gateway (a worker) reports an empty mapping.
|
|
141
|
+
environment: The environment the app is running in.
|
|
142
|
+
build: The build the running binary came from.
|
|
143
|
+
deploy: This deployment of the binary.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
app_id: str
|
|
147
|
+
|
|
148
|
+
app_slug: str
|
|
149
|
+
|
|
150
|
+
gateways: Dict[str, GatewayMeta]
|
|
151
|
+
|
|
152
|
+
environment: EnvironmentMeta
|
|
153
|
+
|
|
154
|
+
build: BuildMeta
|
|
155
|
+
|
|
156
|
+
deploy: DeployMeta
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_cached: Optional[AppMeta] = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def app_meta() -> AppMeta:
|
|
163
|
+
"""Return metadata about the running Encore application.
|
|
164
|
+
|
|
165
|
+
The metadata is computed on first use and the same object is returned by every
|
|
166
|
+
subsequent call.
|
|
167
|
+
"""
|
|
168
|
+
global _cached
|
|
169
|
+
if _cached is None:
|
|
170
|
+
_cached = _decode(json.loads(runtime().app_meta_json()))
|
|
171
|
+
return _cached
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _decode(raw: dict) -> AppMeta:
|
|
175
|
+
env = raw.get("environment") or {}
|
|
176
|
+
build = raw.get("build") or {}
|
|
177
|
+
deploy = raw.get("deploy") or {}
|
|
178
|
+
|
|
179
|
+
return AppMeta(
|
|
180
|
+
app_id=raw.get("app_id", ""),
|
|
181
|
+
app_slug=raw.get("app_slug", ""),
|
|
182
|
+
gateways={
|
|
183
|
+
name: GatewayMeta(
|
|
184
|
+
name=gw.get("name", name), base_url=gw.get("base_url", "")
|
|
185
|
+
)
|
|
186
|
+
for name, gw in (raw.get("gateways") or {}).items()
|
|
187
|
+
},
|
|
188
|
+
environment=EnvironmentMeta(
|
|
189
|
+
name=env.get("name", ""),
|
|
190
|
+
type=env.get("type", "development"),
|
|
191
|
+
cloud=env.get("cloud", "local"),
|
|
192
|
+
),
|
|
193
|
+
build=BuildMeta(revision=build.get("revision", "")),
|
|
194
|
+
deploy=DeployMeta(
|
|
195
|
+
id=deploy.get("id", ""),
|
|
196
|
+
time=_parse_time(deploy.get("deploy_time")),
|
|
197
|
+
hosted_services=[
|
|
198
|
+
HostedService(name=svc.get("name", ""))
|
|
199
|
+
for svc in (deploy.get("hosted_services") or [])
|
|
200
|
+
],
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Matches a fractional-seconds group of any precision, so it can be trimmed to
|
|
206
|
+
# the six digits datetime supports.
|
|
207
|
+
_FRACTION = re.compile(r"\.(\d+)")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_time(value: Optional[str]) -> datetime:
|
|
211
|
+
"""Parse the core's RFC 3339 timestamp.
|
|
212
|
+
|
|
213
|
+
``datetime.fromisoformat`` is only lenient enough on Python 3.11+, and the
|
|
214
|
+
core emits nanosecond precision and a ``Z`` suffix — both of which older
|
|
215
|
+
versions reject. Normalize to what every supported version accepts.
|
|
216
|
+
"""
|
|
217
|
+
if not value:
|
|
218
|
+
return datetime.fromtimestamp(0, tz=timezone.utc)
|
|
219
|
+
|
|
220
|
+
normalized = value
|
|
221
|
+
if normalized.endswith(("Z", "z")):
|
|
222
|
+
normalized = normalized[:-1] + "+00:00"
|
|
223
|
+
normalized = _FRACTION.sub(lambda m: "." + m.group(1)[:6].ljust(6, "0"), normalized)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
return datetime.fromisoformat(normalized)
|
|
227
|
+
except ValueError:
|
|
228
|
+
return datetime.fromtimestamp(0, tz=timezone.utc)
|