kataster-mcp 0.3.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.
- kataster_mcp-0.3.0/.gitignore +26 -0
- kataster_mcp-0.3.0/LICENSE +21 -0
- kataster_mcp-0.3.0/PKG-INFO +173 -0
- kataster_mcp-0.3.0/README.md +144 -0
- kataster_mcp-0.3.0/pyproject.toml +57 -0
- kataster_mcp-0.3.0/src/kataster_mcp/__init__.py +3 -0
- kataster_mcp-0.3.0/src/kataster_mcp/cache.py +232 -0
- kataster_mcp-0.3.0/src/kataster_mcp/cli.py +227 -0
- kataster_mcp-0.3.0/src/kataster_mcp/data/ku.json +1 -0
- kataster_mcp-0.3.0/src/kataster_mcp/envelope.py +68 -0
- kataster_mcp-0.3.0/src/kataster_mcp/geo.py +90 -0
- kataster_mcp-0.3.0/src/kataster_mcp/http.py +138 -0
- kataster_mcp-0.3.0/src/kataster_mcp/ku.py +49 -0
- kataster_mcp-0.3.0/src/kataster_mcp/lv.py +307 -0
- kataster_mcp-0.3.0/src/kataster_mcp/mapa.py +96 -0
- kataster_mcp-0.3.0/src/kataster_mcp/navod.py +114 -0
- kataster_mcp-0.3.0/src/kataster_mcp/normalize.py +39 -0
- kataster_mcp-0.3.0/src/kataster_mcp/osoba.py +213 -0
- kataster_mcp-0.3.0/src/kataster_mcp/owners.py +302 -0
- kataster_mcp-0.3.0/src/kataster_mcp/pacing.py +78 -0
- kataster_mcp-0.3.0/src/kataster_mcp/parsers/__init__.py +0 -0
- kataster_mcp-0.3.0/src/kataster_mcp/parsers/lv.py +432 -0
- kataster_mcp-0.3.0/src/kataster_mcp/parsers/lv_pdf.py +246 -0
- kataster_mcp-0.3.0/src/kataster_mcp/parsers/nahlad.py +150 -0
- kataster_mcp-0.3.0/src/kataster_mcp/policy.py +30 -0
- kataster_mcp-0.3.0/src/kataster_mcp/render.py +306 -0
- kataster_mcp-0.3.0/src/kataster_mcp/server.py +249 -0
- kataster_mcp-0.3.0/src/kataster_mcp/skupina.py +106 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/dmr.py +216 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/eskn.py +218 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/eskn_building.py +115 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/kn_wms.py +242 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/layers.py +448 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/pu.py +147 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/wfs.py +99 -0
- kataster_mcp-0.3.0/src/kataster_mcp/sources/zbgis.py +217 -0
- kataster_mcp-0.3.0/src/kataster_mcp/vrstvy.py +114 -0
- kataster_mcp-0.3.0/tests/conftest.py +11 -0
- kataster_mcp-0.3.0/tests/fixtures/dmr_wcs_e503_half.tif +0 -0
- kataster_mcp-0.3.0/tests/fixtures/eskn_detail_c4_anon.html +170 -0
- kataster_mcp-0.3.0/tests/fixtures/eskn_detail_c_anon.html +156 -0
- kataster_mcp-0.3.0/tests/fixtures/eskn_detail_e_anon.html +109 -0
- kataster_mcp-0.3.0/tests/fixtures/eskn_detail_st_anon.html +133 -0
- kataster_mcp-0.3.0/tests/fixtures/eskn_identify_e503.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/kn_gfi_4_tarchy.xml +16 -0
- kataster_mcp-0.3.0/tests/fixtures/kn_gfi_5_13_e503.xml +33 -0
- kataster_mcp-0.3.0/tests/fixtures/kn_gfi_5_v_zu.xml +27 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_nahlad_anon.html +466 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_nahlad_byty_anon.html +259 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_nahlad_stavba_anon.html +89 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_pdf_runs_anon.json +1382 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_pdf_runs_byty_anon.json +2030 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_pdf_runs_lv1926_anon.json +1418 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_vypis_anon.html +491 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_vypis_anon_generated.pdf +0 -0
- kataster_mcp-0.3.0/tests/fixtures/lv_vypis_byty_anon.html +583 -0
- kataster_mcp-0.3.0/tests/fixtures/mprv_pu_jpu_rozprac.csv +6 -0
- kataster_mcp-0.3.0/tests/fixtures/mprv_pu_jpu_ukoncene.csv +4 -0
- kataster_mcp-0.3.0/tests/fixtures/mprv_pu_kpu_rozprac.csv +4 -0
- kataster_mcp-0.3.0/tests/fixtures/mprv_pu_kpu_ukoncene.csv +6 -0
- kataster_mcp-0.3.0/tests/fixtures/nlc_jprl_c751.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/nlc_lesne_typy_c751.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/wfs_c751_geom.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/wfs_e503.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/wfs_e599_3_geom.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/zbgis_e599_3_gfi.json +1 -0
- kataster_mcp-0.3.0/tests/fixtures/zbgis_e599_3_map.png +0 -0
- kataster_mcp-0.3.0/tests/test_buildings.py +198 -0
- kataster_mcp-0.3.0/tests/test_byty.py +136 -0
- kataster_mcp-0.3.0/tests/test_cli.py +154 -0
- kataster_mcp-0.3.0/tests/test_core.py +226 -0
- kataster_mcp-0.3.0/tests/test_dmr.py +120 -0
- kataster_mcp-0.3.0/tests/test_kn_wms.py +168 -0
- kataster_mcp-0.3.0/tests/test_layers.py +108 -0
- kataster_mcp-0.3.0/tests/test_les.py +71 -0
- kataster_mcp-0.3.0/tests/test_limits_dir.py +109 -0
- kataster_mcp-0.3.0/tests/test_lv_integrity.py +149 -0
- kataster_mcp-0.3.0/tests/test_lv_pdf.py +87 -0
- kataster_mcp-0.3.0/tests/test_mapa.py +120 -0
- kataster_mcp-0.3.0/tests/test_osoba.py +283 -0
- kataster_mcp-0.3.0/tests/test_owners.py +277 -0
- kataster_mcp-0.3.0/tests/test_pacing.py +182 -0
- kataster_mcp-0.3.0/tests/test_platform.py +64 -0
- kataster_mcp-0.3.0/tests/test_pu.py +111 -0
- kataster_mcp-0.3.0/tests/test_purge.py +75 -0
- kataster_mcp-0.3.0/tests/test_render.py +96 -0
- kataster_mcp-0.3.0/tests/test_siete.py +99 -0
- kataster_mcp-0.3.0/tests/test_stdio.py +40 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# venv, cache, build
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.pyc
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# lokálne dáta a cache (osobné údaje!)
|
|
12
|
+
/data/
|
|
13
|
+
*.sqlite
|
|
14
|
+
*.sqlite-*
|
|
15
|
+
snapshots/
|
|
16
|
+
*.log
|
|
17
|
+
|
|
18
|
+
# zálohy
|
|
19
|
+
*.bak*
|
|
20
|
+
*.pred-*
|
|
21
|
+
|
|
22
|
+
# lokálne nastavenia klienta
|
|
23
|
+
.claude/settings.local.json
|
|
24
|
+
|
|
25
|
+
# lokálny pin z update-mcp-venvs.sh (verejne platí requires-python)
|
|
26
|
+
.python-version
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Branislav Klocok
|
|
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,173 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kataster-mcp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: MCP server for the Slovak land registry (kataster nehnuteľností SR): parcels, LV, owners, soil, LPIS, floods, nature protection, maps
|
|
5
|
+
Project-URL: Homepage, https://github.com/braklo/kataster-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/braklo/kataster-mcp/issues
|
|
7
|
+
Author-email: Branislav Klocok <branislav.klocok@orava.sk>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cadastre,kataster,mcp,slovakia
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Natural Language :: Slovak
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: mcp[cli]<3,>=2.3
|
|
24
|
+
Requires-Dist: pillow>=12.3.0
|
|
25
|
+
Requires-Dist: pypdf>=6.19.0
|
|
26
|
+
Requires-Dist: shapely>=2.2.0
|
|
27
|
+
Requires-Dist: tzdata>=2026.5; sys_platform == 'win32'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# kataster-mcp
|
|
31
|
+
|
|
32
|
+
MCP server pre kataster nehnuteľností Slovenskej republiky. Umožňuje AI asistentovi alebo jazykovému modelu
|
|
33
|
+
v ľubovoľnom klientovi s podporou [Model Context Protocol](https://modelcontextprotocol.io) zistiť o konkrétnom pozemku
|
|
34
|
+
čo najviac z verejných zdrojov: hranice a výmeru parcely, list vlastníctva a vlastníkov,
|
|
35
|
+
bonitu pôdy, úradnú hodnotu, LPIS, záplavové územia, ochranu prírody, les, sklon terénu, siete a mapu.
|
|
36
|
+
|
|
37
|
+
*English: an MCP server for the Slovak land registry. An English README is in preparation.*
|
|
38
|
+
|
|
39
|
+
Údaje majú informatívny charakter. Nie sú výpisom z listu vlastníctva a nedajú sa použiť na právne úkony.
|
|
40
|
+
|
|
41
|
+
## Čo vie
|
|
42
|
+
|
|
43
|
+
| Nástroj | Čo vráti |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `kataster_ku` | katastrálne územie podľa názvu alebo kódu (obec, okres) |
|
|
46
|
+
| `kataster_parcela` | parcelu registra C alebo E podľa čísla alebo súradníc: výmera, hranica, referenčný bod |
|
|
47
|
+
| `kataster_vlastnici` | vlastníkov a podiely jednej zadanej parcely a číslo listu vlastníctva; na požiadanie (`detail_stavieb`) aj detail stavby na nej: LV stavby, druh a vlastníkov (každá stavba je ďalší dotaz do denného limitu; pri bytovom dome vlastníkov bytov nevracia, namiesto toho navedie, ako si uložiť a naimportovať list vlastníctva domu) |
|
|
48
|
+
| `kataster_vrstvy` | tematické údaje o parcele (zoznam nižšie), pri plošných vrstvách aj podiel na výmere |
|
|
49
|
+
| `kataster_skupina` | rozbor viacerých parciel: celková výmera, spoločné hranice, súvislé celky, susedia |
|
|
50
|
+
| `kataster_mapa` | mapu parcely ako PNG, HTML (Leaflet), GeoJSON alebo KML, bez osobných údajov |
|
|
51
|
+
| `kataster_lv` | odkaz na výpis z listu vlastníctva na portáli ESKN (výpis si otvoríte sami) |
|
|
52
|
+
| `kataster_osoba_navod` | postup krok za krokom, ako si v oficiálnom vyhľadávaní portálu ESKN nájsť listy vlastníctva osoby a uložiť ich náhľady (server pri tom na portál nevolá) |
|
|
53
|
+
| `kataster_lv_import` | načítanie výpisu LV alebo náhľadu LV, ktorý ste si uložili (HTML alebo PDF); aj celý priečinok naraz; pri bytovom dome načíta aj byty a nebytové priestory s vchodom, poschodím, podielom a vlastníkmi |
|
|
54
|
+
| `kataster_osoba_prehlad` | prehľad parciel a podielov jednej osoby zložený z náhľadov LV, ktoré ste si uložili; bez dotazu na portál |
|
|
55
|
+
| `kataster_status` | verziu, limity a dnešné využitie kvóty |
|
|
56
|
+
|
|
57
|
+
Vrstvy v `kataster_vrstvy`: bonitovaná pôdnoekologická jednotka (BPEJ), úradná hodnota pôdy, LPIS, užívateľ pôdy,
|
|
58
|
+
záplavy, chránené územia, nadmorská výška, sklon terénu, zastavané územie obce, zakreslené ťarchy, pozemkové úpravy
|
|
59
|
+
v katastrálnom území, les a siete (cesty, železnice, elektrické vedenia).
|
|
60
|
+
|
|
61
|
+
Každá odpoveď uvádza zdroj, čas a to, či údaj prišiel z lokálnej cache. Neoficiálne rozhrania sú označené.
|
|
62
|
+
|
|
63
|
+
## Čo zámerne nerobí
|
|
64
|
+
|
|
65
|
+
- **Sám nehľadá podľa osoby.** Vlastníkov zistí len pre parcelu, ktorú zadáte. Ak chcete parcely konkrétnej osoby, vyhľadáte
|
|
66
|
+
ju sami v oficiálnom vyhľadávaní portálu ESKN („Fyzické osoby“), server vás tým prevedie a uložené náhľady listov
|
|
67
|
+
vlastníctva potom načíta a zloží do prehľadu.
|
|
68
|
+
- **Nenahrádza súpis nehnuteľností podľa § 69 ods. 9 zákona č. 162/1995 Z. z.**, ktorý vydáva úrad len samotnej osobe.
|
|
69
|
+
Prehľad zložený z náhľadov je informatívny.
|
|
70
|
+
- **Neprechádza celé katastrálne územia** kvôli vlastníkom.
|
|
71
|
+
- **Neobchádza CAPTCHA.** Keď portál CAPTCHA vyžiada, alebo keď je denná kvóta vyčerpaná, server vám poradí presný ručný
|
|
72
|
+
postup v prehliadači a uložený výpis či náhľad potom načíta.
|
|
73
|
+
- **Nezaťažuje portál.** Medzi dotazmi na kataster.skgeodesy.sk čaká aspoň 3 sekundy a nikdy sa nepýta paralelne.
|
|
74
|
+
Vlastníkov zistí najviac pre 50 parciel denne. Limity platia spoločne pre všetky spustené procesy (viac MCP klientov
|
|
75
|
+
aj príkazový riadok) a nedajú sa vypnúť nastavením ani zmenou dátového adresára.
|
|
76
|
+
|
|
77
|
+
## Inštalácia
|
|
78
|
+
|
|
79
|
+
Potrebujete [uv](https://docs.astral.sh/uv/getting-started/installation/) (Linux, macOS aj Windows). Server sa spúšťa
|
|
80
|
+
ako lokálny proces cez stdio, takže ho pridáte do každého MCP klienta rovnako: príkaz `uvx` s argumentmi
|
|
81
|
+
`--from git+https://github.com/braklo/kataster-mcp kataster-mcp`. Väčšina klientov to zapisuje do JSON konfigurácie
|
|
82
|
+
v tomto tvare (presné miesto a názov kľúča nájdete v dokumentácii svojho klienta):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"kataster": {
|
|
88
|
+
"command": "uvx",
|
|
89
|
+
"args": ["--from", "git+https://github.com/braklo/kataster-mcp", "kataster-mcp"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Napríklad v Claude Code jedným príkazom:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
claude mcp add kataster -- uvx --from git+https://github.com/braklo/kataster-mcp kataster-mcp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Potom sa stačí opýtať, napríklad: *„Čo je za pozemok E 503 v katastrálnom území Vyšný Kubín a kto ho vlastní?“*
|
|
102
|
+
|
|
103
|
+
## Príkazový riadok
|
|
104
|
+
|
|
105
|
+
Balík obsahuje aj príkaz `sk-kataster`. Bez inštalácie ho spustíte cez
|
|
106
|
+
`uvx --from git+https://github.com/braklo/kataster-mcp sk-kataster`, výstup je JSON.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
sk-kataster doctor
|
|
110
|
+
sk-kataster ku resolve 'Vyšný Kubín'
|
|
111
|
+
sk-kataster lv inspect ~/Stiahnuté/vypis.html
|
|
112
|
+
sk-kataster lv import ~/Stiahnuté/vypis.html
|
|
113
|
+
sk-kataster person overview --surname Novák --first-name Ján
|
|
114
|
+
sk-kataster tools
|
|
115
|
+
sk-kataster call kataster_parcela '{"ku": "Vyšný Kubín", "cislo": "E 503"}'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`doctor` ukáže verziu, adresáre a dnešné využitie kvóty. `ku`, `lv url`, `lv inspect`, `lv import` a
|
|
119
|
+
`person overview` na portál nevolajú, `lv inspect` ani `lv import --dry-run` nič neukladajú. Výpis LV vracajú
|
|
120
|
+
predvolene v súhrne; parcely a vlastníkov pridá `--full`. `tools` vypíše nástroje servera a `call` jeden z nich
|
|
121
|
+
spustí. Ak nástroj ide na portál, platí rovnaká pauza a denný limit ako pre MCP server.
|
|
122
|
+
|
|
123
|
+
Dátový adresár (cache a uložené LV) presuniete premennou `KATASTER_DATA_DIR`, v príkazovom riadku aj voľbou
|
|
124
|
+
`--data-dir`. Stav limitov zostáva na pevnom mieste.
|
|
125
|
+
|
|
126
|
+
Pre AI klientov, ktorí pracujú cez príkazový riadok, je v repozitári voliteľný návod
|
|
127
|
+
[`skills/kataster/SKILL.md`](skills/kataster/SKILL.md).
|
|
128
|
+
|
|
129
|
+
## Osobné údaje
|
|
130
|
+
|
|
131
|
+
Mená, dátumy narodenia a adresy vlastníkov sa ukladajú len lokálne, do súboru `osoby.sqlite` v dátovom adresári
|
|
132
|
+
servera, a po 7 dňoch sa pri ďalšom dotaze načítajú znova. Dátový adresár (presnú cestu vypíše `kataster_status`):
|
|
133
|
+
|
|
134
|
+
| Systém | Adresár | Ochrana |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| Linux | `~/.local/share/kataster-mcp` (alebo `$XDG_DATA_HOME/kataster-mcp`) | práva 0700 / 0600 |
|
|
137
|
+
| macOS | `~/Library/Application Support/kataster-mcp` | práva 0700 / 0600 |
|
|
138
|
+
| Windows | `%LOCALAPPDATA%\kataster-mcp` | prístup len pre používateľa (predvolené nastavenie Windows) |
|
|
139
|
+
|
|
140
|
+
Premenná `KATASTER_DATA_DIR` má prednosť pred týmito umiestneniami na všetkých troch systémoch a platí pre MCP server aj
|
|
141
|
+
príkazový riadok.
|
|
142
|
+
|
|
143
|
+
Denný limit dotazov na vlastníkov, pauza medzi dotazmi a blokácia po CAPTCHA sa evidujú zvlášť, v pevnom adresári, ktorý
|
|
144
|
+
nemení žiadna premenná prostredia ani presun dátového adresára (presnú cestu vypíše `kataster_status` ako `limity_dir`):
|
|
145
|
+
|
|
146
|
+
| Systém | Adresár limitov | Ochrana |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| Linux | `~/.local/state/kataster-mcp` | práva 0700 / 0600 |
|
|
149
|
+
| macOS | `~/Library/Application Support/kataster-mcp-limits` | práva 0700 / 0600 |
|
|
150
|
+
| Windows | `%USERPROFILE%\AppData\Local\kataster-mcp-limits` | prístup len pre používateľa (predvolené nastavenie Windows) |
|
|
151
|
+
|
|
152
|
+
Server ich neposiela nikam inam než do odpovede vášmu AI asistentovi; ak model beží v cloude, dostane ich aj jeho
|
|
153
|
+
poskytovateľ. Za to, ako ich použijete, zodpovedáte vy (GDPR, katastrálny zákon).
|
|
154
|
+
|
|
155
|
+
## Zdroje dát
|
|
156
|
+
|
|
157
|
+
Úrad geodézie, kartografie a katastra SR (INSPIRE, ESKN, ZBGIS, výškový model), Výskumný ústav pôdoznalectva a ochrany
|
|
158
|
+
pôdy, Národné lesnícke centrum, Ministerstvo pôdohospodárstva a rozvoja vidieka SR, Slovenský vodohospodársky podnik,
|
|
159
|
+
Štátna ochrana prírody SR. Podmienky ich použitia platia aj pri použití cez tento server; zdroj uvádzajte.
|
|
160
|
+
|
|
161
|
+
## Stav
|
|
162
|
+
|
|
163
|
+
Otestované s Claude Code na Linuxe. Na Windows a macOS prechádzajú automatické testy, v bežnom používaní
|
|
164
|
+
to zatiaľ overené nie je; skúsenosti uvítame v issues.
|
|
165
|
+
|
|
166
|
+
## Chyby a návrhy
|
|
167
|
+
|
|
168
|
+
[Issues](https://github.com/braklo/kataster-mcp/issues), po slovensky aj po anglicky. Do issue nedávajte mená
|
|
169
|
+
vlastníkov ani výpisy z listu vlastníctva.
|
|
170
|
+
|
|
171
|
+
## Licencia
|
|
172
|
+
|
|
173
|
+
Kód: [MIT](LICENSE). Licencia kódu sa nevzťahuje na údaje, ktoré server sprostredkúva.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# kataster-mcp
|
|
2
|
+
|
|
3
|
+
MCP server pre kataster nehnuteľností Slovenskej republiky. Umožňuje AI asistentovi alebo jazykovému modelu
|
|
4
|
+
v ľubovoľnom klientovi s podporou [Model Context Protocol](https://modelcontextprotocol.io) zistiť o konkrétnom pozemku
|
|
5
|
+
čo najviac z verejných zdrojov: hranice a výmeru parcely, list vlastníctva a vlastníkov,
|
|
6
|
+
bonitu pôdy, úradnú hodnotu, LPIS, záplavové územia, ochranu prírody, les, sklon terénu, siete a mapu.
|
|
7
|
+
|
|
8
|
+
*English: an MCP server for the Slovak land registry. An English README is in preparation.*
|
|
9
|
+
|
|
10
|
+
Údaje majú informatívny charakter. Nie sú výpisom z listu vlastníctva a nedajú sa použiť na právne úkony.
|
|
11
|
+
|
|
12
|
+
## Čo vie
|
|
13
|
+
|
|
14
|
+
| Nástroj | Čo vráti |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `kataster_ku` | katastrálne územie podľa názvu alebo kódu (obec, okres) |
|
|
17
|
+
| `kataster_parcela` | parcelu registra C alebo E podľa čísla alebo súradníc: výmera, hranica, referenčný bod |
|
|
18
|
+
| `kataster_vlastnici` | vlastníkov a podiely jednej zadanej parcely a číslo listu vlastníctva; na požiadanie (`detail_stavieb`) aj detail stavby na nej: LV stavby, druh a vlastníkov (každá stavba je ďalší dotaz do denného limitu; pri bytovom dome vlastníkov bytov nevracia, namiesto toho navedie, ako si uložiť a naimportovať list vlastníctva domu) |
|
|
19
|
+
| `kataster_vrstvy` | tematické údaje o parcele (zoznam nižšie), pri plošných vrstvách aj podiel na výmere |
|
|
20
|
+
| `kataster_skupina` | rozbor viacerých parciel: celková výmera, spoločné hranice, súvislé celky, susedia |
|
|
21
|
+
| `kataster_mapa` | mapu parcely ako PNG, HTML (Leaflet), GeoJSON alebo KML, bez osobných údajov |
|
|
22
|
+
| `kataster_lv` | odkaz na výpis z listu vlastníctva na portáli ESKN (výpis si otvoríte sami) |
|
|
23
|
+
| `kataster_osoba_navod` | postup krok za krokom, ako si v oficiálnom vyhľadávaní portálu ESKN nájsť listy vlastníctva osoby a uložiť ich náhľady (server pri tom na portál nevolá) |
|
|
24
|
+
| `kataster_lv_import` | načítanie výpisu LV alebo náhľadu LV, ktorý ste si uložili (HTML alebo PDF); aj celý priečinok naraz; pri bytovom dome načíta aj byty a nebytové priestory s vchodom, poschodím, podielom a vlastníkmi |
|
|
25
|
+
| `kataster_osoba_prehlad` | prehľad parciel a podielov jednej osoby zložený z náhľadov LV, ktoré ste si uložili; bez dotazu na portál |
|
|
26
|
+
| `kataster_status` | verziu, limity a dnešné využitie kvóty |
|
|
27
|
+
|
|
28
|
+
Vrstvy v `kataster_vrstvy`: bonitovaná pôdnoekologická jednotka (BPEJ), úradná hodnota pôdy, LPIS, užívateľ pôdy,
|
|
29
|
+
záplavy, chránené územia, nadmorská výška, sklon terénu, zastavané územie obce, zakreslené ťarchy, pozemkové úpravy
|
|
30
|
+
v katastrálnom území, les a siete (cesty, železnice, elektrické vedenia).
|
|
31
|
+
|
|
32
|
+
Každá odpoveď uvádza zdroj, čas a to, či údaj prišiel z lokálnej cache. Neoficiálne rozhrania sú označené.
|
|
33
|
+
|
|
34
|
+
## Čo zámerne nerobí
|
|
35
|
+
|
|
36
|
+
- **Sám nehľadá podľa osoby.** Vlastníkov zistí len pre parcelu, ktorú zadáte. Ak chcete parcely konkrétnej osoby, vyhľadáte
|
|
37
|
+
ju sami v oficiálnom vyhľadávaní portálu ESKN („Fyzické osoby“), server vás tým prevedie a uložené náhľady listov
|
|
38
|
+
vlastníctva potom načíta a zloží do prehľadu.
|
|
39
|
+
- **Nenahrádza súpis nehnuteľností podľa § 69 ods. 9 zákona č. 162/1995 Z. z.**, ktorý vydáva úrad len samotnej osobe.
|
|
40
|
+
Prehľad zložený z náhľadov je informatívny.
|
|
41
|
+
- **Neprechádza celé katastrálne územia** kvôli vlastníkom.
|
|
42
|
+
- **Neobchádza CAPTCHA.** Keď portál CAPTCHA vyžiada, alebo keď je denná kvóta vyčerpaná, server vám poradí presný ručný
|
|
43
|
+
postup v prehliadači a uložený výpis či náhľad potom načíta.
|
|
44
|
+
- **Nezaťažuje portál.** Medzi dotazmi na kataster.skgeodesy.sk čaká aspoň 3 sekundy a nikdy sa nepýta paralelne.
|
|
45
|
+
Vlastníkov zistí najviac pre 50 parciel denne. Limity platia spoločne pre všetky spustené procesy (viac MCP klientov
|
|
46
|
+
aj príkazový riadok) a nedajú sa vypnúť nastavením ani zmenou dátového adresára.
|
|
47
|
+
|
|
48
|
+
## Inštalácia
|
|
49
|
+
|
|
50
|
+
Potrebujete [uv](https://docs.astral.sh/uv/getting-started/installation/) (Linux, macOS aj Windows). Server sa spúšťa
|
|
51
|
+
ako lokálny proces cez stdio, takže ho pridáte do každého MCP klienta rovnako: príkaz `uvx` s argumentmi
|
|
52
|
+
`--from git+https://github.com/braklo/kataster-mcp kataster-mcp`. Väčšina klientov to zapisuje do JSON konfigurácie
|
|
53
|
+
v tomto tvare (presné miesto a názov kľúča nájdete v dokumentácii svojho klienta):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"kataster": {
|
|
59
|
+
"command": "uvx",
|
|
60
|
+
"args": ["--from", "git+https://github.com/braklo/kataster-mcp", "kataster-mcp"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Napríklad v Claude Code jedným príkazom:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add kataster -- uvx --from git+https://github.com/braklo/kataster-mcp kataster-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Potom sa stačí opýtať, napríklad: *„Čo je za pozemok E 503 v katastrálnom území Vyšný Kubín a kto ho vlastní?“*
|
|
73
|
+
|
|
74
|
+
## Príkazový riadok
|
|
75
|
+
|
|
76
|
+
Balík obsahuje aj príkaz `sk-kataster`. Bez inštalácie ho spustíte cez
|
|
77
|
+
`uvx --from git+https://github.com/braklo/kataster-mcp sk-kataster`, výstup je JSON.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
sk-kataster doctor
|
|
81
|
+
sk-kataster ku resolve 'Vyšný Kubín'
|
|
82
|
+
sk-kataster lv inspect ~/Stiahnuté/vypis.html
|
|
83
|
+
sk-kataster lv import ~/Stiahnuté/vypis.html
|
|
84
|
+
sk-kataster person overview --surname Novák --first-name Ján
|
|
85
|
+
sk-kataster tools
|
|
86
|
+
sk-kataster call kataster_parcela '{"ku": "Vyšný Kubín", "cislo": "E 503"}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`doctor` ukáže verziu, adresáre a dnešné využitie kvóty. `ku`, `lv url`, `lv inspect`, `lv import` a
|
|
90
|
+
`person overview` na portál nevolajú, `lv inspect` ani `lv import --dry-run` nič neukladajú. Výpis LV vracajú
|
|
91
|
+
predvolene v súhrne; parcely a vlastníkov pridá `--full`. `tools` vypíše nástroje servera a `call` jeden z nich
|
|
92
|
+
spustí. Ak nástroj ide na portál, platí rovnaká pauza a denný limit ako pre MCP server.
|
|
93
|
+
|
|
94
|
+
Dátový adresár (cache a uložené LV) presuniete premennou `KATASTER_DATA_DIR`, v príkazovom riadku aj voľbou
|
|
95
|
+
`--data-dir`. Stav limitov zostáva na pevnom mieste.
|
|
96
|
+
|
|
97
|
+
Pre AI klientov, ktorí pracujú cez príkazový riadok, je v repozitári voliteľný návod
|
|
98
|
+
[`skills/kataster/SKILL.md`](skills/kataster/SKILL.md).
|
|
99
|
+
|
|
100
|
+
## Osobné údaje
|
|
101
|
+
|
|
102
|
+
Mená, dátumy narodenia a adresy vlastníkov sa ukladajú len lokálne, do súboru `osoby.sqlite` v dátovom adresári
|
|
103
|
+
servera, a po 7 dňoch sa pri ďalšom dotaze načítajú znova. Dátový adresár (presnú cestu vypíše `kataster_status`):
|
|
104
|
+
|
|
105
|
+
| Systém | Adresár | Ochrana |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Linux | `~/.local/share/kataster-mcp` (alebo `$XDG_DATA_HOME/kataster-mcp`) | práva 0700 / 0600 |
|
|
108
|
+
| macOS | `~/Library/Application Support/kataster-mcp` | práva 0700 / 0600 |
|
|
109
|
+
| Windows | `%LOCALAPPDATA%\kataster-mcp` | prístup len pre používateľa (predvolené nastavenie Windows) |
|
|
110
|
+
|
|
111
|
+
Premenná `KATASTER_DATA_DIR` má prednosť pred týmito umiestneniami na všetkých troch systémoch a platí pre MCP server aj
|
|
112
|
+
príkazový riadok.
|
|
113
|
+
|
|
114
|
+
Denný limit dotazov na vlastníkov, pauza medzi dotazmi a blokácia po CAPTCHA sa evidujú zvlášť, v pevnom adresári, ktorý
|
|
115
|
+
nemení žiadna premenná prostredia ani presun dátového adresára (presnú cestu vypíše `kataster_status` ako `limity_dir`):
|
|
116
|
+
|
|
117
|
+
| Systém | Adresár limitov | Ochrana |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Linux | `~/.local/state/kataster-mcp` | práva 0700 / 0600 |
|
|
120
|
+
| macOS | `~/Library/Application Support/kataster-mcp-limits` | práva 0700 / 0600 |
|
|
121
|
+
| Windows | `%USERPROFILE%\AppData\Local\kataster-mcp-limits` | prístup len pre používateľa (predvolené nastavenie Windows) |
|
|
122
|
+
|
|
123
|
+
Server ich neposiela nikam inam než do odpovede vášmu AI asistentovi; ak model beží v cloude, dostane ich aj jeho
|
|
124
|
+
poskytovateľ. Za to, ako ich použijete, zodpovedáte vy (GDPR, katastrálny zákon).
|
|
125
|
+
|
|
126
|
+
## Zdroje dát
|
|
127
|
+
|
|
128
|
+
Úrad geodézie, kartografie a katastra SR (INSPIRE, ESKN, ZBGIS, výškový model), Výskumný ústav pôdoznalectva a ochrany
|
|
129
|
+
pôdy, Národné lesnícke centrum, Ministerstvo pôdohospodárstva a rozvoja vidieka SR, Slovenský vodohospodársky podnik,
|
|
130
|
+
Štátna ochrana prírody SR. Podmienky ich použitia platia aj pri použití cez tento server; zdroj uvádzajte.
|
|
131
|
+
|
|
132
|
+
## Stav
|
|
133
|
+
|
|
134
|
+
Otestované s Claude Code na Linuxe. Na Windows a macOS prechádzajú automatické testy, v bežnom používaní
|
|
135
|
+
to zatiaľ overené nie je; skúsenosti uvítame v issues.
|
|
136
|
+
|
|
137
|
+
## Chyby a návrhy
|
|
138
|
+
|
|
139
|
+
[Issues](https://github.com/braklo/kataster-mcp/issues), po slovensky aj po anglicky. Do issue nedávajte mená
|
|
140
|
+
vlastníkov ani výpisy z listu vlastníctva.
|
|
141
|
+
|
|
142
|
+
## Licencia
|
|
143
|
+
|
|
144
|
+
Kód: [MIT](LICENSE). Licencia kódu sa nevzťahuje na údaje, ktoré server sprostredkúva.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kataster-mcp"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "MCP server for the Slovak land registry (kataster nehnuteľností SR): parcels, LV, owners, soil, LPIS, floods, nature protection, maps"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Branislav Klocok", email = "branislav.klocok@orava.sk" }]
|
|
10
|
+
keywords = ["kataster", "slovakia", "cadastre", "mcp"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: End Users/Desktop",
|
|
15
|
+
"Natural Language :: Slovak",
|
|
16
|
+
"Natural Language :: English",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp[cli]>=2.3,<3",
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"shapely>=2.2.0",
|
|
27
|
+
"pillow>=12.3.0",
|
|
28
|
+
"pypdf>=6.19.0",
|
|
29
|
+
"tzdata>=2026.5 ; sys_platform == 'win32'",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/braklo/kataster-mcp"
|
|
34
|
+
Issues = "https://github.com/braklo/kataster-mcp/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
kataster-mcp = "kataster_mcp.server:main"
|
|
38
|
+
sk-kataster = "kataster_mcp.cli:main"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = ["pytest>=8"]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
# Allowlist for the source distribution.
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
only-include = ["src", "tests", "LICENSE", "README.md", "pyproject.toml"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/kataster_mcp"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
markers = ["live: hits real public endpoints (run by hand, human pace)"]
|
|
57
|
+
addopts = "-m 'not live'"
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Local SQLite cache and the daily owners quota.
|
|
2
|
+
|
|
3
|
+
Two files under the data dir (0700):
|
|
4
|
+
- cache.sqlite public data (geometry, layers)
|
|
5
|
+
- osoby.sqlite personal data (owners, land users), always 0600
|
|
6
|
+
and the state of the limits in a fixed per-user directory (default_limits_dir):
|
|
7
|
+
- stav.sqlite the daily owners quota and a portal block, 0600
|
|
8
|
+
- tempo/ one file per host with the time of its last request (pacing.Pacer)
|
|
9
|
+
|
|
10
|
+
Every entry is keyed by (source, parser version, key). The parser version is a hash
|
|
11
|
+
of the parser's source code, so fixing a parser silently invalidates old entries.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import inspect
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sqlite3
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from zoneinfo import ZoneInfo
|
|
24
|
+
|
|
25
|
+
# Owner-only file permissions exist only on POSIX; on Windows chmod just toggles the read-only flag and the
|
|
26
|
+
# data directory under %LOCALAPPDATA% is protected by the user profile's ACL instead.
|
|
27
|
+
POSIX = os.name == "posix"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
TZ = ZoneInfo("Europe/Bratislava") # on Windows from the tzdata package (a dependency there)
|
|
31
|
+
|
|
32
|
+
DAY = 86400
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_data_dir(platform: str | None = None, env: dict | None = None) -> Path:
|
|
36
|
+
"""Per-user data directory: Windows %LOCALAPPDATA%, macOS ~/Library/Application Support,
|
|
37
|
+
elsewhere $XDG_DATA_HOME or ~/.local/share."""
|
|
38
|
+
platform = platform or sys.platform
|
|
39
|
+
env = os.environ if env is None else env
|
|
40
|
+
if platform == "win32":
|
|
41
|
+
base = env.get("LOCALAPPDATA") or os.path.join(Path.home(), "AppData", "Local")
|
|
42
|
+
elif platform == "darwin":
|
|
43
|
+
base = os.path.join(Path.home(), "Library", "Application Support")
|
|
44
|
+
else:
|
|
45
|
+
base = env.get("XDG_DATA_HOME") or os.path.join(Path.home(), ".local", "share")
|
|
46
|
+
return Path(base) / "kataster-mcp"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def configured_data_dir() -> Path:
|
|
50
|
+
"""Data directory (cache, imports, snapshots): KATASTER_DATA_DIR when set, else default_data_dir().
|
|
51
|
+
It never moves the limits (default_limits_dir)."""
|
|
52
|
+
configured = os.environ.get("KATASTER_DATA_DIR")
|
|
53
|
+
return Path(configured).expanduser() if configured else default_data_dir()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def default_limits_dir(platform: str | None = None) -> Path:
|
|
57
|
+
"""Fixed per-user directory for the state of the limits (daily owners quota, pause across processes, portal
|
|
58
|
+
block). Unlike the data directory it follows no environment variable or configuration:
|
|
59
|
+
another data directory or XDG_DATA_HOME must not mean a new quota. Linux and others ~/.local/state/kataster-mcp,
|
|
60
|
+
macOS ~/Library/Application Support/kataster-mcp-limits, Windows <profile>\\AppData\\Local\\kataster-mcp-limits."""
|
|
61
|
+
platform = platform or sys.platform
|
|
62
|
+
home = _home()
|
|
63
|
+
if platform == "win32":
|
|
64
|
+
return home / "AppData" / "Local" / "kataster-mcp-limits"
|
|
65
|
+
if platform == "darwin":
|
|
66
|
+
return home / "Library" / "Application Support" / "kataster-mcp-limits"
|
|
67
|
+
return home / ".local" / "state" / "kataster-mcp"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _home() -> Path:
|
|
71
|
+
"""Home directory from the account database on POSIX (not $HOME), the user profile on Windows."""
|
|
72
|
+
try:
|
|
73
|
+
import pwd
|
|
74
|
+
return Path(pwd.getpwuid(os.getuid()).pw_dir)
|
|
75
|
+
except (ImportError, KeyError, AttributeError):
|
|
76
|
+
return Path.home()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# os.open() on Windows opens the C runtime descriptor in text mode unless O_BINARY is given (0 on POSIX).
|
|
80
|
+
O_BINARY = getattr(os, "O_BINARY", 0)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def restrict(path, mode: int) -> None:
|
|
84
|
+
"""chmod where it means something (POSIX); a no-op elsewhere, never an error."""
|
|
85
|
+
if POSIX:
|
|
86
|
+
os.chmod(path, mode)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def protection() -> str:
|
|
90
|
+
return ("owner only (0700 directory, 0600 personal-data files)" if POSIX
|
|
91
|
+
else "user profile ACL of the data directory (no POSIX permissions on this OS)")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parser_version(*funcs) -> str:
|
|
95
|
+
h = hashlib.sha256()
|
|
96
|
+
for f in funcs:
|
|
97
|
+
h.update(inspect.getsource(f).encode())
|
|
98
|
+
return h.hexdigest()[:12]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def today() -> str:
|
|
102
|
+
return datetime.now(TZ).date().isoformat()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def next_day_iso() -> str:
|
|
106
|
+
"""Midnight starting the next calendar day in Bratislava (when the daily quota resets)."""
|
|
107
|
+
from datetime import timedelta
|
|
108
|
+
|
|
109
|
+
d = datetime.now(TZ).date() + timedelta(days=1)
|
|
110
|
+
return datetime(d.year, d.month, d.day, tzinfo=TZ).isoformat(timespec="seconds")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ensure_private_file(path: Path) -> None:
|
|
114
|
+
if not path.exists():
|
|
115
|
+
os.close(os.open(path, os.O_CREAT | os.O_WRONLY | O_BINARY, 0o600))
|
|
116
|
+
restrict(path, 0o600)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
PERSONAL_TTL = 7 * DAY # personal data (owners, imported LVs, page snapshots) is deleted after this
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def purge_files(directory: Path, max_age_s: float) -> int:
|
|
123
|
+
"""Delete files older than max_age_s (by modification time); returns how many."""
|
|
124
|
+
if not directory.is_dir():
|
|
125
|
+
return 0
|
|
126
|
+
limit = time.time() - max_age_s
|
|
127
|
+
n = 0
|
|
128
|
+
for f in directory.iterdir():
|
|
129
|
+
try:
|
|
130
|
+
if f.is_file() and f.stat().st_mtime < limit:
|
|
131
|
+
f.unlink()
|
|
132
|
+
n += 1
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
return n
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Cache:
|
|
139
|
+
"""SQLite cache. With max_age_s (personal data) rows older than that are physically deleted when the
|
|
140
|
+
cache opens (then VACUUM) and after every write; freed pages are overwritten (secure_delete)."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, path: Path, *, private: bool, max_age_s: float | None = None):
|
|
143
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
restrict(path.parent, 0o700)
|
|
145
|
+
if private:
|
|
146
|
+
_ensure_private_file(path)
|
|
147
|
+
self.path = path
|
|
148
|
+
self.max_age_s = max_age_s
|
|
149
|
+
self.db = sqlite3.connect(path, check_same_thread=False)
|
|
150
|
+
if max_age_s is not None:
|
|
151
|
+
self.db.execute("PRAGMA secure_delete=ON")
|
|
152
|
+
self.db.execute(
|
|
153
|
+
"CREATE TABLE IF NOT EXISTS cache (zdroj TEXT, ver TEXT, kluc TEXT, ulozene REAL, data TEXT,"
|
|
154
|
+
" PRIMARY KEY (zdroj, ver, kluc))"
|
|
155
|
+
)
|
|
156
|
+
self.db.commit()
|
|
157
|
+
if max_age_s is not None and self.purge():
|
|
158
|
+
self.db.execute("VACUUM")
|
|
159
|
+
|
|
160
|
+
def purge(self) -> int:
|
|
161
|
+
"""Delete rows older than max_age_s, whatever their source or version; returns how many."""
|
|
162
|
+
if self.max_age_s is None:
|
|
163
|
+
return 0
|
|
164
|
+
n = self.db.execute("DELETE FROM cache WHERE ulozene < ?", (time.time() - self.max_age_s,)).rowcount
|
|
165
|
+
self.db.commit()
|
|
166
|
+
return n
|
|
167
|
+
|
|
168
|
+
def get(self, zdroj: str, ver: str, kluc: str, ttl_s: float | None):
|
|
169
|
+
row = self.db.execute(
|
|
170
|
+
"SELECT data, ulozene FROM cache WHERE zdroj=? AND ver=? AND kluc=?", (zdroj, ver, kluc)
|
|
171
|
+
).fetchone()
|
|
172
|
+
if row is None:
|
|
173
|
+
return None
|
|
174
|
+
data, ulozene = row
|
|
175
|
+
if ttl_s is not None and time.time() - ulozene > ttl_s:
|
|
176
|
+
return None
|
|
177
|
+
return json.loads(data), ulozene
|
|
178
|
+
|
|
179
|
+
def put(self, zdroj: str, ver: str, kluc: str, data) -> float:
|
|
180
|
+
ts = time.time()
|
|
181
|
+
self.db.execute(
|
|
182
|
+
"INSERT OR REPLACE INTO cache VALUES (?, ?, ?, ?, ?)",
|
|
183
|
+
(zdroj, ver, kluc, ts, json.dumps(data, ensure_ascii=False)),
|
|
184
|
+
)
|
|
185
|
+
self.db.commit()
|
|
186
|
+
self.purge()
|
|
187
|
+
return ts
|
|
188
|
+
|
|
189
|
+
def stats(self) -> dict:
|
|
190
|
+
rows = self.db.execute("SELECT zdroj, ver, COUNT(*) FROM cache GROUP BY zdroj, ver").fetchall()
|
|
191
|
+
return {f"{z}@{v}": n for z, v, n in rows}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Quota:
|
|
195
|
+
"""Parcels per calendar day whose owners were fetched from the portal."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, db: sqlite3.Connection):
|
|
198
|
+
self.db = db
|
|
199
|
+
db.execute("CREATE TABLE IF NOT EXISTS kvota (den TEXT, parcela TEXT, PRIMARY KEY (den, parcela))")
|
|
200
|
+
db.commit()
|
|
201
|
+
|
|
202
|
+
def merge_today(self, old: Path) -> int:
|
|
203
|
+
"""Add today's rows of a quota kept elsewhere before (the data directory); the union is never laxer."""
|
|
204
|
+
if not old.exists():
|
|
205
|
+
return 0
|
|
206
|
+
src = sqlite3.connect(f"file:{old}?mode=ro", uri=True)
|
|
207
|
+
try:
|
|
208
|
+
rows = src.execute("SELECT den, parcela FROM kvota WHERE den=?", (today(),)).fetchall()
|
|
209
|
+
except sqlite3.OperationalError: # no quota table there
|
|
210
|
+
rows = []
|
|
211
|
+
finally:
|
|
212
|
+
src.close()
|
|
213
|
+
before = self.used()
|
|
214
|
+
self.db.executemany("INSERT OR IGNORE INTO kvota VALUES (?, ?)", rows)
|
|
215
|
+
self.db.commit()
|
|
216
|
+
return self.used() - before
|
|
217
|
+
|
|
218
|
+
def used(self, den: str | None = None) -> int:
|
|
219
|
+
return self.db.execute("SELECT COUNT(*) FROM kvota WHERE den=?", (den or today(),)).fetchone()[0]
|
|
220
|
+
|
|
221
|
+
def counted(self, parcela: str, den: str | None = None) -> bool:
|
|
222
|
+
return self.db.execute(
|
|
223
|
+
"SELECT 1 FROM kvota WHERE den=? AND parcela=?", (den or today(), parcela)
|
|
224
|
+
).fetchone() is not None
|
|
225
|
+
|
|
226
|
+
def record(self, parcela: str, den: str | None = None) -> None:
|
|
227
|
+
self.db.execute("INSERT OR IGNORE INTO kvota VALUES (?, ?)", (den or today(), parcela))
|
|
228
|
+
self.db.commit()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def ts_iso(ts: float) -> str:
|
|
232
|
+
return datetime.fromtimestamp(ts, TZ).isoformat(timespec="seconds")
|