taghound 0.2.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.
taghound-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Roman
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,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: taghound
3
+ Version: 0.2.0
4
+ Summary: TagHound: A Python library for managing and evaluating tag rules using scalar and vector operations. Supports YAML and JSON rule loading.
5
+ Keywords: tags,rules,YAML,JSON,Python
6
+ Author: Roman Zagrebnev
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: pyyaml>=6.0.2
16
+ Requires-Dist: pandas>=2.2.3,<3
17
+ Requires-Python: >=3.11
18
+ Project-URL: Homepage, https://github.com/rzagreb/TagHound
19
+ Project-URL: Repository, https://github.com/rzagreb/TagHound
20
+ Project-URL: Documentation, https://github.com/rzagreb/TagHound#readme
21
+ Project-URL: Changelog, https://github.com/rzagreb/TagHound/blob/main/CHANGELOG.md
22
+ Description-Content-Type: text/markdown
23
+
24
+ # TagHound
25
+
26
+ Declarative tagging for Python: write matching rules in YAML or JSON, and TagHound attaches tags to your dicts or whole pandas DataFrames.
27
+
28
+ Rules stay readable and editable by non-technical users, while your pipeline code stays a one-liner. Typical uses:
29
+
30
+ - **Categorize bank transactions** — regex rules on merchant strings turn a CSV export into budget categories, no ML training required
31
+ - **Triage tickets and log events** — keyword and threshold rules attach routing tags (severity, team, topic) to each incoming record
32
+ - **Enrich scraped datasets** — bulk-tag job postings or product listings in a pandas pipeline, then rank matches by rule weights
33
+
34
+ ## Installation
35
+
36
+ Requires Python 3.11+.
37
+
38
+ ```bash
39
+ pip install taghound
40
+ ```
41
+
42
+ Latest from source: `pip install git+https://github.com/rzagreb/TagHound.git`
43
+
44
+ ## Quick start
45
+
46
+ Create `rules.yml`:
47
+
48
+ ```yaml
49
+ - id: food/coffee
50
+ label: Coffee
51
+ weight: 3
52
+ and:
53
+ - key: merchant
54
+ op: "~"
55
+ value: starbucks|blue bottle
56
+
57
+ - id: alerts/big-purchase
58
+ and:
59
+ - key: amount
60
+ op: ">"
61
+ value: 100
62
+ ```
63
+
64
+ Then:
65
+
66
+ ```python
67
+ from taghound import TagHound
68
+
69
+ th = TagHound.rules_from_yaml("rules.yml")
70
+
71
+ print(th.find_all_tags({"merchant": "STARBUCKS #1234", "amount": 6.40}))
72
+ # ('food/coffee',)
73
+ print(th.find_all_tags({"merchant": "Delta Airlines", "amount": 420.00}))
74
+ # ('alerts/big-purchase',)
75
+ ```
76
+
77
+ ## Rule format
78
+
79
+ A rule is a unique `id` plus a tree of conditions under `and` / `or`, nested as deep as you need:
80
+
81
+ ```yaml
82
+ - id: inventory/tall-tropical-tree # required, unique; returned as the tag
83
+ label: Tall tropical tree # optional, defaults to id
84
+ weight: 12 # optional score, defaults to 0
85
+ info: internal note, not matched # optional
86
+ and:
87
+ - key: type
88
+ value: tree # no `op` means `=`
89
+ - or:
90
+ - key: height
91
+ op: ">"
92
+ value: 20
93
+ - key: location
94
+ op: "~"
95
+ value: tropical
96
+ ```
97
+
98
+ ### Operators
99
+
100
+ | Op | Meaning | Value types |
101
+ |---|---|---|
102
+ | `=` | equal (default when `op` is omitted) | int, float, str, bool |
103
+ | `!=` | not equal | int, float, str, bool |
104
+ | `>` `<` `>=` `<=` | numeric comparison | int, float |
105
+ | `in` | field value is in the list | list |
106
+ | `not_in` | field value is not in the list | list |
107
+ | `~` | regex match | str or list of str |
108
+ | `!~` | regex does not match | str or list of str |
109
+
110
+ Regex matching is case-insensitive, and a list value is OR-joined (`starbucks|blue bottle`). Patterns are wrapped in `(?<!\w)(?:...)(?!\w)` so they match whole words; pass `merge_pattern=r"{pattern}"` to `rules_from_yaml`/`rules_from_json` for raw substring behavior, or any other wrapper with a `{pattern}` placeholder.
111
+
112
+ Invalid rules (bad regex, unknown operator) raise at load time, not on first evaluation.
113
+
114
+ ## Tagging a DataFrame
115
+
116
+ For large batches, evaluating a whole DataFrame at once is usually faster than calling `find_all_tags` per row (the break-even depends on your rules, so measure):
117
+
118
+ ```python
119
+ import pandas as pd
120
+
121
+ df = pd.DataFrame([
122
+ {"merchant": "Blue Bottle Coffee", "amount": 5.75},
123
+ {"merchant": "Delta Airlines", "amount": 420.00},
124
+ ])
125
+
126
+ print(th.find_tags_using_vector(df))
127
+ # merchant amount tags
128
+ # 0 Blue Bottle Coffee 5.75 [food/coffee]
129
+ # 1 Delta Airlines 420.00 [alerts/big-purchase]
130
+ ```
131
+
132
+ The tags column is added to the input DataFrame in place; rename it with `output_column_name=`. Alternatively, `output_format="columns"` returns a copy with one boolean column per rule (`food/coffee`, `alerts/big-purchase`) — handy for filtering and aggregation.
133
+
134
+ ## Scoring and labeling matches
135
+
136
+ Rule weights and labels are exposed as maps, so ranking matched records is a couple of lines:
137
+
138
+ ```python
139
+ tags = th.find_all_tags({"merchant": "STARBUCKS #1234", "amount": 6.40})
140
+
141
+ score = sum(th.rule_id_to_weight_map[t] for t in tags)
142
+ labels = [th.rule_id_to_label_map[t] for t in tags]
143
+ print(score, labels)
144
+ # 3.0 ['Coffee']
145
+ ```
146
+
147
+ ## Rules in code
148
+
149
+ Skip the files entirely by building rules with any Python callable:
150
+
151
+ ```python
152
+ from taghound import TagHound
153
+ from taghound.models import TagRule
154
+
155
+ rules = [
156
+ TagRule(
157
+ id="python_rule",
158
+ label="Python",
159
+ weight=10,
160
+ required_fields={"language", "year"},
161
+ scalar_check=lambda d: d["language"] == "python" and d["year"] > 1990,
162
+ )
163
+ ]
164
+
165
+ th = TagHound(rules=rules)
166
+ ```
167
+
168
+ JSON works the same as YAML with an identical structure: `TagHound.rules_from_json("rules.json")`.
169
+
170
+ ## Example
171
+
172
+ [examples/greek_gods](examples/greek_gods) tags a CSV of Greek gods with role/domain rules and prints the resulting DataFrame: `uv run python examples/greek_gods/attribute_tags.py`
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ uv sync # set up the environment
178
+ just # list recipes: test, lint, bench, profile, ...
179
+ just check # lint + tests, same as CI
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,161 @@
1
+ # TagHound
2
+
3
+ Declarative tagging for Python: write matching rules in YAML or JSON, and TagHound attaches tags to your dicts or whole pandas DataFrames.
4
+
5
+ Rules stay readable and editable by non-technical users, while your pipeline code stays a one-liner. Typical uses:
6
+
7
+ - **Categorize bank transactions** — regex rules on merchant strings turn a CSV export into budget categories, no ML training required
8
+ - **Triage tickets and log events** — keyword and threshold rules attach routing tags (severity, team, topic) to each incoming record
9
+ - **Enrich scraped datasets** — bulk-tag job postings or product listings in a pandas pipeline, then rank matches by rule weights
10
+
11
+ ## Installation
12
+
13
+ Requires Python 3.11+.
14
+
15
+ ```bash
16
+ pip install taghound
17
+ ```
18
+
19
+ Latest from source: `pip install git+https://github.com/rzagreb/TagHound.git`
20
+
21
+ ## Quick start
22
+
23
+ Create `rules.yml`:
24
+
25
+ ```yaml
26
+ - id: food/coffee
27
+ label: Coffee
28
+ weight: 3
29
+ and:
30
+ - key: merchant
31
+ op: "~"
32
+ value: starbucks|blue bottle
33
+
34
+ - id: alerts/big-purchase
35
+ and:
36
+ - key: amount
37
+ op: ">"
38
+ value: 100
39
+ ```
40
+
41
+ Then:
42
+
43
+ ```python
44
+ from taghound import TagHound
45
+
46
+ th = TagHound.rules_from_yaml("rules.yml")
47
+
48
+ print(th.find_all_tags({"merchant": "STARBUCKS #1234", "amount": 6.40}))
49
+ # ('food/coffee',)
50
+ print(th.find_all_tags({"merchant": "Delta Airlines", "amount": 420.00}))
51
+ # ('alerts/big-purchase',)
52
+ ```
53
+
54
+ ## Rule format
55
+
56
+ A rule is a unique `id` plus a tree of conditions under `and` / `or`, nested as deep as you need:
57
+
58
+ ```yaml
59
+ - id: inventory/tall-tropical-tree # required, unique; returned as the tag
60
+ label: Tall tropical tree # optional, defaults to id
61
+ weight: 12 # optional score, defaults to 0
62
+ info: internal note, not matched # optional
63
+ and:
64
+ - key: type
65
+ value: tree # no `op` means `=`
66
+ - or:
67
+ - key: height
68
+ op: ">"
69
+ value: 20
70
+ - key: location
71
+ op: "~"
72
+ value: tropical
73
+ ```
74
+
75
+ ### Operators
76
+
77
+ | Op | Meaning | Value types |
78
+ |---|---|---|
79
+ | `=` | equal (default when `op` is omitted) | int, float, str, bool |
80
+ | `!=` | not equal | int, float, str, bool |
81
+ | `>` `<` `>=` `<=` | numeric comparison | int, float |
82
+ | `in` | field value is in the list | list |
83
+ | `not_in` | field value is not in the list | list |
84
+ | `~` | regex match | str or list of str |
85
+ | `!~` | regex does not match | str or list of str |
86
+
87
+ Regex matching is case-insensitive, and a list value is OR-joined (`starbucks|blue bottle`). Patterns are wrapped in `(?<!\w)(?:...)(?!\w)` so they match whole words; pass `merge_pattern=r"{pattern}"` to `rules_from_yaml`/`rules_from_json` for raw substring behavior, or any other wrapper with a `{pattern}` placeholder.
88
+
89
+ Invalid rules (bad regex, unknown operator) raise at load time, not on first evaluation.
90
+
91
+ ## Tagging a DataFrame
92
+
93
+ For large batches, evaluating a whole DataFrame at once is usually faster than calling `find_all_tags` per row (the break-even depends on your rules, so measure):
94
+
95
+ ```python
96
+ import pandas as pd
97
+
98
+ df = pd.DataFrame([
99
+ {"merchant": "Blue Bottle Coffee", "amount": 5.75},
100
+ {"merchant": "Delta Airlines", "amount": 420.00},
101
+ ])
102
+
103
+ print(th.find_tags_using_vector(df))
104
+ # merchant amount tags
105
+ # 0 Blue Bottle Coffee 5.75 [food/coffee]
106
+ # 1 Delta Airlines 420.00 [alerts/big-purchase]
107
+ ```
108
+
109
+ The tags column is added to the input DataFrame in place; rename it with `output_column_name=`. Alternatively, `output_format="columns"` returns a copy with one boolean column per rule (`food/coffee`, `alerts/big-purchase`) — handy for filtering and aggregation.
110
+
111
+ ## Scoring and labeling matches
112
+
113
+ Rule weights and labels are exposed as maps, so ranking matched records is a couple of lines:
114
+
115
+ ```python
116
+ tags = th.find_all_tags({"merchant": "STARBUCKS #1234", "amount": 6.40})
117
+
118
+ score = sum(th.rule_id_to_weight_map[t] for t in tags)
119
+ labels = [th.rule_id_to_label_map[t] for t in tags]
120
+ print(score, labels)
121
+ # 3.0 ['Coffee']
122
+ ```
123
+
124
+ ## Rules in code
125
+
126
+ Skip the files entirely by building rules with any Python callable:
127
+
128
+ ```python
129
+ from taghound import TagHound
130
+ from taghound.models import TagRule
131
+
132
+ rules = [
133
+ TagRule(
134
+ id="python_rule",
135
+ label="Python",
136
+ weight=10,
137
+ required_fields={"language", "year"},
138
+ scalar_check=lambda d: d["language"] == "python" and d["year"] > 1990,
139
+ )
140
+ ]
141
+
142
+ th = TagHound(rules=rules)
143
+ ```
144
+
145
+ JSON works the same as YAML with an identical structure: `TagHound.rules_from_json("rules.json")`.
146
+
147
+ ## Example
148
+
149
+ [examples/greek_gods](examples/greek_gods) tags a CSV of Greek gods with role/domain rules and prints the resulting DataFrame: `uv run python examples/greek_gods/attribute_tags.py`
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ uv sync # set up the environment
155
+ just # list recipes: test, lint, bench, profile, ...
156
+ just check # lint + tests, same as CI
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "taghound"
3
+ version = "0.2.0"
4
+ description = "TagHound: A Python library for managing and evaluating tag rules using scalar and vector operations. Supports YAML and JSON rule loading."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Roman Zagrebnev" }]
10
+ keywords = ["tags", "rules", "YAML", "JSON", "Python"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Operating System :: OS Independent",
17
+ "Typing :: Typed",
18
+ ]
19
+ dependencies = [
20
+ "pyyaml>=6.0.2",
21
+ "pandas>=2.2.3,<3",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/rzagreb/TagHound"
26
+ Repository = "https://github.com/rzagreb/TagHound"
27
+ Documentation = "https://github.com/rzagreb/TagHound#readme"
28
+ Changelog = "https://github.com/rzagreb/TagHound/blob/main/CHANGELOG.md"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest==9.1.1",
33
+ "ruff==0.15.18",
34
+ ]
35
+ profiling = [
36
+ "line-profiler==5.0.2",
37
+ "memray==1.19.3",
38
+ "pytest-benchmark==5.2.3",
39
+ ]
40
+
41
+ [tool.uv]
42
+ # Cap transitives to releases at least 30 days old (dependency soak window)
43
+ constraint-dependencies = [
44
+ "numpy<=2.4.6",
45
+ "pillow<=12.2.0",
46
+ "typing-extensions<=4.15.0",
47
+ "tzdata<=2026.2",
48
+ ]
49
+
50
+ [tool.uv.build-backend]
51
+ module-root = ""
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests", "taghound"]
55
+ pythonpath = ["."]
56
+ addopts = "--doctest-modules"
57
+
58
+ [build-system]
59
+ requires = ["uv_build>=0.11.21,<0.12"]
60
+ build-backend = "uv_build"
@@ -0,0 +1,5 @@
1
+ """TagHound: rule-based tagging for dicts and pandas DataFrames."""
2
+
3
+ from taghound.taghound import TagHound
4
+
5
+ __all__ = ["TagHound"]
@@ -0,0 +1,87 @@
1
+ """Shared enums, type aliases, and defaults for rule definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ import pandas as pd
10
+
11
+ PyEvalFn = Callable[[Mapping[str, Any]], bool]
12
+ PdEvalFn = Callable[[pd.DataFrame], pd.Series]
13
+
14
+
15
+ class LogicalOperator(Enum):
16
+ """Logical operators combining condition lists."""
17
+
18
+ AND = "and"
19
+ OR = "or"
20
+
21
+
22
+ class ComparisonOperator(Enum):
23
+ """Comparison operators usable in a condition's `op` field."""
24
+
25
+ EQUAL = "="
26
+ NOT_EQUAL = "!="
27
+ GREATER_THAN = ">"
28
+ GREATER_THAN_OR_EQUAL = ">="
29
+ LESS_THAN = "<"
30
+ LESS_THAN_OR_EQUAL = "<="
31
+ IS = "is"
32
+ IS_NOT = "is_not"
33
+ IN = "in"
34
+ NOT_IN = "not_in"
35
+ REGEX_MATCH = "~"
36
+ REGEX_NOT_MATCH = "!~"
37
+
38
+
39
+ ComparisonOperatorRegexOnly = {
40
+ ComparisonOperator.REGEX_MATCH,
41
+ ComparisonOperator.REGEX_NOT_MATCH,
42
+ }
43
+ ComparisonOperatorNumericOnly = {
44
+ ComparisonOperator.GREATER_THAN,
45
+ ComparisonOperator.GREATER_THAN_OR_EQUAL,
46
+ ComparisonOperator.LESS_THAN,
47
+ ComparisonOperator.LESS_THAN_OR_EQUAL,
48
+ }
49
+ ComparisonOperatorBoolOnly = {
50
+ ComparisonOperator.IS,
51
+ ComparisonOperator.IS_NOT,
52
+ }
53
+ ComparisonOperatorListOnly = {
54
+ ComparisonOperator.IN,
55
+ ComparisonOperator.NOT_IN,
56
+ }
57
+
58
+
59
+ class RuleKey(Enum):
60
+ """Top-level keys of a rule definition."""
61
+
62
+ ID = "id"
63
+ LABEL = "label"
64
+ WEIGHT = "weight"
65
+ INFO = "info"
66
+ ROOT_AND = LogicalOperator.AND.value
67
+ ROOT_OR = LogicalOperator.OR.value
68
+
69
+
70
+ class ComparisonKey(Enum):
71
+ """Keys of a single comparison condition."""
72
+
73
+ KEY = "key"
74
+ OPERATOR = "op"
75
+ VALUE = "value"
76
+
77
+
78
+ DEFAULT_COMPARISON_OPERATOR = ComparisonOperator.EQUAL
79
+ """ Default comparison operator to use when not specified """
80
+
81
+ DEFAULT_WEIGHT = 0.0
82
+ """ Default weight to use when not specified """
83
+
84
+ # - This is better than \b...\b because it also matches cases like `C++`
85
+ # - Limitations: does not work when we need to match `.python` with `python` pattern
86
+ DEFAULT_REGEX_MERGE_PATTERN = r"(?<!\w)(?:{pattern})(?!\w)"
87
+ """ Pattern used to merge list of patterns into a single regex pattern """
@@ -0,0 +1,13 @@
1
+ """Exceptions raised while parsing rule definitions."""
2
+
3
+
4
+ class InvalidOperatorError(Exception):
5
+ """A logical node uses an operator other than `and`/`or`."""
6
+
7
+
8
+ class RuleAndOrTogetherError(Exception):
9
+ """A rule's root condition has both `and` and `or`."""
10
+
11
+
12
+ class MissingRootConditionError(Exception):
13
+ """A rule's root condition has neither `and` nor `or`."""
@@ -0,0 +1,38 @@
1
+ """Data model for tag rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from taghound.constants import PdEvalFn, PyEvalFn
10
+
11
+
12
+ @dataclass(frozen=True, eq=True, slots=True)
13
+ class TagRule:
14
+ """A single tag rule: identity, metadata, and its prebuilt check functions."""
15
+
16
+ id: str
17
+ """ Unique identifier for the rule. """
18
+
19
+ label: str = field(compare=False)
20
+ """ The label for the rule. No uniqueness is enforced. """
21
+
22
+ weight: float = field(compare=False)
23
+ """ The weight of the rule. """
24
+
25
+ required_fields: set[str] = field(compare=False)
26
+ """ The required fields for the rule. """
27
+
28
+ info: str | None = field(compare=False, default=None)
29
+ """ Internal comments for the rule. """
30
+
31
+ scalar_check: PyEvalFn | None = field(compare=False, default=None)
32
+ """ Scalar function used to check if the rule is satisfied. """
33
+
34
+ vector_check: PdEvalFn | None = field(compare=False, default=None)
35
+ """ Vector function used to check if the rule is satisfied. """
36
+
37
+ data: Mapping[str, Any] = field(compare=False, default_factory=dict)
38
+ """ The original data used to create the TagRule object. """
File without changes
@@ -0,0 +1 @@
1
+ """Scalar (one mapping at a time) rule evaluation."""