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 +21 -0
- taghound-0.2.0/PKG-INFO +184 -0
- taghound-0.2.0/README.md +161 -0
- taghound-0.2.0/pyproject.toml +60 -0
- taghound-0.2.0/taghound/__init__.py +5 -0
- taghound-0.2.0/taghound/constants.py +87 -0
- taghound-0.2.0/taghound/exceptions.py +13 -0
- taghound-0.2.0/taghound/models.py +38 -0
- taghound-0.2.0/taghound/py.typed +0 -0
- taghound-0.2.0/taghound/scalar/__init__.py +1 -0
- taghound-0.2.0/taghound/scalar/function_builder.py +218 -0
- taghound-0.2.0/taghound/scalar/operators.py +91 -0
- taghound-0.2.0/taghound/serializers.py +129 -0
- taghound-0.2.0/taghound/taghound.py +283 -0
- taghound-0.2.0/taghound/vector/__init__.py +1 -0
- taghound-0.2.0/taghound/vector/function_builder.py +278 -0
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.
|
taghound-0.2.0/PKG-INFO
ADDED
|
@@ -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).
|
taghound-0.2.0/README.md
ADDED
|
@@ -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,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."""
|