rule-auto-label 0.1.0__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.
@@ -0,0 +1,30 @@
1
+ # Virtual environments and scratch space used while building. Never committed:
2
+ # they are large, machine-specific, and rebuilt from pyproject.toml anyway.
3
+ _venvs/
4
+ _proof/
5
+ .venv*/
6
+
7
+ # Build output. Wheels are built by the release workflow from this source, so a
8
+ # wheel in git could silently differ from the code beside it.
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ src/*.egg-info/
13
+
14
+ # Python noise
15
+ __pycache__/
16
+ *.py[cod]
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+
21
+ # Local verification state, not source
22
+ verify_results.json
23
+ publish-log.txt
24
+ published.json
25
+
26
+ # Credentials. None of these belong here, and this line is the backstop.
27
+ .pypirc
28
+ .env
29
+ *.pem
30
+ *.key
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pranay Mahendrakar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: rule-auto-label
3
+ Version: 0.1.0
4
+ Summary: Generate labels for text or tabular data from rules, then extend them with a lightweight ML model and an optional LLM hook
5
+ Project-URL: Homepage, https://pypi.org/project/rule-auto-label/
6
+ Project-URL: Author, https://pypi.org/user/pranaymahendrakar/
7
+ Author: Pranay Mahendrakar
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: labeling,labelling,llm,pandas,rules,scikit-learn,text-classification,weak-supervision
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: numpy>=1.23
20
+ Requires-Dist: pandas>=1.5
21
+ Requires-Dist: scikit-learn>=1.1
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7; extra == 'dev'
24
+ Requires-Dist: readme-renderer[md]>=40; extra == 'dev'
25
+ Provides-Extra: parquet
26
+ Requires-Dist: pyarrow>=12; extra == 'parquet'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # rule-auto-label
30
+
31
+ > Installs as `rule-auto-label`; imports as `auto_label`. PyPI blocks the plain
32
+ > name `auto-label` as too close to the existing `autolabel` and `auto-labeler`.
33
+
34
+ Turn a few keyword, regex or query rules into labels for a whole text list or DataFrame: rules label what they can, a small model trained on those labels covers the rest, and an optional LLM callable takes the items nobody was sure about.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install rule-auto-label
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ ```python
45
+ import auto_label
46
+
47
+ texts = ["Free prize, click now to claim it", "Team meeting at 10 in room B", "Win a free voucher today",
48
+ "Agenda for the meeting is attached", "Claim your prize before midnight", "Meeting notes from the team",
49
+ "Click now and claim your voucher", "Team notes from room B attached"]
50
+ result = auto_label.label(texts, {"spam": ["free", "prize", "win"], "work": ["meeting", "agenda"]})
51
+ print(result.summary())
52
+ print(result.to_frame())
53
+ ```
54
+
55
+ The last two items match no rule; the model trained on the first six labels them `spam` and `work`.
56
+
57
+ ## What it does
58
+
59
+ - **Rules first.** `keywords` (case-insensitive whole words or phrases), `regex`, a pandas `query`
60
+ string for DataFrames, or any `func(item) -> bool`. A rule fires when every condition it carries holds.
61
+ - **Conflicts resolve by weight.** Each firing rule adds its `weight` to its label; the label with the
62
+ largest total wins, ties go to the rule added first. The rule confidence is the share of the winning
63
+ weight (1.0 when only one label fired). A negative weight vetoes.
64
+ - **A small model extends the rules.** TF-IDF + logistic regression for text; for a table, each column
65
+ is read for what it holds — free-text columns go through TF-IDF, genuine categories are one-hot
66
+ encoded, numbers are scaled — and `.notes` says which column was read as what. Columns that cannot
67
+ teach the model anything are left out and named there too: a near-unique column of whole numbers in
68
+ order is a row id rather than a measurement (an export sorted by type would otherwise let the id
69
+ outvote the text), and a column that is empty for every rule-labeled row has nothing to learn from.
70
+ Either way the model
71
+ trains on the rule-labeled items and is applied to the rest, with the predicted probability as
72
+ confidence. Training is skipped when fewer than 2 labels or fewer than 5 rule-labeled examples
73
+ exist, or when nothing is left to predict.
74
+ - **An optional LLM hook finishes the job.** Items still under `min_confidence` are passed to
75
+ `llm(text, candidate_labels)`; a returned label is used with source `"llm"`. If the callable raises,
76
+ the error is logged and the item stays as it was.
77
+ - **Anything else is left `None`.** `coverage` tells you how much was labeled.
78
+ - Text input: `list[str]`, a pandas `Series`, or a 1-D array. Tabular input: a `DataFrame`, a dict of
79
+ columns, or a path to `.csv` / `.parquet` (`pip install rule-auto-label[parquet]`). On tables, keyword and
80
+ regex rules see the string-like columns of each row joined by spaces; `func` gets the row as a
81
+ Series, whose `.name` is that row's label in your index.
82
+ - Deterministic (`random_state`), no network, no model downloads, logs through `logging`.
83
+
84
+ ## API
85
+
86
+ ```python
87
+ auto_label.label(data, rules, *, labels=None, min_confidence=0.6, random_state=0, model=True, llm=None) -> LabelResult
88
+ ```
89
+
90
+ `rules` maps a label to a list of keywords, a regex string, a callable, a dict of `add_rule` keyword
91
+ arguments (`{"keywords": [...], "regex": ..., "query": ..., "func": ..., "weight": ...}`), or a list of
92
+ such dicts.
93
+
94
+ ```python
95
+ labeler = auto_label.Labeler(labels=None, min_confidence=0.6, random_state=0)
96
+ labeler.add_rule(label, *, keywords=None, regex=None, query=None, func=None, weight=1.0) # returns self
97
+ result = labeler.label(data, *, model=True, llm=None)
98
+ labeler.model_ # the fitted scikit-learn pipeline from the last call, or None
99
+ ```
100
+
101
+ `labels` (optional) is a closed set: rules must use one of them and LLM answers outside it are ignored.
102
+
103
+ `LabelResult`
104
+
105
+ | attribute / method | meaning |
106
+ | --- | --- |
107
+ | `.labels` | `list[str \| None]`, one per item |
108
+ | `.confidence` | `list[float]`; rule share, model probability, 1.0 for the LLM, 0.0 when unlabeled |
109
+ | `.source` | `list["rule" \| "model" \| "llm" \| None]` |
110
+ | `.coverage` | fraction of items that got a label |
111
+ | `.items`, `.index` | the original items (text, or a row dict) and the input index |
112
+ | `.counts`, `.source_counts` | items per label, items per source |
113
+ | `.to_frame()` | DataFrame `item, label, confidence, source` on the input index |
114
+ | `.to_dict()` | JSON-safe dict with totals, counts, notes and every record |
115
+ | `.summary()` | a few lines of human-readable text |
116
+ | `.model_trained`, `.notes` | whether the model ran, and what each stage did |
117
+
118
+ ## CLI
119
+
120
+ ```bash
121
+ auto-label tickets.csv --column text --rule billing=invoice,refund --rule bug=crash,error
122
+ auto-label lines.txt --regex urgent="(?i)asap|urgent" --json
123
+ auto-label orders.csv --query big="amount > 100" --rules more_rules.json --output labeled.csv
124
+ ```
125
+
126
+ `INPUT` is a `.csv` / `.parquet` file (tabular, or one text column with `--column`), a text file with
127
+ one item per line, or `-` for stdin. Repeat `--rule LABEL=kw1,kw2`, `--regex LABEL=PATTERN` and
128
+ `--query LABEL=EXPR` as needed, or put the same rules in a JSON file for `--rules`. By default the
129
+ summary is printed; `--json` prints `to_dict()`; `--output PATH` writes the labeled table as `.csv`
130
+ or `.parquet` (`pip install rule-auto-label[parquet]`), or the full result as `.json`. For tabular input
131
+ the written table is the input columns plus `label, confidence, source`, so it joins back to the
132
+ source file; if the input already uses one of those names, or `index`, the written column gets a free
133
+ one (`label_2`, `index_2`) so every header stays unique. The destination is replaced only once the
134
+ whole file has been written, so a write that fails leaves the file already at that path intact.
135
+
136
+ `--encoding` sets the encoding of the input, whether that is a CSV, a text file or piped stdin.
137
+ Without it everything is read as UTF-8 and undecodable bytes are replaced, with a warning both logged
138
+ and recorded in `.notes`. `--no-model`, `--min-confidence`, `--labels` and `--random-state` mirror the
139
+ Python API.
140
+
141
+ A text file is read one item per line; blank lines are skipped and each record keeps its 0-based
142
+ line number in the file as its index, so results map back to the source. `-` reads stdin the same
143
+ way, and decodes it as UTF-8 whatever the console's locale encoding happens to be, so the same bytes
144
+ give the same labels piped in as they do saved to a file.
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,120 @@
1
+ # rule-auto-label
2
+
3
+ > Installs as `rule-auto-label`; imports as `auto_label`. PyPI blocks the plain
4
+ > name `auto-label` as too close to the existing `autolabel` and `auto-labeler`.
5
+
6
+ Turn a few keyword, regex or query rules into labels for a whole text list or DataFrame: rules label what they can, a small model trained on those labels covers the rest, and an optional LLM callable takes the items nobody was sure about.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install rule-auto-label
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ import auto_label
18
+
19
+ texts = ["Free prize, click now to claim it", "Team meeting at 10 in room B", "Win a free voucher today",
20
+ "Agenda for the meeting is attached", "Claim your prize before midnight", "Meeting notes from the team",
21
+ "Click now and claim your voucher", "Team notes from room B attached"]
22
+ result = auto_label.label(texts, {"spam": ["free", "prize", "win"], "work": ["meeting", "agenda"]})
23
+ print(result.summary())
24
+ print(result.to_frame())
25
+ ```
26
+
27
+ The last two items match no rule; the model trained on the first six labels them `spam` and `work`.
28
+
29
+ ## What it does
30
+
31
+ - **Rules first.** `keywords` (case-insensitive whole words or phrases), `regex`, a pandas `query`
32
+ string for DataFrames, or any `func(item) -> bool`. A rule fires when every condition it carries holds.
33
+ - **Conflicts resolve by weight.** Each firing rule adds its `weight` to its label; the label with the
34
+ largest total wins, ties go to the rule added first. The rule confidence is the share of the winning
35
+ weight (1.0 when only one label fired). A negative weight vetoes.
36
+ - **A small model extends the rules.** TF-IDF + logistic regression for text; for a table, each column
37
+ is read for what it holds — free-text columns go through TF-IDF, genuine categories are one-hot
38
+ encoded, numbers are scaled — and `.notes` says which column was read as what. Columns that cannot
39
+ teach the model anything are left out and named there too: a near-unique column of whole numbers in
40
+ order is a row id rather than a measurement (an export sorted by type would otherwise let the id
41
+ outvote the text), and a column that is empty for every rule-labeled row has nothing to learn from.
42
+ Either way the model
43
+ trains on the rule-labeled items and is applied to the rest, with the predicted probability as
44
+ confidence. Training is skipped when fewer than 2 labels or fewer than 5 rule-labeled examples
45
+ exist, or when nothing is left to predict.
46
+ - **An optional LLM hook finishes the job.** Items still under `min_confidence` are passed to
47
+ `llm(text, candidate_labels)`; a returned label is used with source `"llm"`. If the callable raises,
48
+ the error is logged and the item stays as it was.
49
+ - **Anything else is left `None`.** `coverage` tells you how much was labeled.
50
+ - Text input: `list[str]`, a pandas `Series`, or a 1-D array. Tabular input: a `DataFrame`, a dict of
51
+ columns, or a path to `.csv` / `.parquet` (`pip install rule-auto-label[parquet]`). On tables, keyword and
52
+ regex rules see the string-like columns of each row joined by spaces; `func` gets the row as a
53
+ Series, whose `.name` is that row's label in your index.
54
+ - Deterministic (`random_state`), no network, no model downloads, logs through `logging`.
55
+
56
+ ## API
57
+
58
+ ```python
59
+ auto_label.label(data, rules, *, labels=None, min_confidence=0.6, random_state=0, model=True, llm=None) -> LabelResult
60
+ ```
61
+
62
+ `rules` maps a label to a list of keywords, a regex string, a callable, a dict of `add_rule` keyword
63
+ arguments (`{"keywords": [...], "regex": ..., "query": ..., "func": ..., "weight": ...}`), or a list of
64
+ such dicts.
65
+
66
+ ```python
67
+ labeler = auto_label.Labeler(labels=None, min_confidence=0.6, random_state=0)
68
+ labeler.add_rule(label, *, keywords=None, regex=None, query=None, func=None, weight=1.0) # returns self
69
+ result = labeler.label(data, *, model=True, llm=None)
70
+ labeler.model_ # the fitted scikit-learn pipeline from the last call, or None
71
+ ```
72
+
73
+ `labels` (optional) is a closed set: rules must use one of them and LLM answers outside it are ignored.
74
+
75
+ `LabelResult`
76
+
77
+ | attribute / method | meaning |
78
+ | --- | --- |
79
+ | `.labels` | `list[str \| None]`, one per item |
80
+ | `.confidence` | `list[float]`; rule share, model probability, 1.0 for the LLM, 0.0 when unlabeled |
81
+ | `.source` | `list["rule" \| "model" \| "llm" \| None]` |
82
+ | `.coverage` | fraction of items that got a label |
83
+ | `.items`, `.index` | the original items (text, or a row dict) and the input index |
84
+ | `.counts`, `.source_counts` | items per label, items per source |
85
+ | `.to_frame()` | DataFrame `item, label, confidence, source` on the input index |
86
+ | `.to_dict()` | JSON-safe dict with totals, counts, notes and every record |
87
+ | `.summary()` | a few lines of human-readable text |
88
+ | `.model_trained`, `.notes` | whether the model ran, and what each stage did |
89
+
90
+ ## CLI
91
+
92
+ ```bash
93
+ auto-label tickets.csv --column text --rule billing=invoice,refund --rule bug=crash,error
94
+ auto-label lines.txt --regex urgent="(?i)asap|urgent" --json
95
+ auto-label orders.csv --query big="amount > 100" --rules more_rules.json --output labeled.csv
96
+ ```
97
+
98
+ `INPUT` is a `.csv` / `.parquet` file (tabular, or one text column with `--column`), a text file with
99
+ one item per line, or `-` for stdin. Repeat `--rule LABEL=kw1,kw2`, `--regex LABEL=PATTERN` and
100
+ `--query LABEL=EXPR` as needed, or put the same rules in a JSON file for `--rules`. By default the
101
+ summary is printed; `--json` prints `to_dict()`; `--output PATH` writes the labeled table as `.csv`
102
+ or `.parquet` (`pip install rule-auto-label[parquet]`), or the full result as `.json`. For tabular input
103
+ the written table is the input columns plus `label, confidence, source`, so it joins back to the
104
+ source file; if the input already uses one of those names, or `index`, the written column gets a free
105
+ one (`label_2`, `index_2`) so every header stays unique. The destination is replaced only once the
106
+ whole file has been written, so a write that fails leaves the file already at that path intact.
107
+
108
+ `--encoding` sets the encoding of the input, whether that is a CSV, a text file or piped stdin.
109
+ Without it everything is read as UTF-8 and undecodable bytes are replaced, with a warning both logged
110
+ and recorded in `.notes`. `--no-model`, `--min-confidence`, `--labels` and `--random-state` mirror the
111
+ Python API.
112
+
113
+ A text file is read one item per line; blank lines are skipped and each record keeps its 0-based
114
+ line number in the file as its index, so results map back to the source. `-` reads stdin the same
115
+ way, and decodes it as UTF-8 whatever the console's locale encoding happens to be, so the same bytes
116
+ give the same labels piped in as they do saved to a file.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rule-auto-label"
7
+ version = "0.1.0"
8
+ description = "Generate labels for text or tabular data from rules, then extend them with a lightweight ML model and an optional LLM hook"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Pranay Mahendrakar" }]
14
+ keywords = ["labeling", "labelling", "weak-supervision", "text-classification", "rules", "pandas", "scikit-learn", "llm"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "pandas>=1.5",
26
+ "numpy>=1.23",
27
+ "scikit-learn>=1.1",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ # heavy or niche deps go here, never in `dependencies`
32
+ parquet = ["pyarrow>=12"]
33
+ # readme_renderer[md] is what PyPI renders the long description with: the test suite
34
+ # uses it to check the README tables survive the trip.
35
+ dev = ["pytest>=7", "readme_renderer[md]>=40"]
36
+
37
+ [project.scripts]
38
+ rule-auto-label = "auto_label.cli:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://pypi.org/project/rule-auto-label/"
42
+ Author = "https://pypi.org/user/pranaymahendrakar/"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/auto_label"]
@@ -0,0 +1,17 @@
1
+ """auto-label: label text or tabular data from rules, a small model, and an optional LLM hook.
2
+
3
+ Quick use::
4
+
5
+ import auto_label
6
+ result = auto_label.label(texts, {"spam": ["free", "prize"], "work": ["meeting"]})
7
+ result.to_frame()
8
+
9
+ For control, build a :class:`Labeler`, add rules, then call ``.label(data)``.
10
+ """
11
+
12
+ from auto_label.labeler import Labeler, LabelResult, label
13
+ from auto_label.rules import Rule
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = ["Labeler", "LabelResult", "Rule", "label", "__version__"]
@@ -0,0 +1,5 @@
1
+ """Allow ``python -m auto_label lines.txt --rule spam=free``."""
2
+
3
+ from auto_label.cli import main
4
+
5
+ raise SystemExit(main())