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.
Files changed (34) hide show
  1. email_tagger-0.2.0/LICENSE +21 -0
  2. email_tagger-0.2.0/PKG-INFO +181 -0
  3. email_tagger-0.2.0/README.md +154 -0
  4. email_tagger-0.2.0/pyproject.toml +62 -0
  5. email_tagger-0.2.0/setup.cfg +4 -0
  6. email_tagger-0.2.0/src/email_tagger/__init__.py +0 -0
  7. email_tagger-0.2.0/src/email_tagger/cache.py +97 -0
  8. email_tagger-0.2.0/src/email_tagger/checkpoint.py +71 -0
  9. email_tagger-0.2.0/src/email_tagger/classifiers/__init__.py +0 -0
  10. email_tagger-0.2.0/src/email_tagger/classifiers/contact_classifier.py +144 -0
  11. email_tagger-0.2.0/src/email_tagger/cli.py +516 -0
  12. email_tagger-0.2.0/src/email_tagger/cost_estimator.py +67 -0
  13. email_tagger-0.2.0/src/email_tagger/io/__init__.py +0 -0
  14. email_tagger-0.2.0/src/email_tagger/io/artifacts.py +39 -0
  15. email_tagger-0.2.0/src/email_tagger/io/readers.py +94 -0
  16. email_tagger-0.2.0/src/email_tagger/io/writers.py +71 -0
  17. email_tagger-0.2.0/src/email_tagger/metrics.py +112 -0
  18. email_tagger-0.2.0/src/email_tagger/models.py +165 -0
  19. email_tagger-0.2.0/src/email_tagger/privacy/__init__.py +138 -0
  20. email_tagger-0.2.0/src/email_tagger/privacy/payload_builder.py +67 -0
  21. email_tagger-0.2.0/src/email_tagger/privacy/policies.py +42 -0
  22. email_tagger-0.2.0/src/email_tagger/privacy/redactor.py +88 -0
  23. email_tagger-0.2.0/src/email_tagger/providers/__init__.py +0 -0
  24. email_tagger-0.2.0/src/email_tagger/providers/base.py +65 -0
  25. email_tagger-0.2.0/src/email_tagger/providers/factory.py +80 -0
  26. email_tagger-0.2.0/src/email_tagger/providers/local_provider.py +105 -0
  27. email_tagger-0.2.0/src/email_tagger/providers/openai_provider.py +126 -0
  28. email_tagger-0.2.0/src/email_tagger/types.py +58 -0
  29. email_tagger-0.2.0/src/email_tagger.egg-info/PKG-INFO +181 -0
  30. email_tagger-0.2.0/src/email_tagger.egg-info/SOURCES.txt +32 -0
  31. email_tagger-0.2.0/src/email_tagger.egg-info/dependency_links.txt +1 -0
  32. email_tagger-0.2.0/src/email_tagger.egg-info/entry_points.txt +2 -0
  33. email_tagger-0.2.0/src/email_tagger.egg-info/requires.txt +20 -0
  34. 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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)