provide-uterm-annotation 0.5.1__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 (20) hide show
  1. provide_uterm_annotation-0.5.1/PKG-INFO +16 -0
  2. provide_uterm_annotation-0.5.1/VERSION +1 -0
  3. provide_uterm_annotation-0.5.1/pyproject.toml +96 -0
  4. provide_uterm_annotation-0.5.1/setup.cfg +4 -0
  5. provide_uterm_annotation-0.5.1/src/provide/uterm/annotation/__init__.py +22 -0
  6. provide_uterm_annotation-0.5.1/src/provide/uterm/annotation/_detector.py +86 -0
  7. provide_uterm_annotation-0.5.1/src/provide/uterm/annotation/_models.py +60 -0
  8. provide_uterm_annotation-0.5.1/src/provide/uterm/annotation/_rules.py +216 -0
  9. provide_uterm_annotation-0.5.1/src/provide/uterm/annotation/_streaming.py +63 -0
  10. provide_uterm_annotation-0.5.1/src/provide/uterm/annotation/py.typed +0 -0
  11. provide_uterm_annotation-0.5.1/src/provide_uterm_annotation.egg-info/PKG-INFO +16 -0
  12. provide_uterm_annotation-0.5.1/src/provide_uterm_annotation.egg-info/SOURCES.txt +18 -0
  13. provide_uterm_annotation-0.5.1/src/provide_uterm_annotation.egg-info/dependency_links.txt +1 -0
  14. provide_uterm_annotation-0.5.1/src/provide_uterm_annotation.egg-info/requires.txt +11 -0
  15. provide_uterm_annotation-0.5.1/src/provide_uterm_annotation.egg-info/top_level.txt +1 -0
  16. provide_uterm_annotation-0.5.1/tests/test_annotation_detector.py +218 -0
  17. provide_uterm_annotation-0.5.1/tests/test_annotation_models.py +189 -0
  18. provide_uterm_annotation-0.5.1/tests/test_annotation_rules.py +245 -0
  19. provide_uterm_annotation-0.5.1/tests/test_detector_exception_path.py +86 -0
  20. provide_uterm_annotation-0.5.1/tests/test_streaming_detector.py +85 -0
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: provide-uterm-annotation
3
+ Version: 0.5.1
4
+ Summary: Session-annotation models, pattern detector, and built-in rules for provide-uterm
5
+ License-Expression: AGPL-3.0-or-later
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: provide-uterm>=0.5.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=9.0; extra == "dev"
10
+ Requires-Dist: pytest-asyncio>=1.3; extra == "dev"
11
+ Requires-Dist: pytest-cov>=7.0; extra == "dev"
12
+ Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
13
+ Requires-Dist: mypy>=1.19; extra == "dev"
14
+ Requires-Dist: ruff>=0.15; extra == "dev"
15
+ Requires-Dist: bandit>=1.9; extra == "dev"
16
+ Requires-Dist: provide-uterm>=0.5.0; extra == "dev"
@@ -0,0 +1 @@
1
+ 0.5.1
@@ -0,0 +1,96 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ [build-system]
5
+ requires = ["setuptools>=61.0", "wheel"]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "provide-uterm-annotation"
10
+ dynamic = ["version"]
11
+ description = "Session-annotation models, pattern detector, and built-in rules for provide-uterm"
12
+ requires-python = ">=3.11"
13
+ license = "AGPL-3.0-or-later"
14
+ dependencies = ["provide-uterm>=0.5.0"]
15
+
16
+ [project.optional-dependencies]
17
+ dev = [
18
+ "pytest>=9.0",
19
+ "pytest-asyncio>=1.3",
20
+ "pytest-cov>=7.0",
21
+ "pytest-timeout>=2.4.0",
22
+ "mypy>=1.19",
23
+ "ruff>=0.15",
24
+ "bandit>=1.9",
25
+ "provide-uterm>=0.5.0",
26
+ ]
27
+
28
+ [tool.setuptools.dynamic]
29
+ version = {file = "VERSION"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.setuptools.package-data]
35
+ "provide.uterm" = ["py.typed"]
36
+ "provide.uterm.annotation" = ["py.typed"]
37
+
38
+ [tool.pytest.ini_options]
39
+ consider_namespace_packages = true
40
+ asyncio_mode = "auto"
41
+ asyncio_default_fixture_loop_scope = "function"
42
+ testpaths = ["tests"]
43
+ addopts = [
44
+ "--import-mode=importlib",
45
+ "-v",
46
+ "--strict-markers",
47
+ "--strict-config",
48
+ "--tb=short",
49
+ "-ra",
50
+ "--cov=provide.uterm.annotation",
51
+ "--cov-branch",
52
+ "--cov-report=term-missing",
53
+ "--cov-fail-under=100",
54
+ ]
55
+
56
+ [tool.coverage.run]
57
+ source = ["provide.uterm.annotation"]
58
+ omit = ["tests/*"]
59
+ branch = true
60
+
61
+ [tool.coverage.report]
62
+ fail_under = 100
63
+ show_missing = true
64
+ skip_covered = false
65
+
66
+ [tool.ruff]
67
+ target-version = "py311"
68
+ line-length = 120
69
+
70
+ [tool.ruff.lint]
71
+ select = [
72
+ "E", "W", "F", "I", "N", "UP", "B", "C4", "SIM", "TCH", "PTH",
73
+ "DTZ", "ICN", "ISC", "PERF", "PIE", "RET", "RSE", "TID", "S", "ARG", "RUF",
74
+ ]
75
+ # Aligned with the workspace-root ruff config in ../../pyproject.toml.
76
+ ignore = [
77
+ "E501", "S101", "S104", "S108", "S110", "S112", "S311", "S603", "S607",
78
+ "S604", "F811", "N806", "N814", "B017", "SIM105", "SIM117", "ARG001",
79
+ "ARG002", "PTH108", "SIM108", "PERF401",
80
+ ]
81
+
82
+ [tool.ruff.lint.per-file-ignores]
83
+ "__init__.py" = ["F401", "RUF022"]
84
+ "tests/**/*.py" = ["E402", "B905", "S101", "S105", "S106", "S110", "ARG", "TCH", "RUF003", "RUF012", "RUF015", "RUF043", "RUF059"]
85
+
86
+ [tool.mypy]
87
+ python_version = "3.11"
88
+ strict = true
89
+ namespace_packages = true
90
+ explicit_package_bases = true
91
+ mypy_path = ["src"]
92
+ warn_return_any = true
93
+ warn_unused_configs = true
94
+
95
+ [tool.uv.sources]
96
+ provide-uterm = { workspace = true }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+
6
+ """Annotation feature: mark and detect interesting moments in terminal session recordings."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from provide.uterm.annotation._detector import PatternDetector
11
+ from provide.uterm.annotation._models import Annotation, AnnotationSpan, DetectionRule
12
+ from provide.uterm.annotation._rules import BUILTIN_RULES
13
+ from provide.uterm.annotation._streaming import StreamingDetector
14
+
15
+ __all__ = [
16
+ "Annotation",
17
+ "AnnotationSpan",
18
+ "BUILTIN_RULES",
19
+ "DetectionRule",
20
+ "PatternDetector",
21
+ "StreamingDetector",
22
+ ]
@@ -0,0 +1,86 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ """PatternDetector — hot-path scanner that matches terminal event text against DetectionRules."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from provide.uterm.annotation._models import Annotation, AnnotationSpan, DetectionRule
10
+ from provide.uterm.annotation._rules import BUILTIN_RULES
11
+
12
+ _DESCRIPTION_TRUNCATE = 80
13
+ # Placeholder used in the fallback description so the raw match (a potential
14
+ # secret) is never embedded when a description_template fails to format.
15
+ _FALLBACK_PLACEHOLDER = "<unavailable>"
16
+
17
+
18
+ class PatternDetector:
19
+ """Scan terminal event text against a set of :class:`DetectionRule` objects.
20
+
21
+ Designed for the hot path: returns an empty list immediately when *text* is
22
+ empty and performs no allocations beyond the returned list when no rules
23
+ match.
24
+ """
25
+
26
+ def __init__(self, rules: list[DetectionRule] | None = None) -> None:
27
+ """Initialise with *rules*.
28
+
29
+ If *rules* is ``None`` the built-in rule set is used.
30
+ """
31
+ self._rules: list[DetectionRule] = BUILTIN_RULES if rules is None else rules
32
+
33
+ def detect(self, event_type: str, text: str, seq: int) -> list[Annotation]:
34
+ """Scan *text* against all rules that apply to *event_type*.
35
+
36
+ Returns a (possibly empty) list of :class:`Annotation` objects. At
37
+ most one annotation is returned per *category* — the first rule whose
38
+ pattern matches wins and later rules in that category are skipped.
39
+ """
40
+ return self.scan(event_type, text, seq)[0]
41
+
42
+ def scan(self, event_type: str, text: str, seq: int) -> tuple[list[Annotation], int]:
43
+ """Like :meth:`detect`, but also return the end offset of the furthest
44
+ match in *text* (0 when nothing matches).
45
+
46
+ :class:`StreamingDetector` uses the offset to carry only the window tail
47
+ *after* the matched region — bridging a second secret that straddles the
48
+ boundary without re-reporting a match that already completed.
49
+ """
50
+ if not text:
51
+ return [], 0
52
+
53
+ results: list[Annotation] = []
54
+ seen_categories: set[str] = set()
55
+ max_end = 0
56
+
57
+ for rule in self._rules:
58
+ if rule.category in seen_categories:
59
+ continue
60
+ if event_type not in rule.event_types:
61
+ continue
62
+ m = rule.pattern.search(text)
63
+ if m is None:
64
+ continue
65
+
66
+ seen_categories.add(rule.category)
67
+ max_end = max(max_end, m.end())
68
+ match_text = m.group(0)[:_DESCRIPTION_TRUNCATE]
69
+ try:
70
+ description = rule.description_template.format(match=match_text, event_type=event_type)
71
+ except (KeyError, IndexError):
72
+ # A malformed template must not leak the raw match (a potential
73
+ # secret) into the description, which flows to telemetry/logs.
74
+ description = f"{rule.label}: {_FALLBACK_PLACEHOLDER}"
75
+ results.append(
76
+ Annotation(
77
+ label=rule.label,
78
+ description=description,
79
+ severity=rule.severity,
80
+ source="detector",
81
+ principal="system",
82
+ span=AnnotationSpan(from_seq=seq, to_seq=seq),
83
+ )
84
+ )
85
+
86
+ return results, max_end
@@ -0,0 +1,60 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ """Data models for session annotation — marking interesting moments in terminal recordings."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ import re
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class AnnotationSpan:
18
+ """A contiguous range of recording event sequence numbers."""
19
+
20
+ from_seq: int
21
+ to_seq: int
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class Annotation:
26
+ """A single annotation marking an interesting moment (or range) in a session recording."""
27
+
28
+ label: str
29
+ description: str
30
+ severity: str
31
+ source: str
32
+ principal: str
33
+ span: AnnotationSpan | None = None
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ """Serialise to a plain :class:`dict`, with ``span`` as a nested dict if present."""
37
+ result: dict[str, Any] = {
38
+ "label": self.label,
39
+ "description": self.description,
40
+ "severity": self.severity,
41
+ "source": self.source,
42
+ "principal": self.principal,
43
+ "span": None,
44
+ }
45
+ if self.span is not None:
46
+ result["span"] = {"from_seq": self.span.from_seq, "to_seq": self.span.to_seq}
47
+ return result
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class DetectionRule:
52
+ """A compiled regex rule used to detect annotation-worthy events in terminal output."""
53
+
54
+ rule_id: str
55
+ label: str
56
+ pattern: re.Pattern[str]
57
+ severity: str
58
+ description_template: str
59
+ event_types: frozenset[str]
60
+ category: str
@@ -0,0 +1,216 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ """Built-in detection rules for session annotation."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from provide.uterm.annotation._models import DetectionRule
12
+
13
+ # All built-in rules apply to both inbound and outbound event streams.
14
+ _BOTH: frozenset[str] = frozenset({"read", "send"})
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Built-in rules — ordered most-specific first within each category.
18
+ # ---------------------------------------------------------------------------
19
+
20
+ BUILTIN_RULES: list[DetectionRule] = [
21
+ # -------------------------------------------------------------------------
22
+ # Credentials — high severity
23
+ # -------------------------------------------------------------------------
24
+ DetectionRule(
25
+ rule_id="cred.aws_access_key",
26
+ label="credential_exposure",
27
+ pattern=re.compile(r"AKIA[0-9A-Z]{12}"),
28
+ severity="high",
29
+ description_template="AWS access key detected in {event_type}",
30
+ event_types=_BOTH,
31
+ category="credentials",
32
+ ),
33
+ DetectionRule(
34
+ rule_id="cred.github_token",
35
+ label="credential_exposure",
36
+ pattern=re.compile(r"gh[psourx]_[A-Za-z0-9_]{8}"),
37
+ severity="high",
38
+ description_template="GitHub token detected in {event_type}",
39
+ event_types=_BOTH,
40
+ category="credentials",
41
+ ),
42
+ DetectionRule(
43
+ rule_id="cred.generic_secret",
44
+ label="credential_exposure",
45
+ pattern=re.compile(r"(?i)(password|secret|token|api_key)\s*[=:]"),
46
+ severity="high",
47
+ description_template="Secret assignment detected in {event_type}",
48
+ event_types=_BOTH,
49
+ category="credentials",
50
+ ),
51
+ DetectionRule(
52
+ rule_id="cred.bearer_token",
53
+ label="credential_exposure",
54
+ pattern=re.compile(r"Bearer\s+\S{8}"),
55
+ severity="high",
56
+ description_template="Bearer token detected in {event_type}",
57
+ event_types=_BOTH,
58
+ category="credentials",
59
+ ),
60
+ DetectionRule(
61
+ rule_id="cred.private_key_header",
62
+ label="credential_exposure",
63
+ pattern=re.compile(r"-----BEGIN\s+(RSA|EC|OPENSSH)\s+PRIVATE KEY-----"),
64
+ severity="high",
65
+ description_template="Private key header detected: {match}",
66
+ event_types=_BOTH,
67
+ category="credentials",
68
+ ),
69
+ # -------------------------------------------------------------------------
70
+ # Privilege escalation — high severity
71
+ # -------------------------------------------------------------------------
72
+ DetectionRule(
73
+ rule_id="esc.sudo",
74
+ label="privilege_escalation",
75
+ pattern=re.compile(r"\bsudo\b"),
76
+ severity="high",
77
+ description_template="sudo command detected: {match}",
78
+ event_types=_BOTH,
79
+ category="escalation",
80
+ ),
81
+ DetectionRule(
82
+ rule_id="esc.su_dash",
83
+ label="privilege_escalation",
84
+ pattern=re.compile(r"\bsu\s+-"),
85
+ severity="high",
86
+ description_template="su - (switch to root) detected: {match}",
87
+ event_types=_BOTH,
88
+ category="escalation",
89
+ ),
90
+ DetectionRule(
91
+ rule_id="esc.pkexec",
92
+ label="privilege_escalation",
93
+ pattern=re.compile(r"\bpkexec\b"),
94
+ severity="high",
95
+ description_template="pkexec privilege escalation detected: {match}",
96
+ event_types=_BOTH,
97
+ category="escalation",
98
+ ),
99
+ # -------------------------------------------------------------------------
100
+ # Destructive commands — critical severity
101
+ # -------------------------------------------------------------------------
102
+ DetectionRule(
103
+ rule_id="dest.rm_rf",
104
+ label="destructive_command",
105
+ pattern=re.compile(r"\brm\s+(-[rRf]{2,}|-[rR]\s+-f|-f\s+-[rR])"),
106
+ severity="critical",
107
+ description_template="Recursive force-remove detected: {match}",
108
+ event_types=_BOTH,
109
+ category="destructive",
110
+ ),
111
+ DetectionRule(
112
+ rule_id="dest.drop_table",
113
+ label="destructive_command",
114
+ pattern=re.compile(r"(?i)\bDROP\s+(TABLE|DATABASE)\b"),
115
+ severity="critical",
116
+ description_template="SQL DROP statement detected: {match}",
117
+ event_types=_BOTH,
118
+ category="destructive",
119
+ ),
120
+ DetectionRule(
121
+ rule_id="dest.kubectl_delete",
122
+ label="destructive_command",
123
+ pattern=re.compile(r"\bkubectl\s+delete\b"),
124
+ severity="critical",
125
+ description_template="kubectl delete command detected: {match}",
126
+ event_types=_BOTH,
127
+ category="destructive",
128
+ ),
129
+ DetectionRule(
130
+ rule_id="dest.dd_if",
131
+ label="destructive_command",
132
+ pattern=re.compile(r"\bdd\s+if="),
133
+ severity="critical",
134
+ description_template="dd disk-copy command detected: {match}",
135
+ event_types=_BOTH,
136
+ category="destructive",
137
+ ),
138
+ DetectionRule(
139
+ rule_id="dest.mkfs",
140
+ label="destructive_command",
141
+ pattern=re.compile(r"\bmkfs\."),
142
+ severity="critical",
143
+ description_template="mkfs (format filesystem) detected: {match}",
144
+ event_types=_BOTH,
145
+ category="destructive",
146
+ ),
147
+ # -------------------------------------------------------------------------
148
+ # Outbound connections — info severity
149
+ # -------------------------------------------------------------------------
150
+ DetectionRule(
151
+ rule_id="conn.ssh",
152
+ label="outbound_connection",
153
+ pattern=re.compile(r"\bssh\s+[\w.\-]+@"),
154
+ severity="info",
155
+ description_template="SSH connection detected: {match}",
156
+ event_types=_BOTH,
157
+ category="connections",
158
+ ),
159
+ DetectionRule(
160
+ rule_id="conn.curl",
161
+ label="outbound_connection",
162
+ pattern=re.compile(r"\bcurl\b.*https?://"),
163
+ severity="info",
164
+ description_template="curl HTTP request detected: {match}",
165
+ event_types=_BOTH,
166
+ category="connections",
167
+ ),
168
+ DetectionRule(
169
+ rule_id="conn.wget",
170
+ label="outbound_connection",
171
+ pattern=re.compile(r"\bwget\b.*https?://"),
172
+ severity="info",
173
+ description_template="wget HTTP request detected: {match}",
174
+ event_types=_BOTH,
175
+ category="connections",
176
+ ),
177
+ DetectionRule(
178
+ rule_id="conn.scp",
179
+ label="outbound_connection",
180
+ pattern=re.compile(r"\bscp\b"),
181
+ severity="info",
182
+ description_template="scp file transfer detected: {match}",
183
+ event_types=_BOTH,
184
+ category="connections",
185
+ ),
186
+ # -------------------------------------------------------------------------
187
+ # Session lifecycle — info severity
188
+ # -------------------------------------------------------------------------
189
+ DetectionRule(
190
+ rule_id="life.exit",
191
+ label="session_lifecycle",
192
+ pattern=re.compile(r"\bexit\b"),
193
+ severity="info",
194
+ description_template="exit command detected: {match}",
195
+ event_types=_BOTH,
196
+ category="lifecycle",
197
+ ),
198
+ DetectionRule(
199
+ rule_id="life.shutdown",
200
+ label="session_lifecycle",
201
+ pattern=re.compile(r"\bshutdown\b"),
202
+ severity="info",
203
+ description_template="shutdown command detected: {match}",
204
+ event_types=_BOTH,
205
+ category="lifecycle",
206
+ ),
207
+ DetectionRule(
208
+ rule_id="life.reboot",
209
+ label="session_lifecycle",
210
+ pattern=re.compile(r"\breboot\b"),
211
+ severity="info",
212
+ description_template="reboot command detected: {match}",
213
+ event_types=_BOTH,
214
+ category="lifecycle",
215
+ ),
216
+ ]
@@ -0,0 +1,63 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ """StreamingDetector — catch patterns split across consecutive detect() calls.
6
+
7
+ :class:`PatternDetector` is stateless: it scans one chunk at a time, so a
8
+ multi-character pattern (an AWS key, a URL) that happens to straddle two
9
+ ``detect()`` chunks is silently missed. This wrapper carries a small bounded
10
+ tail of the previous chunk and prepends it to the next one, so a boundary-split
11
+ match is still found.
12
+
13
+ It is **stateful** — use one instance per logical stream (one per session, and
14
+ not shared across event types whose text must not be concatenated). The wrapped
15
+ ``PatternDetector`` stays stateless and may be shared.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from provide.uterm.annotation._detector import PatternDetector
24
+ from provide.uterm.annotation._models import Annotation
25
+
26
+ # Longest fixed-shape secret we expect to bridge a boundary. Bounds how much of
27
+ # the previous chunk is retained (and re-scanned), capping memory and CPU.
28
+ _DEFAULT_MAX_CARRY = 512
29
+
30
+
31
+ class StreamingDetector:
32
+ """Stateful per-stream wrapper that bridges chunk boundaries for a detector."""
33
+
34
+ __slots__ = ("_carry", "_detector", "_max_carry")
35
+
36
+ def __init__(self, detector: PatternDetector, *, max_carry: int = _DEFAULT_MAX_CARRY) -> None:
37
+ self._detector = detector
38
+ self._max_carry = max_carry
39
+ self._carry = ""
40
+
41
+ def detect(self, event_type: str, text: str, seq: int) -> list[Annotation]:
42
+ """Scan *text* (joined with the carried tail) and return any matches.
43
+
44
+ A match is owned by the chunk in which it *completes* — the returned
45
+ annotation's span carries the *seq* passed for that chunk. The carried
46
+ tail is the bounded window suffix *after* the furthest match: it bridges
47
+ a secret straddling the next boundary (including a second one that begins
48
+ right after a completed match) without re-reporting a match that already
49
+ finished.
50
+ """
51
+ if not text:
52
+ return []
53
+ window = self._carry + text if self._carry else text
54
+ annotations, match_end = self._detector.scan(event_type, window, seq)
55
+ self._carry = window[match_end:][-self._max_carry :]
56
+ return annotations
57
+
58
+ def reset(self) -> None:
59
+ """Forget the carried tail (e.g. on screen clear / session resync)."""
60
+ self._carry = ""
61
+
62
+
63
+ __all__ = ["StreamingDetector"]
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: provide-uterm-annotation
3
+ Version: 0.5.1
4
+ Summary: Session-annotation models, pattern detector, and built-in rules for provide-uterm
5
+ License-Expression: AGPL-3.0-or-later
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: provide-uterm>=0.5.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=9.0; extra == "dev"
10
+ Requires-Dist: pytest-asyncio>=1.3; extra == "dev"
11
+ Requires-Dist: pytest-cov>=7.0; extra == "dev"
12
+ Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
13
+ Requires-Dist: mypy>=1.19; extra == "dev"
14
+ Requires-Dist: ruff>=0.15; extra == "dev"
15
+ Requires-Dist: bandit>=1.9; extra == "dev"
16
+ Requires-Dist: provide-uterm>=0.5.0; extra == "dev"
@@ -0,0 +1,18 @@
1
+ VERSION
2
+ pyproject.toml
3
+ src/provide/uterm/annotation/__init__.py
4
+ src/provide/uterm/annotation/_detector.py
5
+ src/provide/uterm/annotation/_models.py
6
+ src/provide/uterm/annotation/_rules.py
7
+ src/provide/uterm/annotation/_streaming.py
8
+ src/provide/uterm/annotation/py.typed
9
+ src/provide_uterm_annotation.egg-info/PKG-INFO
10
+ src/provide_uterm_annotation.egg-info/SOURCES.txt
11
+ src/provide_uterm_annotation.egg-info/dependency_links.txt
12
+ src/provide_uterm_annotation.egg-info/requires.txt
13
+ src/provide_uterm_annotation.egg-info/top_level.txt
14
+ tests/test_annotation_detector.py
15
+ tests/test_annotation_models.py
16
+ tests/test_annotation_rules.py
17
+ tests/test_detector_exception_path.py
18
+ tests/test_streaming_detector.py
@@ -0,0 +1,11 @@
1
+ provide-uterm>=0.5.0
2
+
3
+ [dev]
4
+ pytest>=9.0
5
+ pytest-asyncio>=1.3
6
+ pytest-cov>=7.0
7
+ pytest-timeout>=2.4.0
8
+ mypy>=1.19
9
+ ruff>=0.15
10
+ bandit>=1.9
11
+ provide-uterm>=0.5.0