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,390 @@
1
+ """Application metrics, declared as module-level variables.
2
+
3
+ Typical usage example:
4
+
5
+ from typing import TypedDict
6
+
7
+ from encore import metrics
8
+
9
+
10
+ class ResponseLabels(TypedDict):
11
+ method: str
12
+ status: int
13
+
14
+
15
+ requests = metrics.Counter("requests_total")
16
+ queue_depth = metrics.Gauge("queue_depth")
17
+ responses = metrics.CounterGroup[ResponseLabels]("responses_total")
18
+ latency = metrics.Histogram("request_duration", unit="seconds")
19
+
20
+ requests.increment()
21
+ queue_depth.set(3)
22
+ responses.with_labels({"method": "GET", "status": 200}).increment()
23
+ latency.observe(0.025)
24
+
25
+ Declare metrics at module scope and update them with increment(), set(), or
26
+ observe().
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Mapping
32
+ from dataclasses import fields, is_dataclass
33
+ import math
34
+ from threading import Lock
35
+ from typing import Callable, Generic, Union, cast, get_args, overload
36
+
37
+ from typing_extensions import TypeVar
38
+
39
+ from ..internal.lazy import Lazy
40
+ from ..internal.runtime import _native, runtime
41
+
42
+ LabelValue = Union[str, int, float, bool]
43
+
44
+
45
+ def _label_pairs(labels: object) -> tuple[tuple[str, str], ...]:
46
+ if isinstance(labels, Mapping):
47
+ items = labels.items()
48
+ elif is_dataclass(labels) and not isinstance(labels, type):
49
+ items = ((field.name, getattr(labels, field.name)) for field in fields(labels))
50
+ else:
51
+ raise TypeError("labels must be a mapping or a dataclass instance")
52
+ pairs = []
53
+ for key, value in items:
54
+ if not isinstance(key, str):
55
+ raise TypeError("label names must be strings")
56
+ if isinstance(value, bool):
57
+ text = "true" if value else "false"
58
+ elif isinstance(value, float):
59
+ if not math.isfinite(value):
60
+ raise ValueError("numeric labels must be finite")
61
+ text = str(math.floor(value))
62
+ elif isinstance(value, (str, int)):
63
+ text = str(value)
64
+ else:
65
+ raise TypeError("label values must be strings, booleans, or finite numbers")
66
+ pairs.append((key, text))
67
+ return tuple(sorted(pairs))
68
+
69
+
70
+ class _MetricBinding:
71
+ def __init__(
72
+ self, name: str, kind: _native.MetricType | Callable[[], _native.MetricType]
73
+ ) -> None:
74
+ self.name = name
75
+ self._kind = kind
76
+ self.labels: tuple[tuple[str, str], ...] = ()
77
+ self._handle = Lazy(self._resolve)
78
+
79
+ def get(self) -> tuple[_native.MetricsRegistry, int]:
80
+ return self._handle.get()
81
+
82
+ def _resolve(self) -> tuple[_native.MetricsRegistry, int]:
83
+ registry = runtime().get_metrics_registry()
84
+ kind = self._kind() if callable(self._kind) else self._kind
85
+ slot = registry.allocate_slot(self.name, list(self.labels), kind)
86
+ return registry, slot
87
+
88
+
89
+ _N = TypeVar("_N", int, float, default=int)
90
+
91
+
92
+ def _counter_type(instance: object, index: int) -> type[int] | type[float]:
93
+ """Resolve a constructor's numeric type after generic metadata is attached."""
94
+ args = get_args(getattr(instance, "__orig_class__", None))
95
+ numeric = args[index] if len(args) > index else int
96
+ if numeric is not int and numeric is not float:
97
+ raise TypeError("counter numeric type must be int or float")
98
+ return numeric
99
+
100
+
101
+ class Counter(Generic[_N]):
102
+ """A nondecreasing integer or floating-point counter.
103
+
104
+ Counter(name) and Counter[int](name) use unsigned 64-bit integers and wrap
105
+ on overflow. Counter[float](name) uses double precision and rejects updates
106
+ that would overflow. Float totals may lose precision, including for integers
107
+ above 2**53. A variable annotation alone does not select the runtime type.
108
+ """
109
+
110
+ def __init__(self, name: str) -> None:
111
+ """Declare a counter at module scope.
112
+
113
+ Args:
114
+ name: The metric name used when exporting measurements.
115
+ """
116
+ self._numeric = Lazy(lambda: _counter_type(self, 0))
117
+ self._binding = _MetricBinding(
118
+ name,
119
+ lambda: (
120
+ _native.MetricType.FloatCounter
121
+ if self._numeric.get() is float
122
+ else _native.MetricType.Counter
123
+ ),
124
+ )
125
+
126
+ @property
127
+ def name(self) -> str:
128
+ """The declared metric name."""
129
+ return self._binding.name
130
+
131
+ @overload
132
+ def increment(self) -> None: ...
133
+
134
+ @overload
135
+ def increment(self, value: _N) -> None: ...
136
+
137
+ def increment(self, value: int | float = 1) -> None:
138
+ """Add a nonnegative amount to the counter.
139
+
140
+ Args:
141
+ value: Defaults to 1. Integer counters require an integer from 0
142
+ through 2**64 - 1. Float counters accept finite, nonnegative
143
+ integers or floats.
144
+
145
+ Raises:
146
+ TypeError: The value or constructor's numeric type is unsupported.
147
+ OverflowError: An integer increment is outside the unsigned range.
148
+ ValueError: A float increment is negative or nonfinite, its total
149
+ would overflow, or the series already has a different type.
150
+ """
151
+ registry, slot = self._binding.get()
152
+ if self._numeric.get() is float:
153
+ registry.increment_float(slot, value)
154
+ else:
155
+ registry.increment(slot, cast(int, value))
156
+
157
+
158
+ class Gauge:
159
+ """A gauge that can go up and down."""
160
+
161
+ def __init__(self, name: str) -> None:
162
+ """Declare a gauge at module scope.
163
+
164
+ Args:
165
+ name: The metric name used when exporting measurements.
166
+ """
167
+ self._binding = _MetricBinding(name, _native.MetricType.Gauge)
168
+
169
+ @property
170
+ def name(self) -> str:
171
+ """The declared metric name."""
172
+ return self._binding.name
173
+
174
+ def set(self, value: float) -> None:
175
+ """Set the gauge's current measurement.
176
+
177
+ Args:
178
+ value: A finite number.
179
+ """
180
+ registry, slot = self._binding.get()
181
+ registry.set(slot, value)
182
+
183
+
184
+ class Histogram:
185
+ """A distribution of observations recorded in automatic exponential buckets.
186
+
187
+ Use for measurements such as durations or sizes. The runtime tracks count,
188
+ sum, and buckets without retaining individual observations. Buckets grow by
189
+ at most 10% by default and support positive, negative, and zero values.
190
+
191
+ Prometheus remote write exports the bucket distribution. Other exporters
192
+ currently export only the count and sum, which cannot provide percentiles.
193
+ """
194
+
195
+ def __init__(self, name: str, *, unit: str | None = None) -> None:
196
+ """Declare a histogram at module scope.
197
+
198
+ Args:
199
+ name: The metric name as a string literal.
200
+ unit: The measurement unit as a string literal, such as "seconds"
201
+ or "bytes", or None if unspecified. Encore reads this metadata
202
+ from the declaration; it does not convert observations.
203
+ """
204
+ self._binding = _MetricBinding(name, _native.MetricType.Histogram)
205
+ self._unit = unit
206
+
207
+ @property
208
+ def name(self) -> str:
209
+ """The declared metric name."""
210
+ return self._binding.name
211
+
212
+ @property
213
+ def unit(self) -> str | None:
214
+ """The declared measurement unit, or None if unspecified."""
215
+ return self._unit
216
+
217
+ def observe(self, value: float) -> None:
218
+ """Record one measurement in the declared unit.
219
+
220
+ Args:
221
+ value: The measurement to record. Prefer finite values. As in the
222
+ Rust SDK, NaN increments the count and makes the sum NaN but
223
+ enters no bucket; infinities enter buckets of their own.
224
+ """
225
+ registry, slot = self._binding.get()
226
+ registry.observe(slot, value)
227
+
228
+
229
+ _M = TypeVar("_M")
230
+
231
+
232
+ class _LabelCache(Generic[_M]):
233
+ def __init__(self, factory: Callable[[tuple[tuple[str, str], ...]], _M]) -> None:
234
+ self._factory = factory
235
+ self._cache: dict[tuple[tuple[str, str], ...], _M] = {}
236
+ self._lock = Lock()
237
+
238
+ def get(self, labels: object) -> _M:
239
+ pairs = _label_pairs(labels)
240
+ with self._lock:
241
+ metric = self._cache.get(pairs)
242
+ if metric is None:
243
+ metric = self._factory(pairs)
244
+ self._cache[pairs] = metric
245
+ return metric
246
+
247
+
248
+ _L = TypeVar("_L")
249
+
250
+
251
+ class CounterGroup(Generic[_L, _N]):
252
+ """A counter partitioned by labels.
253
+
254
+ Use a dataclass or TypedDict as the label type. CounterGroup[Labels](name)
255
+ uses integer counters; CounterGroup[Labels, float](name) uses float counters.
256
+ Select the numeric type in the constructor, not only a variable annotation.
257
+ Label values must be strings, booleans, or finite numbers.
258
+ """
259
+
260
+ def __init__(self, name: str) -> None:
261
+ """Declare a counter group at module scope.
262
+
263
+ Args:
264
+ name: The metric name used when exporting measurements.
265
+ """
266
+ self._name = name
267
+ self._cache = _LabelCache(self._create_counter)
268
+
269
+ @property
270
+ def name(self) -> str:
271
+ """The declared metric name."""
272
+ return self._name
273
+
274
+ def with_labels(self, labels: _L) -> Counter[_N]:
275
+ """Bind labels and reuse the counter for those values.
276
+
277
+ Numeric labels are rounded down to integers; booleans become "true"
278
+ or "false". Label names are normalized to snake_case. The service
279
+ label is added automatically and cannot be supplied explicitly.
280
+
281
+ Args:
282
+ labels: A dataclass instance or mapping with string, boolean, or
283
+ finite numeric values. Dataclass fields are read without
284
+ recursively converting nested objects.
285
+
286
+ Returns:
287
+ The cached counter for this combination of label values.
288
+ """
289
+ return self._cache.get(labels)
290
+
291
+ def _create_counter(self, pairs: tuple[tuple[str, str], ...]) -> Counter[_N]:
292
+ counter = (
293
+ Counter[float](self.name)
294
+ if _counter_type(self, 1) is float
295
+ else Counter[int](self.name)
296
+ )
297
+ counter._binding.labels = pairs
298
+ return cast("Counter[_N]", counter)
299
+
300
+
301
+ class GaugeGroup(Generic[_L]):
302
+ """A float gauge partitioned by dataclass or mapping labels."""
303
+
304
+ def __init__(self, name: str) -> None:
305
+ """Declare a gauge group at module scope.
306
+
307
+ Args:
308
+ name: The metric name used when exporting measurements.
309
+ """
310
+ self._name = name
311
+ self._cache = _LabelCache(self._create_gauge)
312
+
313
+ @property
314
+ def name(self) -> str:
315
+ """The declared metric name."""
316
+ return self._name
317
+
318
+ def with_labels(self, labels: _L) -> Gauge:
319
+ """Bind labels and reuse the gauge for those values.
320
+
321
+ Numeric labels are rounded down to integers; booleans become "true"
322
+ or "false". Label names are normalized to snake_case. The service
323
+ label is added automatically and cannot be supplied explicitly.
324
+
325
+ Args:
326
+ labels: A dataclass instance or mapping with string, boolean, or
327
+ finite numeric values.
328
+
329
+ Returns:
330
+ The cached gauge for this combination of label values.
331
+ """
332
+ return self._cache.get(labels)
333
+
334
+ def _create_gauge(self, pairs: tuple[tuple[str, str], ...]) -> Gauge:
335
+ gauge = Gauge(self.name)
336
+ gauge._binding.labels = pairs
337
+ return gauge
338
+
339
+
340
+ class HistogramGroup(Generic[_L]):
341
+ """A separate histogram for each combination of label values.
342
+
343
+ Use a dataclass or TypedDict as the label type to check names and values.
344
+ Every series uses the group's unit and the automatic buckets of Histogram.
345
+ Series remain in memory for the process lifetime, so prefer labels with a
346
+ small set of possible values, such as operation names, over request IDs.
347
+ """
348
+
349
+ def __init__(self, name: str, *, unit: str | None = None) -> None:
350
+ """Declare a histogram group at module scope.
351
+
352
+ Args:
353
+ name: The metric name as a string literal.
354
+ unit: The measurement unit as a string literal, such as "seconds"
355
+ or "bytes", or None if unspecified. Values are not converted.
356
+ """
357
+ self._name = name
358
+ self._unit = unit
359
+ self._cache = _LabelCache(self._create_histogram)
360
+
361
+ @property
362
+ def name(self) -> str:
363
+ """The declared metric name."""
364
+ return self._name
365
+
366
+ @property
367
+ def unit(self) -> str | None:
368
+ """The declared measurement unit, or None if unspecified."""
369
+ return self._unit
370
+
371
+ def with_labels(self, labels: _L) -> Histogram:
372
+ """Bind labels and reuse the histogram for those values.
373
+
374
+ Numeric labels are rounded down to integers; booleans become "true"
375
+ or "false". Label names are normalized to snake_case. The service
376
+ label is added automatically and cannot be supplied explicitly.
377
+
378
+ Args:
379
+ labels: A dataclass instance or mapping with string, boolean, or
380
+ finite numeric values.
381
+
382
+ Returns:
383
+ The cached histogram for this combination of label values.
384
+ """
385
+ return self._cache.get(labels)
386
+
387
+ def _create_histogram(self, pairs: tuple[tuple[str, str], ...]) -> Histogram:
388
+ histogram = Histogram(self.name, unit=self.unit)
389
+ histogram._binding.labels = pairs
390
+ return histogram