cureta 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. cureta/__init__.py +136 -0
  2. cureta/base_tagger.py +270 -0
  3. cureta/cli.py +816 -0
  4. cureta/config.py +632 -0
  5. cureta/data_sources.py +417 -0
  6. cureta/document.py +135 -0
  7. cureta/external_taggers/__init__.py +22 -0
  8. cureta/external_taggers/base.py +88 -0
  9. cureta/external_taggers/datatrove.py +333 -0
  10. cureta/external_taggers/datatrove_fused.py +143 -0
  11. cureta/external_taggers/nemo.py +218 -0
  12. cureta/heuristic_taggers/c4_quality_tagger.py +85 -0
  13. cureta/heuristic_taggers/compression_ratio_tagger.py +37 -0
  14. cureta/heuristic_taggers/content_type_tagger.py +74 -0
  15. cureta/heuristic_taggers/date_mention_tagger.py +64 -0
  16. cureta/heuristic_taggers/exact_norm_hash_tagger.py +48 -0
  17. cureta/heuristic_taggers/fineweb_quality_tagger.py +73 -0
  18. cureta/heuristic_taggers/gopher_quality_tagger.py +95 -0
  19. cureta/heuristic_taggers/heuristic_taggers.py +285 -0
  20. cureta/heuristic_taggers/html_density_tagger.py +47 -0
  21. cureta/heuristic_taggers/markdown_structure_tagger.py +57 -0
  22. cureta/heuristic_taggers/minhash_tagger.py +58 -0
  23. cureta/heuristic_taggers/paragraph_stats_tagger.py +39 -0
  24. cureta/heuristic_taggers/pii_regex_tagger.py +57 -0
  25. cureta/heuristic_taggers/readability_tagger.py +57 -0
  26. cureta/heuristic_taggers/setu/constants.py +282 -0
  27. cureta/heuristic_taggers/setu/setu_filters.py +631 -0
  28. cureta/heuristic_taggers/simhash_tagger.py +54 -0
  29. cureta/heuristic_taggers/type_token_ratio_tagger.py +58 -0
  30. cureta/heuristic_taggers/url_blocklist_tagger.py +93 -0
  31. cureta/heuristic_taggers/url_parse_tagger.py +53 -0
  32. cureta/heuristic_taggers/utils.py +15 -0
  33. cureta/heuristic_taggers/word_length_stats_tagger.py +38 -0
  34. cureta/infra/__init__.py +15 -0
  35. cureta/infra/distributed_runner.py +183 -0
  36. cureta/infra/engine.py +329 -0
  37. cureta/infra/skypilot_cluster.py +291 -0
  38. cureta/manifest.py +196 -0
  39. cureta/pipeline.py +353 -0
  40. cureta/registry.py +584 -0
  41. cureta/results.py +83 -0
  42. cureta/runner.py +763 -0
  43. cureta/tag_ops.py +96 -0
  44. cureta/taggers/__init__.py +31 -0
  45. cureta/taggers/detoxify_tagger.py +67 -0
  46. cureta/taggers/domain_classifier_tagger.py +91 -0
  47. cureta/taggers/fasttext_quality_tagger.py +77 -0
  48. cureta/taggers/fineweb_edu_tagger.py +103 -0
  49. cureta/taggers/glotlid_tagger.py +85 -0
  50. cureta/taggers/indic_lid_tagger.py +200 -0
  51. cureta/taggers/llm_judge_tagger.py +128 -0
  52. cureta/taggers/mmbert_quality_tagger.py +263 -0
  53. cureta/taggers/paragraph_langid_tagger.py +68 -0
  54. cureta/taggers/perplexity_tagger.py +586 -0
  55. cureta/taggers/presidio_pii_tagger.py +73 -0
  56. cureta/taggers/programming_language_tagger.py +90 -0
  57. cureta/taggers/sentence_embedding_tagger.py +76 -0
  58. cureta-0.1.0.dist-info/METADATA +176 -0
  59. cureta-0.1.0.dist-info/RECORD +61 -0
  60. cureta-0.1.0.dist-info/WHEEL +4 -0
  61. cureta-0.1.0.dist-info/entry_points.txt +2 -0
cureta/__init__.py ADDED
@@ -0,0 +1,136 @@
1
+ """Cureta: Unified Data Curation Framework.
2
+
3
+ .. attribute:: __version__
4
+
5
+ Package version string resolved from installed package metadata
6
+ (e.g. ``"0.1.0"``). Falls back to ``"0.0.0+unknown"`` when the
7
+ package is not installed (e.g. in an editable development checkout
8
+ without ``pip install -e .`` having been run).
9
+
10
+ A pipeline-based library that standardises data processing into a sequence
11
+ of **Taggers** which enrich a standard **Document** object with extracted
12
+ features and metadata.
13
+
14
+ Execution surfaces
15
+ ------------------
16
+ run_pipeline -- Canonical entry point; runs end-to-end, returns PipelineResult.
17
+ iter_pipeline_chunks -- Generator variant; yields ChunkResult after each chunk.
18
+ quick_tag -- REPL one-shot; runs taggers on a single string, returns dict.
19
+ stream_tagged_batches -- Streaming without disk writes; yields pd.DataFrame batches.
20
+
21
+ Result types
22
+ ------------
23
+ PipelineResult -- Structured summary of a completed run.
24
+ ChunkResult -- Per-chunk timing and output path.
25
+
26
+ Config / composition types
27
+ --------------------------
28
+ PipelineConfig -- Pydantic model for pipeline-level configuration.
29
+ PipelineStageConfig -- Pydantic model for a single pipeline stage.
30
+ TaggerConfig -- Pydantic model for per-tagger configuration.
31
+ ClusterConfig -- Pydantic model for SkyPilot cluster configuration.
32
+ ExecutionMode -- Enum for SIMD / SDMI / AUTO execution strategies.
33
+ ExecutionConfig -- Pydantic model for per-resource-type execution mode.
34
+ OutputFormat -- Enum for output file format (parquet / jsonl).
35
+
36
+ Authoring types
37
+ ---------------
38
+ Document -- The canonical data envelope.
39
+ BaseTagger -- Abstract base for all taggers.
40
+ CPUTagger -- Base class for CPU-bound taggers.
41
+ GPUTagger -- Base class for GPU-bound taggers.
42
+
43
+ Composition
44
+ -----------
45
+ Pipeline -- Orchestrator; use Pipeline.from_config() or Pipeline.from_taggers()
46
+ to build pipelines programmatically without YAML.
47
+ """
48
+
49
+ from importlib.metadata import PackageNotFoundError
50
+ from importlib.metadata import version as _pkg_version
51
+
52
+ try:
53
+ __version__: str = _pkg_version("cureta")
54
+ except PackageNotFoundError:
55
+ __version__ = "0.0.0+unknown"
56
+
57
+ from cureta import tag_ops # noqa: F401 - re-exported for convenience
58
+ from cureta.base_tagger import BaseTagger, CPUTagger, GPUTagger
59
+ from cureta.config import (
60
+ ClusterConfig,
61
+ ExecutionConfig,
62
+ ExecutionMode,
63
+ OutputFormat,
64
+ PipelineConfig,
65
+ PipelineStageConfig,
66
+ TaggerConfig,
67
+ )
68
+ from cureta.document import Document
69
+ from cureta.manifest import read_manifest, write_manifest
70
+ from cureta.pipeline import Pipeline
71
+ from cureta.results import ChunkResult, PipelineResult
72
+ from cureta.runner import (
73
+ iter_pipeline_chunks,
74
+ quick_tag,
75
+ run_pipeline,
76
+ stream_tagged_batches,
77
+ )
78
+ __all__ = [
79
+ # Package version
80
+ "__version__",
81
+ # Execution surfaces (canonical)
82
+ "run_pipeline",
83
+ "iter_pipeline_chunks",
84
+ "quick_tag",
85
+ "stream_tagged_batches",
86
+ # Result types
87
+ "PipelineResult",
88
+ "ChunkResult",
89
+ # Config types
90
+ "PipelineConfig",
91
+ "PipelineStageConfig",
92
+ "TaggerConfig",
93
+ "ClusterConfig",
94
+ "ExecutionMode",
95
+ "ExecutionConfig",
96
+ "OutputFormat",
97
+ # Composition
98
+ "Pipeline",
99
+ # Authoring
100
+ "Document",
101
+ "BaseTagger",
102
+ "CPUTagger",
103
+ "GPUTagger",
104
+ # Helper modules
105
+ "tag_ops",
106
+ # Manifest / Provenance
107
+ "write_manifest",
108
+ "read_manifest",
109
+ # Optional / lazy-loaded
110
+ "IndicLIDTagger",
111
+ "DistributedRunner",
112
+ "SkyPilotCluster",
113
+ ]
114
+
115
+
116
+ def __getattr__(name: str): # noqa: ANN202
117
+ """Lazy import for optional-dependency modules.
118
+
119
+ DistributedRunner requires ``ray[data]``. IndicLIDTagger and the other
120
+ GPU taggers require ``torch``/``transformers``. Deferring these imports
121
+ avoids breaking users who have not installed the optional extras.
122
+ """
123
+ if name == "IndicLIDTagger":
124
+ from cureta.taggers.indic_lid_tagger import IndicLIDTagger
125
+
126
+ return IndicLIDTagger
127
+ if name == "DistributedRunner":
128
+ from cureta.infra.distributed_runner import DistributedRunner
129
+
130
+ return DistributedRunner
131
+ if name == "SkyPilotCluster":
132
+ from cureta.infra.skypilot_cluster import SkyPilotCluster
133
+
134
+ return SkyPilotCluster
135
+ msg = f"module {__name__!r} has no attribute {name!r}"
136
+ raise AttributeError(msg)
cureta/base_tagger.py ADDED
@@ -0,0 +1,270 @@
1
+ """Tagger architecture for the Cureta framework.
2
+
3
+ Provides the abstract base class :class:`BaseTagger` and two intermediate
4
+ abstract subclasses -- :class:`CPUTagger` and :class:`GPUTagger` -- that every
5
+ concrete tagger must inherit from.
6
+
7
+ Each tagger exposes:
8
+
9
+ * :meth:`process_document` -- Tier-1 single-document logic (simple API).
10
+ * :meth:`run` -- Tier-2 batch interface for vectorised processing.
11
+
12
+ Implement exactly **one** of the two. The framework calls ``run()``; the
13
+ default ``run()`` delegates to ``process_document()`` row by row.
14
+
15
+ * :attr:`resources` -- ``dict`` with ``num_cpus`` / ``num_gpus``.
16
+ * :attr:`use_actors` -- whether Ray should use ``ActorPoolStrategy``.
17
+
18
+ Hierarchy
19
+ ---------
20
+ ::
21
+
22
+ BaseTagger (ABC)
23
+ |- CPUTagger (ABC) -- taggers that run on CPU only
24
+ |- GPUTagger (ABC) -- taggers that require GPU resources
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import re
31
+ import uuid
32
+ from abc import ABC
33
+ from typing import Any
34
+
35
+ import pandas as pd
36
+
37
+ from cureta.config import ExecutionMode, TaggerConfig
38
+ from cureta.document import Document
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class BaseTagger(ABC):
44
+ """Abstract base class for all feature-extraction taggers.
45
+
46
+ Parameters
47
+ ----------
48
+ config : TaggerConfig
49
+ Validated configuration for this tagger instance.
50
+
51
+ Raises
52
+ ------
53
+ ValueError
54
+ If :meth:`validate_config` determines the configuration is invalid.
55
+
56
+ Notes
57
+ -----
58
+ **Column preservation guarantee:** Any column present in the input
59
+ batch will be present, unchanged, in the output batch. Taggers may
60
+ modify ``document_id``, ``raw_content``, ``metadata``, and ``tags``.
61
+ They must not modify or drop any other column. Tier-2 taggers that
62
+ override :meth:`run` are responsible for upholding this contract.
63
+
64
+ **Two-tier API:** Override :meth:`process_document` for simple per-document
65
+ logic (Tier-1, ~90% of taggers). Override :meth:`run` for vectorised or
66
+ GPU batch operations (Tier-2). The framework dispatches to whichever is
67
+ overridden; if neither is overridden, pipeline construction raises
68
+ :class:`TypeError`.
69
+
70
+ **Preferred execution mode:** subclasses may set the class attribute
71
+ ``preferred_mode`` to hint to the runner which execution strategy works
72
+ best for them. A mismatch between the tagger's preference and the
73
+ pipeline's configured mode triggers a startup warning. ``None`` means
74
+ no preference — either mode is acceptable.
75
+
76
+ """
77
+
78
+ preferred_mode: ExecutionMode | None = None
79
+
80
+ def __init__(
81
+ self,
82
+ config: TaggerConfig | None = None,
83
+ **parameters: Any,
84
+ ) -> None:
85
+ """Initialise the tagger with its config or keyword parameters.
86
+
87
+ When *config* is ``None`` the tagger constructs a :class:`TaggerConfig`
88
+ automatically from the class name and any *parameters* kwargs, enabling
89
+ direct instantiation without YAML::
90
+
91
+ tagger = NumWordsTagger(lang="hi")
92
+ """
93
+ if config is None:
94
+ config = TaggerConfig(
95
+ tagger_name=self._default_name(),
96
+ tagger_class=f"{type(self).__module__}.{type(self).__name__}",
97
+ parameters=parameters,
98
+ )
99
+ self.config = config
100
+ self.validate_config()
101
+ logger.info("Initialised tagger '%s'", config.tagger_name)
102
+
103
+ @classmethod
104
+ def _default_name(cls) -> str:
105
+ """Derive a snake_case registry name from the class name.
106
+
107
+ ``NumWordsTagger`` → ``num_words``, ``CuretaIDTagger`` → ``cureta_id``.
108
+ """
109
+ name = cls.__name__.removesuffix("Tagger")
110
+ # Insert _ between a lowercase letter and the next uppercase letter.
111
+ s1 = re.sub(r"([a-z])([A-Z])", r"\1_\2", name)
112
+ # Insert _ between a run of uppercase letters and a trailing uppercase+lowercase.
113
+ s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
114
+ return s2.lower()
115
+
116
+ # ------------------------------------------------------------------
117
+ # Abstract interface
118
+ # ------------------------------------------------------------------
119
+
120
+ def process_document(self, document: Document) -> Document:
121
+ """Tier-1 API: process a single document.
122
+
123
+ Override this for simple, per-document logic. The framework handles
124
+ batching, column preservation, and tag merging.
125
+
126
+ If you override ``run()`` instead (Tier-2), this method is unused and
127
+ you do not need to implement it.
128
+
129
+ Raises
130
+ ------
131
+ NotImplementedError
132
+ If neither this method nor ``run()`` is overridden.
133
+
134
+ """
135
+ raise NotImplementedError(
136
+ f"Tagger '{self.name}' must override either `process_document` "
137
+ f"(Tier-1) or `run` (Tier-2). See docs/writing_taggers.md."
138
+ )
139
+
140
+ # ------------------------------------------------------------------
141
+ # Batch interface (used by Ray map_batches)
142
+ # ------------------------------------------------------------------
143
+
144
+ def run(self, batch: pd.DataFrame) -> pd.DataFrame:
145
+ """Process a batch of documents as a pandas DataFrame.
146
+
147
+ The default implementation iterates rows, deserialises each into
148
+ a :class:`Document`, calls :meth:`process_document`, and writes
149
+ the enriched fields back. Subclasses may override this for
150
+ vectorised / batch-optimised processing.
151
+
152
+ ``load_model()`` is called lazily on the first invocation so
153
+ that heavy resources are initialised inside Ray workers rather
154
+ than on the driver (which would cause serialisation failures).
155
+
156
+ Extra columns present in the input batch are preserved unchanged
157
+ in the output. Only ``document_id``, ``raw_content``,
158
+ ``metadata``, and ``tags`` may be modified by the tagger.
159
+ """
160
+ self.load_model()
161
+ out_rows: list[dict[str, Any]] = []
162
+ for _, row in batch.iterrows():
163
+ row_dict = row.to_dict()
164
+ doc = Document(
165
+ document_id=row_dict.get("document_id") or str(uuid.uuid4()),
166
+ raw_content=str(row_dict.get("raw_content", "")),
167
+ metadata=dict(row_dict.get("metadata") or {}),
168
+ tags=dict(row_dict.get("tags") or {}),
169
+ )
170
+ enriched = self.process_document(doc)
171
+ row_dict["document_id"] = enriched.document_id
172
+ row_dict["raw_content"] = enriched.raw_content
173
+ row_dict["metadata"] = enriched.metadata
174
+ row_dict["tags"] = enriched.tags
175
+ out_rows.append(row_dict)
176
+ return pd.DataFrame(out_rows)
177
+
178
+ # ------------------------------------------------------------------
179
+ # Config validation
180
+ # ------------------------------------------------------------------
181
+
182
+ def validate_config(self) -> None:
183
+ """Verify that the tagger's parameters are sane.
184
+
185
+ The default implementation performs no checks. Override to
186
+ enforce domain-specific constraints and raise :class:`ValueError`.
187
+ """
188
+ return None
189
+
190
+ # ------------------------------------------------------------------
191
+ # Resource management
192
+ # ------------------------------------------------------------------
193
+
194
+ def load_model(self) -> None:
195
+ """Load heavy model weights into memory.
196
+
197
+ This hook is called by the Ray runner immediately before the tagger
198
+ is used to process a shard (SDMI) or when the actor starts (SIMD).
199
+ Implementations should override this to perform deferred initialization.
200
+ """
201
+ return None
202
+
203
+ def unload_model(self) -> None:
204
+ """Unload model weights and clear memory.
205
+
206
+ This hook is called by the Ray runner in SDMI mode after a shard is
207
+ fully processed by this tagger to ensure memory is cleared before
208
+ loading the next tagger.
209
+ """
210
+ return None
211
+
212
+ # ------------------------------------------------------------------
213
+ # Resource requirements
214
+ # ------------------------------------------------------------------
215
+
216
+ @property
217
+ def resources(self) -> dict[str, int]:
218
+ """Return Ray resource requirements for ``map_batches``.
219
+
220
+ Returns ``{"num_cpus": ..., "num_gpus": ...}``.
221
+ """
222
+ return {"num_cpus": 1, "num_gpus": 0}
223
+
224
+ @property
225
+ def use_actors(self) -> bool:
226
+ """Whether Ray should use ``ActorPoolStrategy`` for this tagger.
227
+
228
+ Defaults to ``False``. GPU taggers override to ``True`` so that
229
+ heavy models stay loaded across batches.
230
+ """
231
+ return False
232
+
233
+ # ------------------------------------------------------------------
234
+ # Utilities
235
+ # ------------------------------------------------------------------
236
+
237
+ @property
238
+ def name(self) -> str:
239
+ """Return the tagger's canonical name from its config."""
240
+ return self.config.tagger_name
241
+
242
+ def __repr__(self) -> str:
243
+ """Return a string representation of the tagger."""
244
+ return f"{self.__class__.__name__}(name={self.name!r})"
245
+
246
+
247
+ class CPUTagger(BaseTagger, ABC):
248
+ """Base for taggers that execute exclusively on CPU."""
249
+
250
+ resource_type: str = "cpu"
251
+
252
+
253
+ class GPUTagger(BaseTagger, ABC):
254
+ """Base for taggers that require GPU resources.
255
+
256
+ Uses ``ActorPoolStrategy`` by default so that large models remain
257
+ loaded in GPU memory across batches.
258
+ """
259
+
260
+ resource_type: str = "gpu"
261
+
262
+ @property
263
+ def resources(self) -> dict[str, int]:
264
+ """Request 1 CPU + 1 GPU."""
265
+ return {"num_cpus": 1, "num_gpus": 1}
266
+
267
+ @property
268
+ def use_actors(self) -> bool:
269
+ """GPU taggers use actors to keep models warm."""
270
+ return True