email-tagger 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.
- email_tagger-0.2.0/LICENSE +21 -0
- email_tagger-0.2.0/PKG-INFO +181 -0
- email_tagger-0.2.0/README.md +154 -0
- email_tagger-0.2.0/pyproject.toml +62 -0
- email_tagger-0.2.0/setup.cfg +4 -0
- email_tagger-0.2.0/src/email_tagger/__init__.py +0 -0
- email_tagger-0.2.0/src/email_tagger/cache.py +97 -0
- email_tagger-0.2.0/src/email_tagger/checkpoint.py +71 -0
- email_tagger-0.2.0/src/email_tagger/classifiers/__init__.py +0 -0
- email_tagger-0.2.0/src/email_tagger/classifiers/contact_classifier.py +144 -0
- email_tagger-0.2.0/src/email_tagger/cli.py +516 -0
- email_tagger-0.2.0/src/email_tagger/cost_estimator.py +67 -0
- email_tagger-0.2.0/src/email_tagger/io/__init__.py +0 -0
- email_tagger-0.2.0/src/email_tagger/io/artifacts.py +39 -0
- email_tagger-0.2.0/src/email_tagger/io/readers.py +94 -0
- email_tagger-0.2.0/src/email_tagger/io/writers.py +71 -0
- email_tagger-0.2.0/src/email_tagger/metrics.py +112 -0
- email_tagger-0.2.0/src/email_tagger/models.py +165 -0
- email_tagger-0.2.0/src/email_tagger/privacy/__init__.py +138 -0
- email_tagger-0.2.0/src/email_tagger/privacy/payload_builder.py +67 -0
- email_tagger-0.2.0/src/email_tagger/privacy/policies.py +42 -0
- email_tagger-0.2.0/src/email_tagger/privacy/redactor.py +88 -0
- email_tagger-0.2.0/src/email_tagger/providers/__init__.py +0 -0
- email_tagger-0.2.0/src/email_tagger/providers/base.py +65 -0
- email_tagger-0.2.0/src/email_tagger/providers/factory.py +80 -0
- email_tagger-0.2.0/src/email_tagger/providers/local_provider.py +105 -0
- email_tagger-0.2.0/src/email_tagger/providers/openai_provider.py +126 -0
- email_tagger-0.2.0/src/email_tagger/types.py +58 -0
- email_tagger-0.2.0/src/email_tagger.egg-info/PKG-INFO +181 -0
- email_tagger-0.2.0/src/email_tagger.egg-info/SOURCES.txt +32 -0
- email_tagger-0.2.0/src/email_tagger.egg-info/dependency_links.txt +1 -0
- email_tagger-0.2.0/src/email_tagger.egg-info/entry_points.txt +2 -0
- email_tagger-0.2.0/src/email_tagger.egg-info/requires.txt +20 -0
- email_tagger-0.2.0/src/email_tagger.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 email-tagger
|
|
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,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: email-tagger
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AI Email Contact Tagger — klasyfikacja kontaktów z kontrolą prywatności
|
|
5
|
+
Author-email: email-tagger team <dev@email-tagger.example>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: pandas>=2.0
|
|
12
|
+
Requires-Dist: rich>=13.0
|
|
13
|
+
Requires-Dist: tenacity>=8.0
|
|
14
|
+
Requires-Dist: python-dotenv>=1.0
|
|
15
|
+
Provides-Extra: openai
|
|
16
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
17
|
+
Provides-Extra: local
|
|
18
|
+
Requires-Dist: llama-cpp-python>=0.2; extra == "local"
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: email-tagger[local,openai]; extra == "all"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# email-tagger
|
|
29
|
+
|
|
30
|
+
**AI Email Contact Tagger** — enterprise-grade narzędzie do automatycznej klasyfikacji bazy kontaktów e-mailowych z pełną kontrolą prywatności.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install email-tagger
|
|
34
|
+
email-tagger classify -i kontakty.csv -o otagowane.csv
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 🧠 Co robi
|
|
38
|
+
|
|
39
|
+
Czyta CSV z kontaktami, wysyła dane do AI (OpenAI lub lokalny model), i przypisuje tagi w 3 kategoriach:
|
|
40
|
+
|
|
41
|
+
| Kategoria | Tagi |
|
|
42
|
+
|-----------|------|
|
|
43
|
+
| **Branża** | e-commerce, saas, fintech, agencja, produkcja, edukacja, zdrowie, nieruchomosci, consulting, it_tech, media, nonprofit, handel, inne |
|
|
44
|
+
| **Rola** | founder, c_level, dyrektor, marketing, sprzedaz, it, hr, operacje, administracja, inne_rola |
|
|
45
|
+
| **Intencja** | potencjalny_klient, aktywny_prospect, partner, zimny_lead, inwestor, media_kontakt, rekruter, nieokreslona |
|
|
46
|
+
|
|
47
|
+
Wynik: oryginalny CSV + 4 nowe kolumny `tag_branza`, `tag_rola`, `tag_intencja`, `tag_pewnosc`.
|
|
48
|
+
|
|
49
|
+
## 🚀 Szybki start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Instalacja
|
|
53
|
+
pip install email-tagger
|
|
54
|
+
|
|
55
|
+
# Wymagane: klucz API OpenAI (dla profili cloud)
|
|
56
|
+
export OPENAI_API_KEY=sk-proj-...
|
|
57
|
+
|
|
58
|
+
# Podstawowe użycie
|
|
59
|
+
email-tagger classify -i kontakty.csv -o otagowane.csv
|
|
60
|
+
|
|
61
|
+
# Z pełną kontrolą prywatności i budżetem
|
|
62
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy strict-local --budzet 0.50
|
|
63
|
+
|
|
64
|
+
# Walidacja pliku bez wysyłania do API
|
|
65
|
+
email-tagger validate -i kontakty.csv
|
|
66
|
+
|
|
67
|
+
# Test na małej próbce
|
|
68
|
+
email-tagger sample -i kontakty.csv --size 10
|
|
69
|
+
|
|
70
|
+
# Szacowanie kosztów przed runem
|
|
71
|
+
email-tagger estimate -i kontakty.csv --privacy cloud-minimized
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 🔒 Profil prywatności
|
|
75
|
+
|
|
76
|
+
Najważniejsza cecha produktu — kontrola jakie dane opuszczają Twoją maszynę.
|
|
77
|
+
|
|
78
|
+
### `strict-local`
|
|
79
|
+
- **Żadne dane nie opuszczają maszyny** — używa lokalnego modelu (llama.cpp)
|
|
80
|
+
- Email hashowany, PII maskowane
|
|
81
|
+
- Idealne dla firm z twardymi wymogami compliance
|
|
82
|
+
|
|
83
|
+
### `cloud-minimized` (domyślny)
|
|
84
|
+
- Tylko niezbędne pola: hash emaila + firma + stanowisko
|
|
85
|
+
- Email NIGDY nie jest wysyłany w czystej postaci
|
|
86
|
+
- PII w notatkach maskowane przed wysłaniem
|
|
87
|
+
- Użytkownik zatwierdza przed runem
|
|
88
|
+
|
|
89
|
+
### `enterprise`
|
|
90
|
+
- Pełne dane, ale z gwarancją zero-data-retention u providera
|
|
91
|
+
- Wymaga świadomej zgody użytkownika
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy strict-local
|
|
95
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy cloud-minimized
|
|
96
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy enterprise
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 💰 Koszty
|
|
100
|
+
|
|
101
|
+
Model `gpt-4o-mini`: ~$0.15/1M tokenów input, ~$0.60/1M tokenów output.
|
|
102
|
+
|
|
103
|
+
Dla 1000 kontaktów (~200 znaków każdy): **~$0.001-0.005 za całość**.
|
|
104
|
+
|
|
105
|
+
Zawsze sprawdzaj koszt przed runem:
|
|
106
|
+
```bash
|
|
107
|
+
email-tagger estimate -i kontakty.csv
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 📦 Artefakty
|
|
111
|
+
|
|
112
|
+
Po każdym runie generowane są:
|
|
113
|
+
- `otagowane.csv` — główny wynik
|
|
114
|
+
- `otagowane.report.json` — raport z metrykami i dystrybucją tagów
|
|
115
|
+
- `otagowane.errors.csv` — błędy per kontakt
|
|
116
|
+
|
|
117
|
+
## ⚙️ Subkomendy CLI
|
|
118
|
+
|
|
119
|
+
| Komenda | Opis |
|
|
120
|
+
|---------|------|
|
|
121
|
+
| `classify` | Główne tagowanie kontaktów |
|
|
122
|
+
| `validate` | Sprawdza CSV bez API |
|
|
123
|
+
| `sample` | Test na małej próbce |
|
|
124
|
+
| `estimate` | Szacuje koszt bez wykonywania |
|
|
125
|
+
| `cache stats` | Statystyki cache |
|
|
126
|
+
| `cache clear` | Czyszczenie cache |
|
|
127
|
+
|
|
128
|
+
### Opcje `classify`
|
|
129
|
+
|
|
130
|
+
| Flaga | Opis | Domyślnie |
|
|
131
|
+
|-------|------|-----------|
|
|
132
|
+
| `-i, --input` | Ścieżka do CSV | wymagane |
|
|
133
|
+
| `-o, --output` | Ścieżka wyjściowa | wymagane |
|
|
134
|
+
| `-b, --batch` | Rozmiar batcha | 10 |
|
|
135
|
+
| `-m, --model` | Model AI | gpt-4o-mini |
|
|
136
|
+
| `--provider` | Provider (openai, local) | openai |
|
|
137
|
+
| `--privacy` | Profil prywatności | cloud-minimized |
|
|
138
|
+
| `--budzet` | Max koszt w USD | 0 (brak) |
|
|
139
|
+
| `--resume` | Wznów przerwany run | — |
|
|
140
|
+
| `--no-cache` | Wyłącz cache | — |
|
|
141
|
+
| `-y, --yes` | Automatyczne potwierdzenie | — |
|
|
142
|
+
| `--debug` | Logowanie debug | — |
|
|
143
|
+
|
|
144
|
+
## 🏗️ Architektura
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
src/email_tagger/
|
|
148
|
+
├── cli.py # Interfejs CLI
|
|
149
|
+
├── models.py # Pydantic modele (structured outputs)
|
|
150
|
+
├── types.py # Typy pomocnicze
|
|
151
|
+
├── cache.py # SQLite cache wyników
|
|
152
|
+
├── checkpoint.py # Przyrostowy checkpoint/resume
|
|
153
|
+
├── cost_estimator.py # Szacowanie kosztów
|
|
154
|
+
├── metrics.py # Metryki i raporty
|
|
155
|
+
├── classifiers/
|
|
156
|
+
│ └── contact_classifier.py # Główna logika klasyfikacji
|
|
157
|
+
├── privacy/
|
|
158
|
+
│ ├── redactor.py # Maskowanie PII
|
|
159
|
+
│ ├── payload_builder.py # Budowa bezpiecznego payloadu
|
|
160
|
+
│ └── policies.py # Polityki prywatności
|
|
161
|
+
├── providers/
|
|
162
|
+
│ ├── base.py # Abstrakcyjny interfejs providera
|
|
163
|
+
│ ├── openai_provider.py # OpenAI API
|
|
164
|
+
│ ├── local_provider.py # Lokalny model (llama.cpp)
|
|
165
|
+
│ └── factory.py # Fabryka providerów
|
|
166
|
+
└── io/
|
|
167
|
+
├── readers.py # Wczytywanie CSV
|
|
168
|
+
├── writers.py # Zapis CSV
|
|
169
|
+
└── artifacts.py # Artefakty runa
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 🧪 Testy
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install email-tagger[dev]
|
|
176
|
+
pytest tests/ -v
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 📄 Licencja
|
|
180
|
+
|
|
181
|
+
MIT — gotowe do komercyjnego użycia.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# email-tagger
|
|
2
|
+
|
|
3
|
+
**AI Email Contact Tagger** — enterprise-grade narzędzie do automatycznej klasyfikacji bazy kontaktów e-mailowych z pełną kontrolą prywatności.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install email-tagger
|
|
7
|
+
email-tagger classify -i kontakty.csv -o otagowane.csv
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## 🧠 Co robi
|
|
11
|
+
|
|
12
|
+
Czyta CSV z kontaktami, wysyła dane do AI (OpenAI lub lokalny model), i przypisuje tagi w 3 kategoriach:
|
|
13
|
+
|
|
14
|
+
| Kategoria | Tagi |
|
|
15
|
+
|-----------|------|
|
|
16
|
+
| **Branża** | e-commerce, saas, fintech, agencja, produkcja, edukacja, zdrowie, nieruchomosci, consulting, it_tech, media, nonprofit, handel, inne |
|
|
17
|
+
| **Rola** | founder, c_level, dyrektor, marketing, sprzedaz, it, hr, operacje, administracja, inne_rola |
|
|
18
|
+
| **Intencja** | potencjalny_klient, aktywny_prospect, partner, zimny_lead, inwestor, media_kontakt, rekruter, nieokreslona |
|
|
19
|
+
|
|
20
|
+
Wynik: oryginalny CSV + 4 nowe kolumny `tag_branza`, `tag_rola`, `tag_intencja`, `tag_pewnosc`.
|
|
21
|
+
|
|
22
|
+
## 🚀 Szybki start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Instalacja
|
|
26
|
+
pip install email-tagger
|
|
27
|
+
|
|
28
|
+
# Wymagane: klucz API OpenAI (dla profili cloud)
|
|
29
|
+
export OPENAI_API_KEY=sk-proj-...
|
|
30
|
+
|
|
31
|
+
# Podstawowe użycie
|
|
32
|
+
email-tagger classify -i kontakty.csv -o otagowane.csv
|
|
33
|
+
|
|
34
|
+
# Z pełną kontrolą prywatności i budżetem
|
|
35
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy strict-local --budzet 0.50
|
|
36
|
+
|
|
37
|
+
# Walidacja pliku bez wysyłania do API
|
|
38
|
+
email-tagger validate -i kontakty.csv
|
|
39
|
+
|
|
40
|
+
# Test na małej próbce
|
|
41
|
+
email-tagger sample -i kontakty.csv --size 10
|
|
42
|
+
|
|
43
|
+
# Szacowanie kosztów przed runem
|
|
44
|
+
email-tagger estimate -i kontakty.csv --privacy cloud-minimized
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 🔒 Profil prywatności
|
|
48
|
+
|
|
49
|
+
Najważniejsza cecha produktu — kontrola jakie dane opuszczają Twoją maszynę.
|
|
50
|
+
|
|
51
|
+
### `strict-local`
|
|
52
|
+
- **Żadne dane nie opuszczają maszyny** — używa lokalnego modelu (llama.cpp)
|
|
53
|
+
- Email hashowany, PII maskowane
|
|
54
|
+
- Idealne dla firm z twardymi wymogami compliance
|
|
55
|
+
|
|
56
|
+
### `cloud-minimized` (domyślny)
|
|
57
|
+
- Tylko niezbędne pola: hash emaila + firma + stanowisko
|
|
58
|
+
- Email NIGDY nie jest wysyłany w czystej postaci
|
|
59
|
+
- PII w notatkach maskowane przed wysłaniem
|
|
60
|
+
- Użytkownik zatwierdza przed runem
|
|
61
|
+
|
|
62
|
+
### `enterprise`
|
|
63
|
+
- Pełne dane, ale z gwarancją zero-data-retention u providera
|
|
64
|
+
- Wymaga świadomej zgody użytkownika
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy strict-local
|
|
68
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy cloud-minimized
|
|
69
|
+
email-tagger classify -i kontakty.csv -o out.csv --privacy enterprise
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 💰 Koszty
|
|
73
|
+
|
|
74
|
+
Model `gpt-4o-mini`: ~$0.15/1M tokenów input, ~$0.60/1M tokenów output.
|
|
75
|
+
|
|
76
|
+
Dla 1000 kontaktów (~200 znaków każdy): **~$0.001-0.005 za całość**.
|
|
77
|
+
|
|
78
|
+
Zawsze sprawdzaj koszt przed runem:
|
|
79
|
+
```bash
|
|
80
|
+
email-tagger estimate -i kontakty.csv
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 📦 Artefakty
|
|
84
|
+
|
|
85
|
+
Po każdym runie generowane są:
|
|
86
|
+
- `otagowane.csv` — główny wynik
|
|
87
|
+
- `otagowane.report.json` — raport z metrykami i dystrybucją tagów
|
|
88
|
+
- `otagowane.errors.csv` — błędy per kontakt
|
|
89
|
+
|
|
90
|
+
## ⚙️ Subkomendy CLI
|
|
91
|
+
|
|
92
|
+
| Komenda | Opis |
|
|
93
|
+
|---------|------|
|
|
94
|
+
| `classify` | Główne tagowanie kontaktów |
|
|
95
|
+
| `validate` | Sprawdza CSV bez API |
|
|
96
|
+
| `sample` | Test na małej próbce |
|
|
97
|
+
| `estimate` | Szacuje koszt bez wykonywania |
|
|
98
|
+
| `cache stats` | Statystyki cache |
|
|
99
|
+
| `cache clear` | Czyszczenie cache |
|
|
100
|
+
|
|
101
|
+
### Opcje `classify`
|
|
102
|
+
|
|
103
|
+
| Flaga | Opis | Domyślnie |
|
|
104
|
+
|-------|------|-----------|
|
|
105
|
+
| `-i, --input` | Ścieżka do CSV | wymagane |
|
|
106
|
+
| `-o, --output` | Ścieżka wyjściowa | wymagane |
|
|
107
|
+
| `-b, --batch` | Rozmiar batcha | 10 |
|
|
108
|
+
| `-m, --model` | Model AI | gpt-4o-mini |
|
|
109
|
+
| `--provider` | Provider (openai, local) | openai |
|
|
110
|
+
| `--privacy` | Profil prywatności | cloud-minimized |
|
|
111
|
+
| `--budzet` | Max koszt w USD | 0 (brak) |
|
|
112
|
+
| `--resume` | Wznów przerwany run | — |
|
|
113
|
+
| `--no-cache` | Wyłącz cache | — |
|
|
114
|
+
| `-y, --yes` | Automatyczne potwierdzenie | — |
|
|
115
|
+
| `--debug` | Logowanie debug | — |
|
|
116
|
+
|
|
117
|
+
## 🏗️ Architektura
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
src/email_tagger/
|
|
121
|
+
├── cli.py # Interfejs CLI
|
|
122
|
+
├── models.py # Pydantic modele (structured outputs)
|
|
123
|
+
├── types.py # Typy pomocnicze
|
|
124
|
+
├── cache.py # SQLite cache wyników
|
|
125
|
+
├── checkpoint.py # Przyrostowy checkpoint/resume
|
|
126
|
+
├── cost_estimator.py # Szacowanie kosztów
|
|
127
|
+
├── metrics.py # Metryki i raporty
|
|
128
|
+
├── classifiers/
|
|
129
|
+
│ └── contact_classifier.py # Główna logika klasyfikacji
|
|
130
|
+
├── privacy/
|
|
131
|
+
│ ├── redactor.py # Maskowanie PII
|
|
132
|
+
│ ├── payload_builder.py # Budowa bezpiecznego payloadu
|
|
133
|
+
│ └── policies.py # Polityki prywatności
|
|
134
|
+
├── providers/
|
|
135
|
+
│ ├── base.py # Abstrakcyjny interfejs providera
|
|
136
|
+
│ ├── openai_provider.py # OpenAI API
|
|
137
|
+
│ ├── local_provider.py # Lokalny model (llama.cpp)
|
|
138
|
+
│ └── factory.py # Fabryka providerów
|
|
139
|
+
└── io/
|
|
140
|
+
├── readers.py # Wczytywanie CSV
|
|
141
|
+
├── writers.py # Zapis CSV
|
|
142
|
+
└── artifacts.py # Artefakty runa
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 🧪 Testy
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install email-tagger[dev]
|
|
149
|
+
pytest tests/ -v
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 📄 Licencja
|
|
153
|
+
|
|
154
|
+
MIT — gotowe do komercyjnego użycia.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "email-tagger"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "AI Email Contact Tagger — klasyfikacja kontaktów z kontrolą prywatności"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "email-tagger team", email = "dev@email-tagger.example"},
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pydantic>=2.0",
|
|
18
|
+
"pandas>=2.0",
|
|
19
|
+
"rich>=13.0",
|
|
20
|
+
"tenacity>=8.0",
|
|
21
|
+
"python-dotenv>=1.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
openai = [
|
|
26
|
+
"openai>=1.0",
|
|
27
|
+
]
|
|
28
|
+
local = [
|
|
29
|
+
"llama-cpp-python>=0.2",
|
|
30
|
+
]
|
|
31
|
+
all = [
|
|
32
|
+
"email-tagger[openai,local]",
|
|
33
|
+
]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"pytest-cov>=4.0",
|
|
37
|
+
"ruff>=0.1",
|
|
38
|
+
"mypy>=1.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
email-tagger = "email_tagger.cli:app"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
include = ["email_tagger*"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "W", "I"]
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
python_version = "3.10"
|
|
57
|
+
strict = true
|
|
58
|
+
ignore_missing_imports = true
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
python_files = ["test_*.py"]
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
email_tagger.cache — Lokalny cache wyników klasyfikacji.
|
|
3
|
+
|
|
4
|
+
Unika wielokrotnego wysyłania tych samych danych do API.
|
|
5
|
+
Klucz cache: hash znormalizowanych danych wejściowych.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import sqlite3
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClassificationCache:
|
|
21
|
+
"""
|
|
22
|
+
SQLite-based cache dla wyników klasyfikacji.
|
|
23
|
+
|
|
24
|
+
Przechowuje hash payloadu -> wynik TagResult jako JSON.
|
|
25
|
+
Domyślnie w ~/.email-tagger/cache.db z TTL 30 dni.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, sciezka: Optional[Path] = None) -> None:
|
|
29
|
+
if sciezka is None:
|
|
30
|
+
sciezka = Path.home() / ".email-tagger" / "cache.db"
|
|
31
|
+
self.sciezka = sciezka
|
|
32
|
+
self.sciezka.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def conn(self) -> sqlite3.Connection:
|
|
37
|
+
if self._conn is None:
|
|
38
|
+
self._conn = sqlite3.connect(str(self.sciezka))
|
|
39
|
+
self._conn.execute(
|
|
40
|
+
"""CREATE TABLE IF NOT EXISTS cache (
|
|
41
|
+
klucz TEXT PRIMARY KEY,
|
|
42
|
+
wynik TEXT NOT NULL,
|
|
43
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
44
|
+
)"""
|
|
45
|
+
)
|
|
46
|
+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_cache_created ON cache(created_at)")
|
|
47
|
+
self._conn.commit()
|
|
48
|
+
return self._conn
|
|
49
|
+
|
|
50
|
+
def klucz(self, payload: dict[str, Any]) -> str:
|
|
51
|
+
"""Generuje klucz cache z payloadu."""
|
|
52
|
+
raw = json.dumps(payload, sort_keys=True, ensure_ascii=False)
|
|
53
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
54
|
+
|
|
55
|
+
def pobierz(self, payload: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
56
|
+
"""Próbuje pobrać wynik z cache."""
|
|
57
|
+
k = self.klucz(payload)
|
|
58
|
+
row = self.conn.execute("SELECT wynik FROM cache WHERE klucz = ?", (k,)).fetchone()
|
|
59
|
+
if row:
|
|
60
|
+
logger.debug("Cache HIT: %s", k[:12])
|
|
61
|
+
return json.loads(row[0])
|
|
62
|
+
logger.debug("Cache MISS: %s", k[:12])
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def zapisz(self, payload: dict[str, Any], wynik: dict[str, Any]) -> None:
|
|
66
|
+
"""Zapisuje wynik do cache."""
|
|
67
|
+
k = self.klucz(payload)
|
|
68
|
+
self.conn.execute(
|
|
69
|
+
"INSERT OR REPLACE INTO cache (klucz, wynik) VALUES (?, ?)",
|
|
70
|
+
(k, json.dumps(wynik, ensure_ascii=False)),
|
|
71
|
+
)
|
|
72
|
+
self.conn.commit()
|
|
73
|
+
|
|
74
|
+
def czysc_stare(self, dni: int = 30) -> int:
|
|
75
|
+
"""Usuwa wpisy starsze niż N dni. Zwraca liczbę usuniętych."""
|
|
76
|
+
usuniete = self.conn.execute(
|
|
77
|
+
"DELETE FROM cache WHERE created_at < datetime('now', ?)",
|
|
78
|
+
(f"-{dni} days",),
|
|
79
|
+
).rowcount
|
|
80
|
+
self.conn.commit()
|
|
81
|
+
if usuniete:
|
|
82
|
+
logger.info("Wyczyszczono %d starych wpisów cache", usuniete)
|
|
83
|
+
return usuniete
|
|
84
|
+
|
|
85
|
+
def statystyki(self) -> dict:
|
|
86
|
+
"""Zwraca statystyki cache."""
|
|
87
|
+
total = self.conn.execute("SELECT COUNT(*) FROM cache").fetchone()[0]
|
|
88
|
+
return {
|
|
89
|
+
"sciezka": str(self.sciezka),
|
|
90
|
+
"wpisy": total,
|
|
91
|
+
"rozmiar_mb": round(self.sciezka.stat().st_size / 1_000_000, 2) if self.sciezka.exists() else 0,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def zamknij(self) -> None:
|
|
95
|
+
if self._conn:
|
|
96
|
+
self._conn.close()
|
|
97
|
+
self._conn = None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
email_tagger.checkpoint — Przyrostowy checkpoint i resume.
|
|
3
|
+
|
|
4
|
+
Zapisuje postęp po każdym batchu, pozwala wznowić po przerwaniu.
|
|
5
|
+
Krytyczne dla produkcyjnego użycia — bez tego każdy crash = strata całej pracy.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Checkpoint:
|
|
19
|
+
"""
|
|
20
|
+
Checkpoint dla przyrostowego przetwarzania batchy.
|
|
21
|
+
|
|
22
|
+
Zapisuje do pliku JSON:
|
|
23
|
+
- indeks ostatniego przetworzonego wiersza
|
|
24
|
+
- całkowitą liczbę wierszy
|
|
25
|
+
- timestamp i model
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, sciezka: Path) -> None:
|
|
29
|
+
self.sciezka = sciezka
|
|
30
|
+
self._dane: dict[str, Any] = {}
|
|
31
|
+
|
|
32
|
+
def wczytaj(self) -> Optional[dict[str, Any]]:
|
|
33
|
+
"""Wczytuje checkpoint z pliku. Zwraca None jeśli nie istnieje."""
|
|
34
|
+
if not self.sciezka.exists():
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
with open(self.sciezka) as f:
|
|
38
|
+
self._dane = json.load(f)
|
|
39
|
+
logger.info("Wczytano checkpoint: %s", self.sciezka)
|
|
40
|
+
return self._dane
|
|
41
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
42
|
+
logger.warning("Nie można wczytać checkpointu: %s", e)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def zapisz(self, indeks: int, ogolem: int, **extra) -> None:
|
|
46
|
+
"""Zapisuje checkpoint."""
|
|
47
|
+
self._dane = {
|
|
48
|
+
"indeks": indeks,
|
|
49
|
+
"ogolem": ogolem,
|
|
50
|
+
"procent": round(indeks / ogolem * 100, 1) if ogolem > 0 else 0,
|
|
51
|
+
**extra,
|
|
52
|
+
}
|
|
53
|
+
self.sciezka.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
with open(self.sciezka, "w") as f:
|
|
55
|
+
json.dump(self._dane, f, indent=2, ensure_ascii=False)
|
|
56
|
+
logger.info("Checkpoint: %d/%d (%.1f%%)", indeks, ogolem, self._dane["procent"])
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def ostatni_indeks(self) -> int:
|
|
60
|
+
"""Zwraca indeks ostatniego przetworzonego wiersza."""
|
|
61
|
+
return self._dane.get("indeks", -1)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def czy_istnieje(self) -> bool:
|
|
65
|
+
return self.sciezka.exists()
|
|
66
|
+
|
|
67
|
+
def usun(self) -> None:
|
|
68
|
+
"""Usuwa plik checkpointu — po zakończonym runie."""
|
|
69
|
+
if self.sciezka.exists():
|
|
70
|
+
self.sciezka.unlink()
|
|
71
|
+
logger.info("Usunięto checkpoint: %s", self.sciezka)
|
|
File without changes
|