headroom-ai 0.2.13__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.
- headroom/__init__.py +212 -0
- headroom/cache/__init__.py +76 -0
- headroom/cache/anthropic.py +517 -0
- headroom/cache/base.py +342 -0
- headroom/cache/compression_feedback.py +613 -0
- headroom/cache/compression_store.py +814 -0
- headroom/cache/dynamic_detector.py +1026 -0
- headroom/cache/google.py +884 -0
- headroom/cache/openai.py +584 -0
- headroom/cache/registry.py +175 -0
- headroom/cache/semantic.py +451 -0
- headroom/ccr/__init__.py +77 -0
- headroom/ccr/context_tracker.py +582 -0
- headroom/ccr/mcp_server.py +319 -0
- headroom/ccr/response_handler.py +772 -0
- headroom/ccr/tool_injection.py +415 -0
- headroom/cli.py +219 -0
- headroom/client.py +977 -0
- headroom/compression/__init__.py +42 -0
- headroom/compression/detector.py +424 -0
- headroom/compression/handlers/__init__.py +22 -0
- headroom/compression/handlers/base.py +219 -0
- headroom/compression/handlers/code_handler.py +506 -0
- headroom/compression/handlers/json_handler.py +418 -0
- headroom/compression/masks.py +345 -0
- headroom/compression/universal.py +465 -0
- headroom/config.py +474 -0
- headroom/exceptions.py +192 -0
- headroom/integrations/__init__.py +159 -0
- headroom/integrations/agno/__init__.py +53 -0
- headroom/integrations/agno/hooks.py +345 -0
- headroom/integrations/agno/model.py +625 -0
- headroom/integrations/agno/providers.py +154 -0
- headroom/integrations/langchain/__init__.py +106 -0
- headroom/integrations/langchain/agents.py +326 -0
- headroom/integrations/langchain/chat_model.py +1002 -0
- headroom/integrations/langchain/langsmith.py +324 -0
- headroom/integrations/langchain/memory.py +319 -0
- headroom/integrations/langchain/providers.py +200 -0
- headroom/integrations/langchain/retriever.py +371 -0
- headroom/integrations/langchain/streaming.py +341 -0
- headroom/integrations/mcp/__init__.py +37 -0
- headroom/integrations/mcp/server.py +533 -0
- headroom/memory/__init__.py +37 -0
- headroom/memory/extractor.py +390 -0
- headroom/memory/fast_store.py +621 -0
- headroom/memory/fast_wrapper.py +311 -0
- headroom/memory/inline_extractor.py +229 -0
- headroom/memory/store.py +434 -0
- headroom/memory/worker.py +260 -0
- headroom/memory/wrapper.py +321 -0
- headroom/models/__init__.py +39 -0
- headroom/models/registry.py +687 -0
- headroom/parser.py +293 -0
- headroom/pricing/__init__.py +51 -0
- headroom/pricing/anthropic_prices.py +81 -0
- headroom/pricing/litellm_pricing.py +113 -0
- headroom/pricing/openai_prices.py +91 -0
- headroom/pricing/registry.py +188 -0
- headroom/providers/__init__.py +61 -0
- headroom/providers/anthropic.py +621 -0
- headroom/providers/base.py +131 -0
- headroom/providers/cohere.py +362 -0
- headroom/providers/google.py +427 -0
- headroom/providers/litellm.py +297 -0
- headroom/providers/openai.py +566 -0
- headroom/providers/openai_compatible.py +521 -0
- headroom/proxy/__init__.py +19 -0
- headroom/proxy/server.py +2683 -0
- headroom/py.typed +0 -0
- headroom/relevance/__init__.py +124 -0
- headroom/relevance/base.py +106 -0
- headroom/relevance/bm25.py +255 -0
- headroom/relevance/embedding.py +255 -0
- headroom/relevance/hybrid.py +259 -0
- headroom/reporting/__init__.py +5 -0
- headroom/reporting/generator.py +549 -0
- headroom/storage/__init__.py +41 -0
- headroom/storage/base.py +125 -0
- headroom/storage/jsonl.py +220 -0
- headroom/storage/sqlite.py +289 -0
- headroom/telemetry/__init__.py +91 -0
- headroom/telemetry/collector.py +764 -0
- headroom/telemetry/models.py +880 -0
- headroom/telemetry/toin.py +1579 -0
- headroom/tokenizer.py +80 -0
- headroom/tokenizers/__init__.py +75 -0
- headroom/tokenizers/base.py +210 -0
- headroom/tokenizers/estimator.py +198 -0
- headroom/tokenizers/huggingface.py +317 -0
- headroom/tokenizers/mistral.py +245 -0
- headroom/tokenizers/registry.py +398 -0
- headroom/tokenizers/tiktoken_counter.py +248 -0
- headroom/transforms/__init__.py +106 -0
- headroom/transforms/base.py +57 -0
- headroom/transforms/cache_aligner.py +357 -0
- headroom/transforms/code_compressor.py +1313 -0
- headroom/transforms/content_detector.py +335 -0
- headroom/transforms/content_router.py +1158 -0
- headroom/transforms/llmlingua_compressor.py +638 -0
- headroom/transforms/log_compressor.py +529 -0
- headroom/transforms/pipeline.py +297 -0
- headroom/transforms/rolling_window.py +350 -0
- headroom/transforms/search_compressor.py +365 -0
- headroom/transforms/smart_crusher.py +2682 -0
- headroom/transforms/text_compressor.py +259 -0
- headroom/transforms/tool_crusher.py +338 -0
- headroom/utils.py +215 -0
- headroom_ai-0.2.13.dist-info/METADATA +315 -0
- headroom_ai-0.2.13.dist-info/RECORD +114 -0
- headroom_ai-0.2.13.dist-info/WHEEL +4 -0
- headroom_ai-0.2.13.dist-info/entry_points.txt +2 -0
- headroom_ai-0.2.13.dist-info/licenses/LICENSE +190 -0
- headroom_ai-0.2.13.dist-info/licenses/NOTICE +43 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Universal compressor with ML-based detection and structure preservation.
|
|
2
|
+
|
|
3
|
+
This is the main entry point for compression. It:
|
|
4
|
+
1. Detects content type using Magika (ML)
|
|
5
|
+
2. Extracts structure using appropriate handler
|
|
6
|
+
3. Compresses non-structural content with LLMLingua
|
|
7
|
+
4. Optionally stores original in CCR for retrieval
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
compressor = UniversalCompressor()
|
|
11
|
+
result = compressor.compress(content)
|
|
12
|
+
|
|
13
|
+
# Result contains:
|
|
14
|
+
# - compressed: The compressed content
|
|
15
|
+
# - compression_ratio: original_tokens / compressed_tokens
|
|
16
|
+
# - content_type: Detected content type
|
|
17
|
+
# - preservation_ratio: Fraction of content preserved as structure
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from headroom.compression.detector import (
|
|
28
|
+
ContentType,
|
|
29
|
+
DetectionResult,
|
|
30
|
+
FallbackDetector,
|
|
31
|
+
get_detector,
|
|
32
|
+
)
|
|
33
|
+
from headroom.compression.handlers.base import (
|
|
34
|
+
NoOpHandler,
|
|
35
|
+
StructureHandler,
|
|
36
|
+
)
|
|
37
|
+
from headroom.compression.handlers.code_handler import CodeStructureHandler
|
|
38
|
+
from headroom.compression.handlers.json_handler import JSONStructureHandler
|
|
39
|
+
from headroom.compression.masks import (
|
|
40
|
+
StructureMask,
|
|
41
|
+
compute_entropy_mask,
|
|
42
|
+
mask_to_spans,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class UniversalCompressorConfig:
|
|
50
|
+
"""Configuration for UniversalCompressor.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
use_magika: Use ML-based detection (requires magika package).
|
|
54
|
+
use_llmlingua: Use LLMLingua for content compression.
|
|
55
|
+
use_entropy_preservation: Preserve high-entropy tokens (UUIDs, etc.).
|
|
56
|
+
entropy_threshold: Threshold for entropy-based preservation.
|
|
57
|
+
min_content_length: Minimum content length to compress.
|
|
58
|
+
compression_ratio_target: Target compression ratio (0.0-1.0).
|
|
59
|
+
ccr_enabled: Store originals in CCR for retrieval.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
use_magika: bool = True
|
|
63
|
+
use_llmlingua: bool = True
|
|
64
|
+
use_entropy_preservation: bool = True
|
|
65
|
+
entropy_threshold: float = 0.85
|
|
66
|
+
min_content_length: int = 100
|
|
67
|
+
compression_ratio_target: float = 0.3 # Target 70% reduction
|
|
68
|
+
ccr_enabled: bool = True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CompressionResult:
|
|
73
|
+
"""Result from compression.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
compressed: The compressed content.
|
|
77
|
+
original: The original content (for reference).
|
|
78
|
+
compression_ratio: compressed_length / original_length.
|
|
79
|
+
tokens_before: Estimated token count before compression.
|
|
80
|
+
tokens_after: Estimated token count after compression.
|
|
81
|
+
content_type: Detected content type.
|
|
82
|
+
detection_confidence: Confidence of content type detection.
|
|
83
|
+
handler_used: Name of structure handler used.
|
|
84
|
+
preservation_ratio: Fraction of content marked as structural.
|
|
85
|
+
ccr_key: CCR storage key (if CCR enabled).
|
|
86
|
+
metadata: Additional metadata.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
compressed: str
|
|
90
|
+
original: str
|
|
91
|
+
compression_ratio: float
|
|
92
|
+
tokens_before: int
|
|
93
|
+
tokens_after: int
|
|
94
|
+
content_type: ContentType
|
|
95
|
+
detection_confidence: float
|
|
96
|
+
handler_used: str
|
|
97
|
+
preservation_ratio: float
|
|
98
|
+
ccr_key: str | None = None
|
|
99
|
+
metadata: dict = field(default_factory=dict)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def tokens_saved(self) -> int:
|
|
103
|
+
"""Number of tokens saved."""
|
|
104
|
+
return max(0, self.tokens_before - self.tokens_after)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def savings_percentage(self) -> float:
|
|
108
|
+
"""Percentage of tokens saved."""
|
|
109
|
+
if self.tokens_before == 0:
|
|
110
|
+
return 0.0
|
|
111
|
+
return (self.tokens_saved / self.tokens_before) * 100
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class UniversalCompressor:
|
|
115
|
+
"""Universal compressor with ML detection and structure preservation.
|
|
116
|
+
|
|
117
|
+
This compressor automatically:
|
|
118
|
+
1. Detects content type (JSON, code, logs, text) using ML
|
|
119
|
+
2. Extracts structure (keys, signatures, templates)
|
|
120
|
+
3. Preserves structure while compressing content
|
|
121
|
+
4. Stores original for CCR retrieval
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> compressor = UniversalCompressor()
|
|
125
|
+
>>> result = compressor.compress('{"users": [{"id": 1, "name": "Alice"}]}')
|
|
126
|
+
>>> print(result.content_type) # ContentType.JSON
|
|
127
|
+
>>> print(result.compressed) # Structure preserved, values compressed
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
config: UniversalCompressorConfig | None = None,
|
|
133
|
+
handlers: dict[ContentType, StructureHandler] | None = None,
|
|
134
|
+
compress_fn: Callable[[str], str] | None = None,
|
|
135
|
+
):
|
|
136
|
+
"""Initialize the compressor.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config: Compression configuration.
|
|
140
|
+
handlers: Custom handlers for content types.
|
|
141
|
+
compress_fn: Custom compression function. If None, uses
|
|
142
|
+
LLMLingua when available, else simple truncation.
|
|
143
|
+
"""
|
|
144
|
+
self.config = config or UniversalCompressorConfig()
|
|
145
|
+
|
|
146
|
+
# Initialize detector
|
|
147
|
+
if self.config.use_magika:
|
|
148
|
+
self._detector = get_detector(prefer_magika=True)
|
|
149
|
+
else:
|
|
150
|
+
self._detector = FallbackDetector()
|
|
151
|
+
|
|
152
|
+
# Initialize handlers
|
|
153
|
+
self._handlers: dict[ContentType, StructureHandler] = handlers or {
|
|
154
|
+
ContentType.JSON: JSONStructureHandler(),
|
|
155
|
+
ContentType.CODE: CodeStructureHandler(),
|
|
156
|
+
}
|
|
157
|
+
self._noop_handler = NoOpHandler()
|
|
158
|
+
|
|
159
|
+
# Initialize compression function
|
|
160
|
+
self._compress_fn = compress_fn or self._get_default_compress_fn()
|
|
161
|
+
|
|
162
|
+
# CCR store (lazy initialized)
|
|
163
|
+
self._ccr_store: Any | None = None
|
|
164
|
+
|
|
165
|
+
def _get_default_compress_fn(self) -> Callable[[str], str]:
|
|
166
|
+
"""Get default compression function.
|
|
167
|
+
|
|
168
|
+
Returns LLMLingua wrapper if available, else simple truncation.
|
|
169
|
+
"""
|
|
170
|
+
if self.config.use_llmlingua:
|
|
171
|
+
try:
|
|
172
|
+
return self._llmlingua_compress
|
|
173
|
+
except ImportError:
|
|
174
|
+
logger.info("LLMLingua not available, using simple compression")
|
|
175
|
+
|
|
176
|
+
return self._simple_compress
|
|
177
|
+
|
|
178
|
+
def _llmlingua_compress(self, text: str) -> str:
|
|
179
|
+
"""Compress using LLMLingua.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
text: Text to compress.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Compressed text.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
from headroom.transforms.llmlingua_compressor import compress_with_llmlingua
|
|
189
|
+
|
|
190
|
+
return compress_with_llmlingua(
|
|
191
|
+
text,
|
|
192
|
+
compression_rate=self.config.compression_ratio_target,
|
|
193
|
+
)
|
|
194
|
+
except ImportError:
|
|
195
|
+
return self._simple_compress(text)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning("LLMLingua compression failed: %s", e)
|
|
198
|
+
return self._simple_compress(text)
|
|
199
|
+
|
|
200
|
+
def _simple_compress(self, text: str) -> str:
|
|
201
|
+
"""Simple compression fallback (truncation with indicator).
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
text: Text to compress.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Truncated text with indicator.
|
|
208
|
+
"""
|
|
209
|
+
target_len = int(len(text) * self.config.compression_ratio_target)
|
|
210
|
+
if len(text) <= target_len:
|
|
211
|
+
return text
|
|
212
|
+
|
|
213
|
+
# Keep first and last portions
|
|
214
|
+
keep_start = target_len * 2 // 3
|
|
215
|
+
keep_end = target_len // 3
|
|
216
|
+
|
|
217
|
+
return text[:keep_start] + "\n...[compressed]...\n" + text[-keep_end:]
|
|
218
|
+
|
|
219
|
+
def compress(
|
|
220
|
+
self,
|
|
221
|
+
content: str,
|
|
222
|
+
content_type: ContentType | None = None,
|
|
223
|
+
**kwargs: Any,
|
|
224
|
+
) -> CompressionResult:
|
|
225
|
+
"""Compress content with structure preservation.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
content: Content to compress.
|
|
229
|
+
content_type: Override content type detection.
|
|
230
|
+
**kwargs: Handler-specific options.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
CompressionResult with compressed content and metadata.
|
|
234
|
+
"""
|
|
235
|
+
# Handle empty/short content
|
|
236
|
+
if not content or len(content) < self.config.min_content_length:
|
|
237
|
+
return CompressionResult(
|
|
238
|
+
compressed=content,
|
|
239
|
+
original=content,
|
|
240
|
+
compression_ratio=1.0,
|
|
241
|
+
tokens_before=self._estimate_tokens(content),
|
|
242
|
+
tokens_after=self._estimate_tokens(content),
|
|
243
|
+
content_type=ContentType.UNKNOWN,
|
|
244
|
+
detection_confidence=0.0,
|
|
245
|
+
handler_used="none",
|
|
246
|
+
preservation_ratio=1.0,
|
|
247
|
+
metadata={"skipped": "content too short"},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Detect content type
|
|
251
|
+
if content_type is None:
|
|
252
|
+
detection = self._detector.detect(content)
|
|
253
|
+
else:
|
|
254
|
+
detection = DetectionResult(
|
|
255
|
+
content_type=content_type,
|
|
256
|
+
confidence=1.0,
|
|
257
|
+
raw_label="override",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Get handler for content type
|
|
261
|
+
handler = self._handlers.get(detection.content_type, self._noop_handler)
|
|
262
|
+
|
|
263
|
+
# Tokenize content (character-level for masks)
|
|
264
|
+
tokens = list(content)
|
|
265
|
+
|
|
266
|
+
# Get structure mask from handler
|
|
267
|
+
handler_result = handler.get_mask(content, tokens, **kwargs)
|
|
268
|
+
structure_mask = handler_result.mask
|
|
269
|
+
|
|
270
|
+
# Optionally add entropy-based preservation
|
|
271
|
+
if self.config.use_entropy_preservation:
|
|
272
|
+
entropy_mask = compute_entropy_mask(
|
|
273
|
+
tokens,
|
|
274
|
+
threshold=self.config.entropy_threshold,
|
|
275
|
+
)
|
|
276
|
+
# Union: preserve if either mask says preserve
|
|
277
|
+
structure_mask = structure_mask.union(entropy_mask)
|
|
278
|
+
|
|
279
|
+
# Apply compression to non-structural parts
|
|
280
|
+
compressed = self._compress_with_mask(content, structure_mask)
|
|
281
|
+
|
|
282
|
+
# Estimate tokens
|
|
283
|
+
tokens_before = self._estimate_tokens(content)
|
|
284
|
+
tokens_after = self._estimate_tokens(compressed)
|
|
285
|
+
|
|
286
|
+
# Store in CCR if enabled
|
|
287
|
+
ccr_key = None
|
|
288
|
+
if self.config.ccr_enabled:
|
|
289
|
+
ccr_key = self._store_in_ccr(content, compressed)
|
|
290
|
+
|
|
291
|
+
return CompressionResult(
|
|
292
|
+
compressed=compressed,
|
|
293
|
+
original=content,
|
|
294
|
+
compression_ratio=len(compressed) / len(content) if content else 1.0,
|
|
295
|
+
tokens_before=tokens_before,
|
|
296
|
+
tokens_after=tokens_after,
|
|
297
|
+
content_type=detection.content_type,
|
|
298
|
+
detection_confidence=detection.confidence,
|
|
299
|
+
handler_used=handler_result.handler_name,
|
|
300
|
+
preservation_ratio=structure_mask.preservation_ratio,
|
|
301
|
+
ccr_key=ccr_key,
|
|
302
|
+
metadata={
|
|
303
|
+
"detection": {
|
|
304
|
+
"raw_label": detection.raw_label,
|
|
305
|
+
"language": detection.language,
|
|
306
|
+
},
|
|
307
|
+
"handler": handler_result.metadata,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _compress_with_mask(self, content: str, mask: StructureMask) -> str:
|
|
312
|
+
"""Apply compression respecting structure mask.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
content: Original content.
|
|
316
|
+
mask: Structure mask.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Compressed content with structure preserved.
|
|
320
|
+
"""
|
|
321
|
+
spans = mask_to_spans(mask)
|
|
322
|
+
result_parts: list[str] = []
|
|
323
|
+
|
|
324
|
+
for span in spans:
|
|
325
|
+
span_content = content[span.start : span.end]
|
|
326
|
+
|
|
327
|
+
if span.is_structural:
|
|
328
|
+
# Preserve structural content
|
|
329
|
+
result_parts.append(span_content)
|
|
330
|
+
else:
|
|
331
|
+
# Compress non-structural content
|
|
332
|
+
if len(span_content) > 50: # Only compress if substantial
|
|
333
|
+
compressed = self._compress_fn(span_content)
|
|
334
|
+
result_parts.append(compressed)
|
|
335
|
+
else:
|
|
336
|
+
result_parts.append(span_content)
|
|
337
|
+
|
|
338
|
+
return "".join(result_parts)
|
|
339
|
+
|
|
340
|
+
def _estimate_tokens(self, text: str) -> int:
|
|
341
|
+
"""Estimate token count.
|
|
342
|
+
|
|
343
|
+
Uses simple heuristic: ~4 characters per token.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
text: Text to estimate.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Estimated token count.
|
|
350
|
+
"""
|
|
351
|
+
if not text:
|
|
352
|
+
return 0
|
|
353
|
+
# Simple estimation: ~4 chars per token on average
|
|
354
|
+
return len(text) // 4
|
|
355
|
+
|
|
356
|
+
def _store_in_ccr(self, original: str, compressed: str) -> str | None:
|
|
357
|
+
"""Store original in CCR for retrieval.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
original: Original content.
|
|
361
|
+
compressed: Compressed content.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
CCR key if stored, None otherwise.
|
|
365
|
+
"""
|
|
366
|
+
try:
|
|
367
|
+
if self._ccr_store is None:
|
|
368
|
+
from headroom.cache.compression_store import CompressionStore
|
|
369
|
+
|
|
370
|
+
self._ccr_store = CompressionStore()
|
|
371
|
+
|
|
372
|
+
key = self._ccr_store.store(
|
|
373
|
+
original,
|
|
374
|
+
compressed,
|
|
375
|
+
original_tokens=self._estimate_tokens(original),
|
|
376
|
+
compressed_tokens=self._estimate_tokens(compressed),
|
|
377
|
+
)
|
|
378
|
+
return key
|
|
379
|
+
except ImportError:
|
|
380
|
+
logger.debug("CCR store not available")
|
|
381
|
+
return None
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.warning("Failed to store in CCR: %s", e)
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
def compress_batch(
|
|
387
|
+
self,
|
|
388
|
+
contents: list[str],
|
|
389
|
+
**kwargs: Any,
|
|
390
|
+
) -> list[CompressionResult]:
|
|
391
|
+
"""Compress multiple contents.
|
|
392
|
+
|
|
393
|
+
More efficient than calling compress() in a loop for
|
|
394
|
+
ML detection.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
contents: List of contents to compress.
|
|
398
|
+
**kwargs: Handler-specific options.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
List of CompressionResults.
|
|
402
|
+
"""
|
|
403
|
+
if not contents:
|
|
404
|
+
return []
|
|
405
|
+
|
|
406
|
+
# Batch detection
|
|
407
|
+
if hasattr(self._detector, "detect_batch"):
|
|
408
|
+
detections = self._detector.detect_batch(contents)
|
|
409
|
+
else:
|
|
410
|
+
detections = [self._detector.detect(c) for c in contents]
|
|
411
|
+
|
|
412
|
+
# Compress each with detected type
|
|
413
|
+
results = []
|
|
414
|
+
for content, detection in zip(contents, detections):
|
|
415
|
+
result = self.compress(
|
|
416
|
+
content,
|
|
417
|
+
content_type=detection.content_type,
|
|
418
|
+
**kwargs,
|
|
419
|
+
)
|
|
420
|
+
results.append(result)
|
|
421
|
+
|
|
422
|
+
return results
|
|
423
|
+
|
|
424
|
+
def get_handler(self, content_type: ContentType) -> StructureHandler:
|
|
425
|
+
"""Get handler for content type.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
content_type: Content type.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Handler for the content type.
|
|
432
|
+
"""
|
|
433
|
+
return self._handlers.get(content_type, self._noop_handler)
|
|
434
|
+
|
|
435
|
+
def register_handler(
|
|
436
|
+
self,
|
|
437
|
+
content_type: ContentType,
|
|
438
|
+
handler: StructureHandler,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Register a custom handler for a content type.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
content_type: Content type to handle.
|
|
444
|
+
handler: Handler instance.
|
|
445
|
+
"""
|
|
446
|
+
self._handlers[content_type] = handler
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def compress(content: str, **kwargs: Any) -> CompressionResult:
|
|
450
|
+
"""Convenience function for one-off compression.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
content: Content to compress.
|
|
454
|
+
**kwargs: Passed to UniversalCompressor.compress().
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
CompressionResult.
|
|
458
|
+
|
|
459
|
+
Example:
|
|
460
|
+
>>> from headroom.compression import compress
|
|
461
|
+
>>> result = compress('{"users": [{"id": 1}, {"id": 2}]}')
|
|
462
|
+
>>> print(result.compressed)
|
|
463
|
+
"""
|
|
464
|
+
compressor = UniversalCompressor()
|
|
465
|
+
return compressor.compress(content, **kwargs)
|