chalk-remote-call-python 1.10.5__cp315-cp315-win_amd64.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 (45) hide show
  1. chalk_remote_call/__init__.py +4 -0
  2. chalk_remote_call/__main__.py +3 -0
  3. chalk_remote_call/_gen/__init__.py +0 -0
  4. chalk_remote_call/_gen/chalk/__init__.py +0 -0
  5. chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  6. chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  7. chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +115 -0
  8. chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +4 -0
  9. chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  10. chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  11. chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +33 -0
  12. chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +4 -0
  13. chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  14. chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  15. chalk_remote_call/_gen/chalk/runtime/v1/fnq_consumer_pb2.py +53 -0
  16. chalk_remote_call/_gen/chalk/runtime/v1/fnq_consumer_pb2_grpc.py +138 -0
  17. chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +104 -0
  18. chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +363 -0
  19. chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  20. chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  21. chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +32 -0
  22. chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +4 -0
  23. chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +29 -0
  24. chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +4 -0
  25. chalk_remote_call/_gen/chalk/utils/v1/large_pb2.py +26 -0
  26. chalk_remote_call/_gen/chalk/utils/v1/large_pb2_grpc.py +4 -0
  27. chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +26 -0
  28. chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +4 -0
  29. chalk_remote_call/_native.cp315-win_amd64.pyd +0 -0
  30. chalk_remote_call/_native.pyi +13 -0
  31. chalk_remote_call/_version.py +1 -0
  32. chalk_remote_call/arrow_utils.py +21 -0
  33. chalk_remote_call/cli.py +160 -0
  34. chalk_remote_call/errors.py +118 -0
  35. chalk_remote_call/handler_loader.py +57 -0
  36. chalk_remote_call/input_transform.py +50 -0
  37. chalk_remote_call/logging.py +29 -0
  38. chalk_remote_call/server.py +117 -0
  39. chalk_remote_call/servicer.py +400 -0
  40. chalk_remote_call/tracing.py +445 -0
  41. chalk_remote_call_python-1.10.5.dist-info/METADATA +288 -0
  42. chalk_remote_call_python-1.10.5.dist-info/RECORD +45 -0
  43. chalk_remote_call_python-1.10.5.dist-info/WHEEL +5 -0
  44. chalk_remote_call_python-1.10.5.dist-info/entry_points.txt +2 -0
  45. chalk_remote_call_python-1.10.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,445 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import contextlib
5
+ import importlib
6
+ import logging
7
+ import os
8
+ import threading
9
+ from collections.abc import Callable, Iterator, Mapping, Sequence
10
+ from typing import Any, cast
11
+
12
+ _TRACER_NAME = "chalk_remote_call"
13
+ _TRACE_CONTEXT_METADATA_KEYS = ("traceparent", "tracestate", "baggage")
14
+ _REMOTE_FUNCTION_TRACE_POLICY_ENV_VAR = "CHALK_REMOTE_FUNCTION_TRACE_POLICY"
15
+ _REMOTE_FUNCTION_TRACE_SAMPLE_RATE_ENV_VAR = "CHALK_REMOTE_FUNCTION_TRACE_SAMPLE_RATE"
16
+ _REMOTE_FUNCTION_TRACE_OVERRIDE_KEY = "x-chalk-remote-function-tracing"
17
+ _TRACE_OVERRIDE_ON = "on"
18
+ _TRACE_OVERRIDE_OFF = "off"
19
+ _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF = "parentbased_always_off"
20
+ _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO = "parentbased_traceidratio"
21
+ _TRACE_POLICY_ALWAYS_OFF = "always_off"
22
+ _TRACE_POLICIES = frozenset(
23
+ {
24
+ _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF,
25
+ _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO,
26
+ _TRACE_POLICY_ALWAYS_OFF,
27
+ }
28
+ )
29
+ _missing_otel_modules = object()
30
+ _missing_always_off_tracer = object()
31
+ _otel_modules: tuple[Any, Any, Any] | object | None = None
32
+ _always_off_tracer: Any | object | None = None
33
+ _runtime_tracing_configured = False
34
+ _runtime_tracer_provider: Any | None = None
35
+ _runtime_span_flusher: _BackgroundSpanFlusher | None = None
36
+ # Bounds each background flush so a stalled collector cannot wedge the flush thread.
37
+ _SPAN_FLUSH_TIMEOUT_MILLIS = 5000
38
+
39
+ _logger = logging.getLogger(__name__)
40
+
41
+
42
+ class _BackgroundSpanFlusher:
43
+ """Runs a tracer provider's ``force_flush`` on one daemon thread, off the request path.
44
+
45
+ Function pods outlive invocations, so without a flush an invocation's ended spans wait
46
+ for the batch processor's schedule delay before export. Requests coalesce: every request
47
+ made before a flush starts is satisfied by that flush, while a request made after a flush
48
+ starts schedules another, because spans ended after the flush began may have missed it.
49
+ """
50
+
51
+ def __init__(self, flush: Callable[[int], object], timeout_millis: int) -> None:
52
+ super().__init__()
53
+ self._flush = flush
54
+ self._timeout_millis = timeout_millis
55
+ self._cond = threading.Condition()
56
+ # Monotonic counters: requests made, the request count covered by the latest flush to
57
+ # start, and the request count covered by the latest flush to finish.
58
+ self._requested = 0
59
+ self._started = 0
60
+ self._completed = 0
61
+ self._thread: threading.Thread | None = None
62
+
63
+ def request(self) -> None:
64
+ with self._cond:
65
+ self._requested += 1
66
+ # is_alive (rather than None) also restarts the worker in a forked child.
67
+ if self._thread is None or not self._thread.is_alive():
68
+ self._thread = threading.Thread(
69
+ target=self._run,
70
+ name="chalk-remote-call-span-flush",
71
+ daemon=True,
72
+ )
73
+ self._thread.start()
74
+ self._cond.notify_all()
75
+
76
+ def wait_until_idle(self, timeout: float) -> bool:
77
+ """Block until every request made so far is covered by a finished flush."""
78
+ with self._cond:
79
+ return self._cond.wait_for(lambda: self._completed >= self._requested, timeout)
80
+
81
+ def _run(self) -> None:
82
+ while True:
83
+ with self._cond:
84
+ self._cond.wait_for(lambda: self._started < self._requested)
85
+ target = self._requested
86
+ self._started = target
87
+ try:
88
+ if self._flush(self._timeout_millis) is False:
89
+ _logger.debug("Span flush did not finish within %d ms", self._timeout_millis)
90
+ except Exception:
91
+ _logger.warning("Span flush failed", exc_info=True)
92
+ with self._cond:
93
+ self._completed = target
94
+ self._cond.notify_all()
95
+
96
+
97
+ def _request_span_flush() -> None:
98
+ flusher = _runtime_span_flusher
99
+ if flusher is None:
100
+ return
101
+ try:
102
+ flusher.request()
103
+ except Exception:
104
+ # Flushing is best effort; it must never fail the invocation it follows.
105
+ _logger.debug("Could not schedule span flush", exc_info=True)
106
+
107
+
108
+ def _raw_metadata(context_metadata: Any) -> Mapping[str, Any] | None:
109
+ if not isinstance(context_metadata, Mapping):
110
+ return None
111
+
112
+ raw = context_metadata.get("metadata", context_metadata)
113
+ if isinstance(raw, Mapping):
114
+ return raw
115
+ return None
116
+
117
+
118
+ def _collect_trace_metadata(
119
+ context_metadata: Any,
120
+ ) -> tuple[Mapping[str, Any] | None, Mapping[str, Any] | None]:
121
+ if isinstance(context_metadata, Mapping):
122
+ first = _raw_metadata(context_metadata)
123
+ if first is not None and first.get("traceparent"):
124
+ return first, first
125
+ return first, None
126
+
127
+ first: Mapping[str, Any] | None = None
128
+ if isinstance(context_metadata, Sequence) and not isinstance(context_metadata, str | bytes):
129
+ for item in context_metadata:
130
+ metadata = _raw_metadata(item)
131
+ if metadata is None:
132
+ continue
133
+
134
+ if first is None:
135
+ first = metadata
136
+ if metadata.get("traceparent"):
137
+ return first, metadata
138
+
139
+ return first, None
140
+
141
+
142
+ def _trace_policy() -> str | None:
143
+ raw_policy = os.environ.get(_REMOTE_FUNCTION_TRACE_POLICY_ENV_VAR)
144
+ if raw_policy is None:
145
+ return None
146
+
147
+ policy = raw_policy.strip().lower()
148
+ if policy in _TRACE_POLICIES:
149
+ return policy
150
+ return None
151
+
152
+
153
+ def _trace_sample_rate(force_trace: bool) -> float | None:
154
+ if force_trace:
155
+ return 1.0
156
+ raw_rate = os.environ.get(_REMOTE_FUNCTION_TRACE_SAMPLE_RATE_ENV_VAR)
157
+ if raw_rate is None:
158
+ return None
159
+
160
+ try:
161
+ rate = float(raw_rate)
162
+ except ValueError:
163
+ return None
164
+
165
+ if rate < 0.0 or rate > 1.0:
166
+ return None
167
+ return rate
168
+
169
+
170
+ def _otel_exporter_configured() -> bool:
171
+ return bool(os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"))
172
+
173
+
174
+ def _runtime_resource_attributes() -> dict[str, str]:
175
+ attributes = {"service.name": os.environ.get("CHALK_SERVICE") or _TRACER_NAME}
176
+ if environment_id := os.environ.get("CHALK_ENVIRONMENT_ID"):
177
+ attributes["chalk.environment_id"] = environment_id
178
+ return attributes
179
+
180
+
181
+ def _configure_runtime_tracing(sample_rate: float) -> None:
182
+ global _runtime_tracing_configured, _runtime_tracer_provider, _runtime_span_flusher
183
+ if _runtime_tracing_configured:
184
+ return
185
+
186
+ try:
187
+ resources = importlib.import_module("opentelemetry.sdk.resources")
188
+ sdk_trace = importlib.import_module("opentelemetry.sdk.trace")
189
+ sdk_sampling = importlib.import_module("opentelemetry.sdk.trace.sampling")
190
+ trace_api = importlib.import_module("opentelemetry.trace")
191
+ except Exception:
192
+ _runtime_tracing_configured = True
193
+ return
194
+
195
+ try:
196
+ provider = sdk_trace.TracerProvider(
197
+ resource=resources.Resource.create(_runtime_resource_attributes()),
198
+ sampler=sdk_sampling.ParentBased(sdk_sampling.TraceIdRatioBased(sample_rate)),
199
+ )
200
+
201
+ if _otel_exporter_configured():
202
+ try:
203
+ otlp_exporter = importlib.import_module("opentelemetry.exporter.otlp.proto.grpc.trace_exporter")
204
+ sdk_export = importlib.import_module("opentelemetry.sdk.trace.export")
205
+ provider.add_span_processor(sdk_export.BatchSpanProcessor(otlp_exporter.OTLPSpanExporter()))
206
+ _runtime_span_flusher = _BackgroundSpanFlusher(provider.force_flush, _SPAN_FLUSH_TIMEOUT_MILLIS)
207
+ except Exception:
208
+ pass
209
+
210
+ trace_api.set_tracer_provider(provider)
211
+ _runtime_tracer_provider = provider
212
+ atexit.register(provider.shutdown)
213
+ except Exception:
214
+ pass
215
+ _runtime_tracing_configured = True
216
+
217
+
218
+ def _sample_rate_for_inherited_parent(policy: str, force_trace: bool) -> float | None:
219
+ if policy == _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF:
220
+ return 0.0
221
+ if policy == _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO:
222
+ return _trace_sample_rate(force_trace)
223
+ return None
224
+
225
+
226
+ def _span_metadata(
227
+ first_metadata: Mapping[str, Any] | None,
228
+ traceparent_metadata: Mapping[str, Any] | None,
229
+ policy: str,
230
+ force_trace: bool,
231
+ ) -> tuple[Mapping[str, Any] | None, Any | None, Any]:
232
+ if traceparent_metadata is not None:
233
+ carrier = _trace_carrier(traceparent_metadata)
234
+ if not carrier:
235
+ return None, None, None
236
+
237
+ otel_modules = _get_otel_modules()
238
+ if otel_modules is None:
239
+ return None, None, None
240
+ _, propagate, trace = otel_modules
241
+
242
+ parent_context = propagate.extract(carrier)
243
+ parent_span_context = trace.get_current_span(parent_context).get_span_context()
244
+ if not parent_span_context.is_valid:
245
+ return None, None, None
246
+
247
+ sample_rate = _sample_rate_for_inherited_parent(policy, force_trace)
248
+ if sample_rate is None:
249
+ return None, None, None
250
+ _configure_runtime_tracing(sample_rate)
251
+ return traceparent_metadata, parent_context, otel_modules
252
+
253
+ if policy != _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO:
254
+ return None, None, None
255
+
256
+ sample_rate = _trace_sample_rate(force_trace)
257
+ if sample_rate is None:
258
+ return None, None, None
259
+
260
+ _configure_runtime_tracing(sample_rate)
261
+ otel_modules = _get_otel_modules()
262
+ if otel_modules is None:
263
+ return None, None, None
264
+ return first_metadata, None, otel_modules
265
+
266
+
267
+ def _force_sampled_traceparent(metadata: Mapping[str, Any]) -> Mapping[str, Any]:
268
+ parts = str(metadata.get("traceparent") or "").split("-")
269
+ if len(parts) != 4 or len(parts[3]) != 2:
270
+ return metadata
271
+ try:
272
+ parts[3] = f"{int(parts[3], 16) | 1:02x}"
273
+ except ValueError:
274
+ return metadata
275
+ return {**metadata, "traceparent": "-".join(parts)}
276
+
277
+
278
+ def _always_off_span_metadata(
279
+ first_metadata: Mapping[str, Any] | None,
280
+ traceparent_metadata: Mapping[str, Any] | None,
281
+ ) -> tuple[Mapping[str, Any] | None, Any | None, Any | None, Any | None]:
282
+ otel_modules = _get_otel_modules()
283
+ if otel_modules is None:
284
+ return None, None, None, None
285
+ _, propagate, trace = otel_modules
286
+
287
+ parent_context = None
288
+ if traceparent_metadata is not None:
289
+ carrier = _trace_carrier(traceparent_metadata)
290
+ if carrier:
291
+ extracted_context = propagate.extract(carrier)
292
+ parent_span_context = trace.get_current_span(extracted_context).get_span_context()
293
+ if parent_span_context.is_valid:
294
+ parent_context = extracted_context
295
+
296
+ tracer = _get_always_off_tracer()
297
+ if tracer is None:
298
+ return None, None, None, None
299
+ return traceparent_metadata or first_metadata, parent_context, otel_modules, tracer
300
+
301
+
302
+ def _trace_carrier(metadata: Mapping[str, Any]) -> dict[str, str]:
303
+ return {
304
+ str(key).lower(): str(value)
305
+ for key, value in metadata.items()
306
+ if str(key).lower() in _TRACE_CONTEXT_METADATA_KEYS and value
307
+ }
308
+
309
+
310
+ def _function_name(context_metadata: Any) -> str:
311
+ if isinstance(context_metadata, Mapping):
312
+ return str(context_metadata.get("function_name") or "") or "unknown"
313
+
314
+ if isinstance(context_metadata, Sequence) and not isinstance(context_metadata, str | bytes):
315
+ for item in context_metadata:
316
+ if isinstance(item, Mapping):
317
+ function_name = str(item.get("function_name") or "")
318
+ if function_name:
319
+ return function_name
320
+
321
+ return "unknown"
322
+
323
+
324
+ def _get_tracer(trace_api: Any) -> Any:
325
+ return trace_api.get_tracer(_TRACER_NAME)
326
+
327
+
328
+ def _get_otel_modules() -> tuple[Any, Any, Any] | None:
329
+ global _otel_modules
330
+ if _otel_modules is None:
331
+ try:
332
+ _otel_modules = (
333
+ importlib.import_module("opentelemetry.context"),
334
+ importlib.import_module("opentelemetry.propagate"),
335
+ importlib.import_module("opentelemetry.trace"),
336
+ )
337
+ except ImportError:
338
+ _otel_modules = _missing_otel_modules
339
+
340
+ if _otel_modules is _missing_otel_modules:
341
+ return None
342
+ return cast(tuple[Any, Any, Any], _otel_modules)
343
+
344
+
345
+ def _get_always_off_tracer() -> Any | None:
346
+ global _always_off_tracer
347
+ if _always_off_tracer is None:
348
+ try:
349
+ sdk_trace = importlib.import_module("opentelemetry.sdk.trace")
350
+ sdk_sampling = importlib.import_module("opentelemetry.sdk.trace.sampling")
351
+ except ImportError:
352
+ _always_off_tracer = _missing_always_off_tracer
353
+ else:
354
+ # Keep always-off local to this invocation instead of replacing the
355
+ # process-wide tracer provider.
356
+ provider = sdk_trace.TracerProvider(sampler=sdk_sampling.ALWAYS_OFF)
357
+ _always_off_tracer = provider.get_tracer(_TRACER_NAME)
358
+
359
+ if _always_off_tracer is _missing_always_off_tracer:
360
+ return None
361
+ return _always_off_tracer
362
+
363
+
364
+ @contextlib.contextmanager
365
+ def remote_function_invocation_span(
366
+ context_metadata: Any,
367
+ coalesced_count: int | None = None,
368
+ ) -> Iterator[None]:
369
+ first_metadata, traceparent_metadata = _collect_trace_metadata(context_metadata)
370
+ override = str((first_metadata or {}).get(_REMOTE_FUNCTION_TRACE_OVERRIDE_KEY) or "").strip().lower()
371
+ if override == _TRACE_OVERRIDE_ON:
372
+ policy = _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO
373
+ elif override == _TRACE_OVERRIDE_OFF:
374
+ policy = _TRACE_POLICY_ALWAYS_OFF
375
+ else:
376
+ policy = _trace_policy()
377
+ if policy is None:
378
+ yield
379
+ return
380
+
381
+ force_trace = override == _TRACE_OVERRIDE_ON
382
+ if force_trace and traceparent_metadata is not None:
383
+ traceparent_metadata = _force_sampled_traceparent(traceparent_metadata)
384
+ if policy == _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF and traceparent_metadata is None:
385
+ yield
386
+ return
387
+
388
+ if policy == _TRACE_POLICY_ALWAYS_OFF:
389
+ _metadata, parent_context, otel_modules, tracer = _always_off_span_metadata(
390
+ first_metadata,
391
+ traceparent_metadata,
392
+ )
393
+ else:
394
+ _metadata, parent_context, otel_modules = _span_metadata(
395
+ first_metadata,
396
+ traceparent_metadata,
397
+ policy,
398
+ force_trace,
399
+ )
400
+ tracer = _get_tracer(otel_modules[2]) if otel_modules is not None else None
401
+
402
+ if otel_modules is None:
403
+ yield
404
+ return
405
+ if tracer is None:
406
+ yield
407
+ return
408
+ otel_context, _, trace = otel_modules
409
+
410
+ SpanKind = trace.SpanKind
411
+ Status = trace.Status
412
+ StatusCode = trace.StatusCode
413
+ function_name = _function_name(context_metadata)
414
+ span_kwargs: dict[str, Any] = {"kind": SpanKind.SERVER}
415
+ if parent_context is not None:
416
+ span_kwargs["context"] = parent_context
417
+
418
+ # The always-off tracer exports nothing, so only runtime-provider spans need a flush.
419
+ flush_after = policy != _TRACE_POLICY_ALWAYS_OFF
420
+ try:
421
+ with tracer.start_as_current_span("chalkcompute.remote_function.invoke", **span_kwargs) as span:
422
+ span.set_attribute("chalk.remote_function.name", function_name)
423
+ if coalesced_count is not None:
424
+ span.set_attribute(
425
+ "chalk.remote_function.coalesced_count",
426
+ coalesced_count,
427
+ )
428
+ if parent_context is not None and not span.get_span_context().is_valid:
429
+ token = otel_context.attach(parent_context)
430
+ try:
431
+ yield
432
+ finally:
433
+ otel_context.detach(token)
434
+ return
435
+ try:
436
+ yield
437
+ except Exception as exc:
438
+ span.record_exception(exc)
439
+ span.set_status(Status(StatusCode.ERROR, str(exc)))
440
+ raise
441
+ finally:
442
+ # Runs after the invocation span (and any span the handler opened inside it) has
443
+ # ended, so the flush it schedules covers this invocation's spans.
444
+ if flush_after:
445
+ _request_span_flush()