log-foundry 0.10.2.dev127__tar.gz → 0.10.2.dev129__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 (59) hide show
  1. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/PKG-INFO +10 -6
  2. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/README.md +9 -5
  3. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/__init__.py +2 -2
  5. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/config.py +8 -2
  6. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/decorator.py +76 -4
  7. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/ids.py +3 -1
  8. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/kinesis.py +8 -3
  9. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/LICENSE +0 -0
  10. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/_diag.py +0 -0
  11. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/_fork.py +0 -0
  12. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/_lifecycle.py +0 -0
  13. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/api.py +0 -0
  14. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/console.py +0 -0
  15. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/context.py +0 -0
  16. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/model.py +0 -0
  17. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/py.typed +0 -0
  18. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/results.py +0 -0
  19. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sanitize.py +0 -0
  20. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/__init__.py +0 -0
  21. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/_batch.py +0 -0
  22. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/_chunk.py +0 -0
  23. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/_retry.py +0 -0
  24. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/_socket.py +0 -0
  25. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/_time.py +0 -0
  26. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/base.py +0 -0
  27. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/callback.py +0 -0
  28. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/clickhouse.py +0 -0
  29. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/datadog.py +0 -0
  30. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/elasticsearch.py +0 -0
  31. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/eventhubs.py +0 -0
  32. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/file.py +0 -0
  33. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/filtering.py +0 -0
  34. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/firehose.py +0 -0
  35. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/honeycomb.py +0 -0
  36. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/http.py +0 -0
  37. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/kafka.py +0 -0
  38. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/logging_sink.py +0 -0
  39. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/logstash.py +0 -0
  40. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/loki.py +0 -0
  41. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/memory.py +0 -0
  42. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/mongodb.py +0 -0
  43. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/multi.py +0 -0
  44. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/nats.py +0 -0
  45. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/newrelic.py +0 -0
  46. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/null.py +0 -0
  47. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/postgres.py +0 -0
  48. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/pubsub.py +0 -0
  49. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/rabbitmq.py +0 -0
  50. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/redis.py +0 -0
  51. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/sentry.py +0 -0
  52. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/sns.py +0 -0
  53. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/splunk.py +0 -0
  54. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/sqlite.py +0 -0
  55. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/sqs.py +0 -0
  56. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/stdout.py +0 -0
  57. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/syslog.py +0 -0
  58. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/sinks/transform.py +0 -0
  59. {log_foundry-0.10.2.dev127 → log_foundry-0.10.2.dev129}/src/log_foundry/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log-foundry
3
- Version: 0.10.2.dev127
3
+ Version: 0.10.2.dev129
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -347,7 +347,7 @@ lands can arrive at a sink whose close has already run, and nothing owes that si
347
347
 
348
348
  ### `@trace`
349
349
 
350
- Decorate any **synchronous** function. Usable bare or with arguments:
350
+ Decorate a function, sync or async. Usable bare or with arguments:
351
351
 
352
352
  ```python
353
353
  @lf.trace # span name = func.__qualname__
@@ -363,7 +363,10 @@ def process(): ...
363
363
 
364
364
  `@trace` refuses at decoration what it cannot trace, with a `TypeError` naming the function:
365
365
  a `@classmethod` or `@staticmethod` written *below* it (put them above), a bare string
366
- (`@trace("checkout")` meant `@trace(name="checkout")`), and anything not callable.
366
+ (`@trace("checkout")` meant `@trace(name="checkout")`), anything not callable, and a
367
+ **generator function** (sync or async, including one behind `@contextmanager`, `@lru_cache` or
368
+ any `functools.wraps` wrapper) — its body runs after the wrapper has returned, so the span would
369
+ close before it started. Trace the consumer instead: the function that iterates it.
367
370
 
368
371
  The **outermost** decorated call starts a new trace; every nested decorated call becomes a
369
372
  child span within it. On an exception, the decorator records `status="error"` plus an `error`
@@ -1083,7 +1086,7 @@ They tell you different things, and they want different responses:
1083
1086
 
1084
1087
  | Field | Means | What to do |
1085
1088
  |---|---|---|
1086
- | `dropped` | The queue filled — the destination is not keeping up. Delivery continues. | Make the destination keep up: scale the sink, or reduce what you log. The worker's batch size, flush interval and queue depth are **not** reachable from the public API, so tuning them is not an option this version offers you. |
1089
+ | `dropped` | The queue filled — the destination is not keeping up. Delivery continues. It counts **submissions** — one per span close, or per open span a `flush()` sweeps — so the events lost are a multiple of it. | Make the destination keep up: scale the sink, or reduce what you log. The worker's batch size, flush interval and queue depth are **not** reachable from the public API, so tuning them is not an option this version offers you. |
1087
1090
  | `failed_batches` | A sink stayed broken through the whole retry budget. Delivery continues. | Fix the destination. |
1088
1091
  | `stopped_reason` | The background thread **died** on that exception type. Nothing further will be delivered, ever. | Restart the process; investigate the named exception. |
1089
1092
  | `sink.dropped` | The sink discarded events **before** attempting delivery — an oversized record, or one the client refused outright. | Read the stderr line: it names the cause. An oversized record means shrink what you log; a refused local produce/publish (Kafka, Pub/Sub) points at the client — a saturated buffer, a bad topic, a credential. |
@@ -1092,12 +1095,13 @@ They tell you different things, and they want different responses:
1092
1095
  | `incomplete_swaps` | A late `configure(sink=...)` could not confirm the previous sink was drained. The swap took effect; that sink was left open and some queued events may have gone to the new one. | Investigate the previous sink — it was hung or failing. Configure the sink before the first log where you can. |
1093
1096
  | `inherited_sink` | This process is delivering to a sink it **inherited across a `fork`** and may not release, so it will not be closed here. Not a loss and not an alert term. | Nothing, usually. It explains a handle still open after `shutdown()`, and tells you a deployment shares one sink across a fork at all. `True` for a shared `StdoutSink` too, whose `close()` only flushes — so a `True` is not by itself evidence that anything is held. If you want the child to own its transport, build the sink in the worker process (see Forking). |
1094
1097
  | `orphan_lost` | An event logged **with no open span** never reached the sink. That call emits on your own thread with no worker behind it, so no other field here can carry it — it is not a batch, there was no retry, and there may be no worker at all. Covers a sink that failed to *construct* as well as one that raised. | Fix the destination, or the data. The stderr line names the exception type. If a process logs this way at all, this is the field to alert on: nothing else describes that path. |
1095
- | `in_span_lost` | An event logged **inside a span** could not be built — a value that could not be turned into an event. Always the data, never the destination: the in-span path cannot fail at delivery, which is `failed_batches`. | Fix the call site. Passing a non-string message (an exception object, say) is the common cause. |
1098
+ | `in_span_lost` | An event logged **inside a span** was lost before delivery, for one of two reasons the count tells apart: **the data** — a value that could not be turned into an event, one per call — or **no drain thread at all** — the process could not start the worker, and the span's whole buffer is counted at once (SPEC-050). Never a destination that failed at delivery; that is `failed_batches`. | One event at a time: fix the call site — a non-string message (an exception object, say) is the common cause. A whole buffer at once: the process cannot start a thread; the stderr line names it. |
1096
1099
  | `closing_sinks` | Swapped-out sinks inside `close()` **right now** — a live gauge, not a counter. It and `queued` are the two fields here that fall as well as rise; the other integer counters only climb. Non-zero on a single read is normal during a swap. | Nothing, unless it stays non-zero. That means a destination is stuck in `close()` and will not release its resources. |
1097
1100
 
1098
1101
  `orphan_lost` and `in_span_lost` are deliberately two fields and their sum is deliberately not
1099
1102
  reported. They aggregate different failure populations — one can mean the destination *or* the
1100
- data, the other can only mean the data — so a single number would hide which fix applies.
1103
+ data; the other never the destination — the data, or no drain thread at all (SPEC-050) — so a
1104
+ single number would hide which fix applies.
1101
1105
 
1102
1106
  `h.sink` is a `SinkLosses`, carrying `dropped` and `failed`, or `None` — `None` when no worker
1103
1107
  exists yet, or when the configured sink reports nothing (`losses()` is optional, and a sink whose
@@ -296,7 +296,7 @@ lands can arrive at a sink whose close has already run, and nothing owes that si
296
296
 
297
297
  ### `@trace`
298
298
 
299
- Decorate any **synchronous** function. Usable bare or with arguments:
299
+ Decorate a function, sync or async. Usable bare or with arguments:
300
300
 
301
301
  ```python
302
302
  @lf.trace # span name = func.__qualname__
@@ -312,7 +312,10 @@ def process(): ...
312
312
 
313
313
  `@trace` refuses at decoration what it cannot trace, with a `TypeError` naming the function:
314
314
  a `@classmethod` or `@staticmethod` written *below* it (put them above), a bare string
315
- (`@trace("checkout")` meant `@trace(name="checkout")`), and anything not callable.
315
+ (`@trace("checkout")` meant `@trace(name="checkout")`), anything not callable, and a
316
+ **generator function** (sync or async, including one behind `@contextmanager`, `@lru_cache` or
317
+ any `functools.wraps` wrapper) — its body runs after the wrapper has returned, so the span would
318
+ close before it started. Trace the consumer instead: the function that iterates it.
316
319
 
317
320
  The **outermost** decorated call starts a new trace; every nested decorated call becomes a
318
321
  child span within it. On an exception, the decorator records `status="error"` plus an `error`
@@ -1032,7 +1035,7 @@ They tell you different things, and they want different responses:
1032
1035
 
1033
1036
  | Field | Means | What to do |
1034
1037
  |---|---|---|
1035
- | `dropped` | The queue filled — the destination is not keeping up. Delivery continues. | Make the destination keep up: scale the sink, or reduce what you log. The worker's batch size, flush interval and queue depth are **not** reachable from the public API, so tuning them is not an option this version offers you. |
1038
+ | `dropped` | The queue filled — the destination is not keeping up. Delivery continues. It counts **submissions** — one per span close, or per open span a `flush()` sweeps — so the events lost are a multiple of it. | Make the destination keep up: scale the sink, or reduce what you log. The worker's batch size, flush interval and queue depth are **not** reachable from the public API, so tuning them is not an option this version offers you. |
1036
1039
  | `failed_batches` | A sink stayed broken through the whole retry budget. Delivery continues. | Fix the destination. |
1037
1040
  | `stopped_reason` | The background thread **died** on that exception type. Nothing further will be delivered, ever. | Restart the process; investigate the named exception. |
1038
1041
  | `sink.dropped` | The sink discarded events **before** attempting delivery — an oversized record, or one the client refused outright. | Read the stderr line: it names the cause. An oversized record means shrink what you log; a refused local produce/publish (Kafka, Pub/Sub) points at the client — a saturated buffer, a bad topic, a credential. |
@@ -1041,12 +1044,13 @@ They tell you different things, and they want different responses:
1041
1044
  | `incomplete_swaps` | A late `configure(sink=...)` could not confirm the previous sink was drained. The swap took effect; that sink was left open and some queued events may have gone to the new one. | Investigate the previous sink — it was hung or failing. Configure the sink before the first log where you can. |
1042
1045
  | `inherited_sink` | This process is delivering to a sink it **inherited across a `fork`** and may not release, so it will not be closed here. Not a loss and not an alert term. | Nothing, usually. It explains a handle still open after `shutdown()`, and tells you a deployment shares one sink across a fork at all. `True` for a shared `StdoutSink` too, whose `close()` only flushes — so a `True` is not by itself evidence that anything is held. If you want the child to own its transport, build the sink in the worker process (see Forking). |
1043
1046
  | `orphan_lost` | An event logged **with no open span** never reached the sink. That call emits on your own thread with no worker behind it, so no other field here can carry it — it is not a batch, there was no retry, and there may be no worker at all. Covers a sink that failed to *construct* as well as one that raised. | Fix the destination, or the data. The stderr line names the exception type. If a process logs this way at all, this is the field to alert on: nothing else describes that path. |
1044
- | `in_span_lost` | An event logged **inside a span** could not be built — a value that could not be turned into an event. Always the data, never the destination: the in-span path cannot fail at delivery, which is `failed_batches`. | Fix the call site. Passing a non-string message (an exception object, say) is the common cause. |
1047
+ | `in_span_lost` | An event logged **inside a span** was lost before delivery, for one of two reasons the count tells apart: **the data** — a value that could not be turned into an event, one per call — or **no drain thread at all** — the process could not start the worker, and the span's whole buffer is counted at once (SPEC-050). Never a destination that failed at delivery; that is `failed_batches`. | One event at a time: fix the call site — a non-string message (an exception object, say) is the common cause. A whole buffer at once: the process cannot start a thread; the stderr line names it. |
1045
1048
  | `closing_sinks` | Swapped-out sinks inside `close()` **right now** — a live gauge, not a counter. It and `queued` are the two fields here that fall as well as rise; the other integer counters only climb. Non-zero on a single read is normal during a swap. | Nothing, unless it stays non-zero. That means a destination is stuck in `close()` and will not release its resources. |
1046
1049
 
1047
1050
  `orphan_lost` and `in_span_lost` are deliberately two fields and their sum is deliberately not
1048
1051
  reported. They aggregate different failure populations — one can mean the destination *or* the
1049
- data, the other can only mean the data — so a single number would hide which fix applies.
1052
+ data; the other never the destination — the data, or no drain thread at all (SPEC-050) — so a
1053
+ single number would hide which fix applies.
1050
1054
 
1051
1055
  `h.sink` is a `SinkLosses`, carrying `dropped` and `failed`, or `None` — `None` when no worker
1052
1056
  exists yet, or when the configured sink reports nothing (`losses()` is optional, and a sink whose
@@ -74,7 +74,7 @@ keywords = [
74
74
  # vulnerability-reporting channel. The repository is still named `log-forge` — the ORIGINAL name,
75
75
  # which PyPI rejected for the distribution — so these URLs deliberately do not match the package
76
76
  # name. See the note on `name` above before "correcting" them.
77
- version = "0.10.2.dev127"
77
+ version = "0.10.2.dev129"
78
78
 
79
79
  [project.urls]
80
80
  Homepage = "https://github.com/agriffi10/log-forge"
@@ -87,8 +87,8 @@ def health() -> Health:
87
87
  an event that cannot be *built* never reaches a queue at all — so every other field here
88
88
  describes machinery those two losses never touched, and a process that only logs outside a
89
89
  span read all zeros over total loss until they existed. They stay separate because one can
90
- mean the destination or the data and the other can only mean the data; their sum is a number
91
- nobody can act on.
90
+ mean the destination or the data and the other never the destination — the data, or no drain
91
+ thread at all (SPEC-050 FR-003); their sum is a number nobody can act on.
92
92
 
93
93
  ``retired`` alone is not a fault — a process that shuts down and then stops logging is
94
94
  doing the right thing, which is why it is paired with the count rather than alerted on.
@@ -271,7 +271,12 @@ def get_config() -> Config:
271
271
  validation :func:`configure` performs — ``max_value_bytes = 0`` was accepted and emptied
272
272
  every event it touched. Both measured. :func:`configure` is the only route to a change.
273
273
 
274
- It is a **copy**, not the frozen original, and ``defaults`` is copied with it. A caller who
274
+ It is a **copy**, not the frozen original, and ``defaults`` is copied with it — **one level
275
+ deep**: the mapping is the caller's own, its values are shared with the library, so a nested
276
+ mutable value edited through the copy still reaches every later event (2026-09-04 audit,
277
+ N8). Deep-copying was declined because a default can be any object and copying an arbitrary
278
+ object can raise or be wrong; a caller who needs isolation for a nested value copies it. A
279
+ caller who
275
280
  defeats the freeze — ``object.__setattr__`` reaches through any frozen dataclass — then
276
281
  edits an object the library does not read, rather than the live config; and ``defaults`` is
277
282
  a plain mutable ``dict``, so sharing it would leave the freeze cosmetic at the one field
@@ -282,7 +287,8 @@ def get_config() -> Config:
282
287
  None.
283
288
 
284
289
  Returns:
285
- A copy of the process-wide :class:`Config`, with its own ``defaults``.
290
+ A copy of the process-wide :class:`Config`, with its own top-level ``defaults`` mapping
291
+ whose values are shared.
286
292
 
287
293
  Raises:
288
294
  None.
@@ -639,6 +639,21 @@ def _refuse_unusable(fn: object) -> None:
639
639
  ``self`` to a function declared without one. A ``str`` is the slip ``@trace("checkout")``,
640
640
  which meant ``name=``. Anything else that is not callable is refused by type.
641
641
 
642
+ A generator function or an async generator function is refused too (SPEC-055 FR-003): its
643
+ body runs after the wrapper has returned the generator object, so the span would open and
644
+ close before a single line of it ran, and every event the body logged would be an orphan on
645
+ a fresh trace — measured, ``['span.start', 'span.end']`` before the first ``next()``. The
646
+ check is the code flags, on ``fn``, on whatever ``fn`` advertises through ``__wrapped__``
647
+ — so ``@trace`` above ``@contextmanager``, ``@lru_cache`` or any ``functools.wraps`` wrapper
648
+ of a generator is refused too, each measured recording the same orphaned ``inside`` event —
649
+ and on the type's ``__call__`` for a callable instance; all of them see through a
650
+ ``functools.partial``. A plain function that merely *returns* a generator object without
651
+ advertising it is indistinguishable here and is not detected, which is a stated limit
652
+ rather than a gap to close at call time, where invariant 13 does not refuse. Wrapping the
653
+ iteration instead — a span per generator, pushed around every resumption — was considered
654
+ and deferred: a refusal can be lifted into a wrap later without breaking anyone, while a
655
+ wrap shipped first freezes its semantics at 1.0.
656
+
642
657
  Args:
643
658
  fn: Whatever was handed to the decorator.
644
659
 
@@ -646,8 +661,8 @@ def _refuse_unusable(fn: object) -> None:
646
661
  None.
647
662
 
648
663
  Raises:
649
- TypeError: If ``fn`` is a ``classmethod`` or ``staticmethod`` object, a ``str``, or not
650
- callable at all.
664
+ TypeError: If ``fn`` is a ``classmethod`` or ``staticmethod`` object, a ``str``, not
665
+ callable at all, or a generator function or async generator function.
651
666
  Exception: Whatever the callable's own attribute access raises — a ``__class__`` property,
652
667
  say — at decoration, in the caller's frame, which is invariant 13's moment.
653
668
  """
@@ -664,6 +679,61 @@ def _refuse_unusable(fn: object) -> None:
664
679
  )
665
680
  if not callable(fn):
666
681
  raise TypeError(f"@trace needs a callable, got {type(fn).__name__}")
682
+ if _is_generator_function(fn):
683
+ raise TypeError(
684
+ f"@trace cannot trace the iteration of the generator function {_span_name(fn)}: its "
685
+ f"body runs after the wrapper has returned, so the span would close before it "
686
+ f"starts. Trace the consumer instead: the function that iterates it"
687
+ )
688
+
689
+
690
+ def _is_generator_function(fn: object) -> bool:
691
+ """Reports whether a callable's body is a generator or async generator (SPEC-055 FR-003).
692
+
693
+ The same shape as :func:`_is_async`: the code flags of ``fn`` itself, which see through a
694
+ ``functools.partial``; then of whatever ``fn`` advertises through ``__wrapped__``, which is
695
+ how ``@contextmanager``, ``@lru_cache`` and every ``functools.wraps`` wrapper point back at
696
+ the generator they wrap; and for anything that is not a plain function the type's
697
+ ``__call__`` as well, so a callable instance whose ``__call__`` yields is caught too. The
698
+ unwrap is guarded because :func:`inspect.unwrap` raises ``ValueError`` on a ``__wrapped__``
699
+ cycle, and a cycle is not a generator.
700
+
701
+ Args:
702
+ fn: The callable being decorated.
703
+
704
+ Returns:
705
+ Whether ``fn`` would return a generator or async generator object when called.
706
+
707
+ Raises:
708
+ Exception: Whatever the callable's own ``__class__``, ``__call__`` or ``__wrapped__``
709
+ lookup raises, at decoration, in the caller's frame.
710
+ """
711
+ if _yields(fn):
712
+ return True
713
+ try:
714
+ target = inspect.unwrap(cast("Callable[..., Any]", fn))
715
+ except ValueError:
716
+ target = fn
717
+ if target is not fn and _yields(target):
718
+ return True
719
+ if inspect.isfunction(fn):
720
+ return False
721
+ return _yields(type(fn).__call__)
722
+
723
+
724
+ def _yields(fn: object) -> bool:
725
+ """Reads a callable's code flags for a generator or async generator body.
726
+
727
+ Args:
728
+ fn: The callable to inspect.
729
+
730
+ Returns:
731
+ Whether its body is a generator or an async generator.
732
+
733
+ Raises:
734
+ None.
735
+ """
736
+ return inspect.isgeneratorfunction(fn) or inspect.isasyncgenfunction(fn)
667
737
 
668
738
 
669
739
  def _span_name(fn: object) -> str:
@@ -749,8 +819,10 @@ def trace(
749
819
 
750
820
  Raises:
751
821
  TypeError: At decoration, for a ``classmethod`` or ``staticmethod`` object (the decorators
752
- are in the wrong order), a ``str`` (``name=`` was meant), anything not callable, or a
753
- ``name=`` that is not a ``str`` (SPEC-055 FR-002, invariant 13).
822
+ are in the wrong order), a ``str`` (``name=`` was meant), anything not callable, a
823
+ ``name=`` that is not a ``str`` (SPEC-055 FR-002), or a generator function, whose
824
+ iteration cannot be traced by a wrapper around the call (SPEC-055 FR-003) — invariant
825
+ 13.
754
826
  """
755
827
  span_defaults = None if defaults is None else dict(defaults)
756
828
 
@@ -137,7 +137,9 @@ def parse_traceparent(value: object) -> tuple[str, str] | None:
137
137
  its own identity.
138
138
 
139
139
  Args:
140
- value: An inbound header value of any type, from outside the process.
140
+ value: An inbound header value of any type, from outside the process. Surrounding
141
+ whitespace is stripped before parsing, since a header value arrives trimmed from some
142
+ transports and not others; whitespace anywhere else is a malformed field.
141
143
 
142
144
  Returns:
143
145
  A ``(trace_id, span_id)`` tuple, or ``None`` if the value is unusable.
@@ -282,11 +282,15 @@ class KinesisSink:
282
282
 
283
283
 
284
284
  MAX_PARTITION_KEY_BYTES = 256
285
- """The service's ceiling on a ``PutRecords`` partition key, in UTF-8 bytes."""
285
+ """This sink's ceiling on a ``PutRecords`` partition key, in UTF-8 bytes.
286
+
287
+ At or below the service's own limit, which its API reference states as 256 **characters**: a
288
+ byte bound is stricter for any non-ASCII key and therefore safe (2026-09-04 audit, N12).
289
+ """
286
290
 
287
291
 
288
292
  def _partition_key(raw: str) -> str:
289
- """Bounds a partition key to the service's 256 **bytes**, never 256 characters.
293
+ """Bounds a partition key to 256 UTF-8 **bytes**, at or below the service's 256-character limit.
290
294
 
291
295
  Both encodes carry an ``errors=`` and both are load-bearing. Assembly replaces a lone
292
296
  surrogate since SPEC-055 FR-001, so an event field cannot carry one — but this key is derived
@@ -319,7 +323,8 @@ def _record_size(record: dict[str, Any]) -> int:
319
323
  """Measures one request entry, partition key included (SPEC-038 FR-009).
320
324
 
321
325
  ``PutRecords`` charges the partition key against the 5 MiB request limit, and a key may be up
322
- to 256 bytes, so a 500-record request could understate itself by ~128 KB — enough to have the
326
+ to 256 characters at the service (bounded here to 256 bytes), so a 500-record request could
327
+ understate itself by ~128 KB — enough to have the
323
328
  service reject a chunk this sink believed was inside the budget. ``SQSSink`` charges its FIFO
324
329
  ids for the same reason and records the same rationale.
325
330