ruian-import 0.1.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.
- ruian_import-0.1.0/.gitignore +7 -0
- ruian_import-0.1.0/PKG-INFO +116 -0
- ruian_import-0.1.0/README.md +90 -0
- ruian_import-0.1.0/pyproject.toml +39 -0
- ruian_import-0.1.0/src/ruian_import/__init__.py +67 -0
- ruian_import-0.1.0/src/ruian_import/_arcgis.py +50 -0
- ruian_import-0.1.0/src/ruian_import/_coordinates.py +45 -0
- ruian_import-0.1.0/src/ruian_import/_http.py +16 -0
- ruian_import-0.1.0/src/ruian_import/_models.py +32 -0
- ruian_import-0.1.0/src/ruian_import/_parse.py +142 -0
- ruian_import-0.1.0/tests/__init__.py +0 -0
- ruian_import-0.1.0/tests/fixtures/UI_OBEC.zip +0 -0
- ruian_import-0.1.0/tests/fixtures/UI_OKRES.zip +0 -0
- ruian_import-0.1.0/tests/fixtures/UI_VUSC.zip +0 -0
- ruian_import-0.1.0/tests/test_parse.py +75 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ruian-import
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Download and parse Czech RUIAN (ČÚZK) data: municipalities, districts, and regions
|
|
5
|
+
Project-URL: Repository, https://gitlab.com/alexandra.tapkova/ruian-import
|
|
6
|
+
Author-email: Alexandra Ťapková <git@ouppy.space>
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: cuzk,czech,gis,municipalities,ruian
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: requests>=2.28
|
|
19
|
+
Provides-Extra: coordinates
|
|
20
|
+
Requires-Dist: pyproj>=3.4; extra == 'coordinates'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pyproj>=3.4; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Requires-Dist: responses; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ruian-import
|
|
28
|
+
|
|
29
|
+
Python knihovna pro stahování a parsování číselníků ČÚZK (RÚIAN): kraje, okresy a obce.
|
|
30
|
+
|
|
31
|
+
## Instalace
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install ruian-import
|
|
35
|
+
# s podporou souřadnic (JTSK -> WGS84):
|
|
36
|
+
pip install "ruian-import[coordinates]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Použití
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from ruian_import import fetch_kraje, fetch_okresy, fetch_obce
|
|
43
|
+
|
|
44
|
+
kraje = fetch_kraje() # list[Kraj]
|
|
45
|
+
okresy = fetch_okresy() # list[Okres]
|
|
46
|
+
obce = fetch_obce() # list[Obec]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Datové třídy
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
@dataclass
|
|
53
|
+
class Kraj:
|
|
54
|
+
kod: str # např. "19"
|
|
55
|
+
nazev: str # např. "Hlavní město Praha"
|
|
56
|
+
nuts_lau: str | None # např. "CZ010"
|
|
57
|
+
zrusen: str | None # datum zrušení "YYYY-MM-DD" nebo None
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Okres:
|
|
61
|
+
kod: str
|
|
62
|
+
nazev: str
|
|
63
|
+
kraj_kod: str
|
|
64
|
+
nuts_lau: str | None
|
|
65
|
+
zrusen: str | None
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Obec:
|
|
69
|
+
kod: str # 6místný kód obce, např. "554979"
|
|
70
|
+
nazev: str
|
|
71
|
+
okres_kod: str
|
|
72
|
+
lat: float | None # WGS84, dostupné jen s [coordinates]
|
|
73
|
+
lon: float | None
|
|
74
|
+
zrusen: str | None
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Zrušené záznamy mají vyplněné pole `zrusen`; platné záznamy mají `zrusen = None`.
|
|
78
|
+
|
|
79
|
+
### Souřadnice obcí
|
|
80
|
+
|
|
81
|
+
`UI_OBEC.csv` souřadnice neobsahuje. Pro doplnění souřadnic použij `fetch_obce_with_coordinates()`, které provede druhý dotaz na ArcGIS REST API ČÚZK a vrátí centroidy polygonů (přesnost ~100 m, jeden HTTP request pro všechny obce):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from ruian_import import fetch_obce_with_coordinates
|
|
85
|
+
|
|
86
|
+
obce = fetch_obce_with_coordinates() # lat/lon vyplněné
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Nebo načti souřadnice samostatně:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from ruian_import import fetch_obec_coordinates
|
|
93
|
+
|
|
94
|
+
coords = fetch_obec_coordinates() # dict[str, tuple[float, float]] - kod -> (lat, lon)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Vlastní URL / lokální soubory
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from ruian_import._parse import parse_obce
|
|
101
|
+
|
|
102
|
+
data = Path("UI_OBEC.zip").read_bytes()
|
|
103
|
+
obce = parse_obce(data)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Zdroje dat
|
|
107
|
+
|
|
108
|
+
Číselníky ČÚZK se aktualizují denně:
|
|
109
|
+
|
|
110
|
+
- **Kraje**: `https://services.cuzk.cz/sestavy/cis/UI_VUSC.zip`
|
|
111
|
+
- **Okresy**: `https://services.cuzk.cz/sestavy/cis/UI_OKRES.zip`
|
|
112
|
+
- **Obce**: `https://services.cuzk.cz/sestavy/cis/UI_OBEC.zip`
|
|
113
|
+
|
|
114
|
+
## Licence
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ruian-import
|
|
2
|
+
|
|
3
|
+
Python knihovna pro stahování a parsování číselníků ČÚZK (RÚIAN): kraje, okresy a obce.
|
|
4
|
+
|
|
5
|
+
## Instalace
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ruian-import
|
|
9
|
+
# s podporou souřadnic (JTSK -> WGS84):
|
|
10
|
+
pip install "ruian-import[coordinates]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Použití
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from ruian_import import fetch_kraje, fetch_okresy, fetch_obce
|
|
17
|
+
|
|
18
|
+
kraje = fetch_kraje() # list[Kraj]
|
|
19
|
+
okresy = fetch_okresy() # list[Okres]
|
|
20
|
+
obce = fetch_obce() # list[Obec]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Datové třídy
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
@dataclass
|
|
27
|
+
class Kraj:
|
|
28
|
+
kod: str # např. "19"
|
|
29
|
+
nazev: str # např. "Hlavní město Praha"
|
|
30
|
+
nuts_lau: str | None # např. "CZ010"
|
|
31
|
+
zrusen: str | None # datum zrušení "YYYY-MM-DD" nebo None
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Okres:
|
|
35
|
+
kod: str
|
|
36
|
+
nazev: str
|
|
37
|
+
kraj_kod: str
|
|
38
|
+
nuts_lau: str | None
|
|
39
|
+
zrusen: str | None
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Obec:
|
|
43
|
+
kod: str # 6místný kód obce, např. "554979"
|
|
44
|
+
nazev: str
|
|
45
|
+
okres_kod: str
|
|
46
|
+
lat: float | None # WGS84, dostupné jen s [coordinates]
|
|
47
|
+
lon: float | None
|
|
48
|
+
zrusen: str | None
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Zrušené záznamy mají vyplněné pole `zrusen`; platné záznamy mají `zrusen = None`.
|
|
52
|
+
|
|
53
|
+
### Souřadnice obcí
|
|
54
|
+
|
|
55
|
+
`UI_OBEC.csv` souřadnice neobsahuje. Pro doplnění souřadnic použij `fetch_obce_with_coordinates()`, které provede druhý dotaz na ArcGIS REST API ČÚZK a vrátí centroidy polygonů (přesnost ~100 m, jeden HTTP request pro všechny obce):
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from ruian_import import fetch_obce_with_coordinates
|
|
59
|
+
|
|
60
|
+
obce = fetch_obce_with_coordinates() # lat/lon vyplněné
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Nebo načti souřadnice samostatně:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from ruian_import import fetch_obec_coordinates
|
|
67
|
+
|
|
68
|
+
coords = fetch_obec_coordinates() # dict[str, tuple[float, float]] - kod -> (lat, lon)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Vlastní URL / lokální soubory
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from ruian_import._parse import parse_obce
|
|
75
|
+
|
|
76
|
+
data = Path("UI_OBEC.zip").read_bytes()
|
|
77
|
+
obce = parse_obce(data)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Zdroje dat
|
|
81
|
+
|
|
82
|
+
Číselníky ČÚZK se aktualizují denně:
|
|
83
|
+
|
|
84
|
+
- **Kraje**: `https://services.cuzk.cz/sestavy/cis/UI_VUSC.zip`
|
|
85
|
+
- **Okresy**: `https://services.cuzk.cz/sestavy/cis/UI_OKRES.zip`
|
|
86
|
+
- **Obce**: `https://services.cuzk.cz/sestavy/cis/UI_OBEC.zip`
|
|
87
|
+
|
|
88
|
+
## Licence
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ruian-import"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Download and parse Czech RUIAN (ČÚZK) data: municipalities, districts, and regions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Alexandra Ťapková", email = "git@ouppy.space" }]
|
|
13
|
+
keywords = ["ruian", "cuzk", "czech", "municipalities", "gis"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
dependencies = ["requests>=2.28"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
coordinates = ["pyproj>=3.4"]
|
|
29
|
+
dev = ["pytest", "responses", "pyproj>=3.4"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Repository = "https://gitlab.com/alexandra.tapkova/ruian-import"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/ruian_import"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
markers = ["network: requires internet access"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ruian-import: stahování a parsování číselníků ČÚZK (RÚIAN).
|
|
3
|
+
|
|
4
|
+
Základní použití::
|
|
5
|
+
|
|
6
|
+
from ruian_import import fetch_kraje, fetch_okresy, fetch_obce
|
|
7
|
+
|
|
8
|
+
kraje = fetch_kraje()
|
|
9
|
+
okresy = fetch_okresy()
|
|
10
|
+
obce = fetch_obce()
|
|
11
|
+
|
|
12
|
+
Každá funkce vrátí seznam dataclass objektů (Kraj, Okres, Obec).
|
|
13
|
+
Zrušené záznamy mají vyplněné pole ``zrusen`` (datum jako řetězec).
|
|
14
|
+
|
|
15
|
+
Geolokace obcí (lat/lon) je k dispozici jen pokud je nainstalován
|
|
16
|
+
volitelný balíček ``pyproj`` (``pip install ruian-import[coordinates]``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ._arcgis import fetch_obec_coordinates
|
|
20
|
+
from ._http import download
|
|
21
|
+
from ._models import Kraj, Obec, Okres
|
|
22
|
+
from ._parse import OBCE_URL, OKRESY_URL, KRAJE_URL, parse_kraje, parse_obce, parse_okresy
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Kraj",
|
|
26
|
+
"Okres",
|
|
27
|
+
"Obec",
|
|
28
|
+
"fetch_kraje",
|
|
29
|
+
"fetch_okresy",
|
|
30
|
+
"fetch_obce",
|
|
31
|
+
"fetch_obec_coordinates",
|
|
32
|
+
"KRAJE_URL",
|
|
33
|
+
"OKRESY_URL",
|
|
34
|
+
"OBCE_URL",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fetch_kraje(url: str = KRAJE_URL) -> list[Kraj]:
|
|
39
|
+
"""Stáhne a naparsuje číselník krajů z ČÚZK."""
|
|
40
|
+
return parse_kraje(download(url))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fetch_okresy(url: str = OKRESY_URL) -> list[Okres]:
|
|
44
|
+
"""Stáhne a naparsuje číselník okresů z ČÚZK."""
|
|
45
|
+
return parse_okresy(download(url))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_obce(url: str = OBCE_URL) -> list[Obec]:
|
|
49
|
+
"""Stáhne a naparsuje číselník obcí z ČÚZK. Pole lat/lon bude None."""
|
|
50
|
+
return parse_obce(download(url))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fetch_obce_with_coordinates(
|
|
54
|
+
obce_url: str = OBCE_URL,
|
|
55
|
+
) -> list[Obec]:
|
|
56
|
+
"""
|
|
57
|
+
Stáhne číselník obcí i jejich souřadnice (WGS84) a vrátí je sloučené.
|
|
58
|
+
|
|
59
|
+
Provede dva HTTP dotazy: jeden na ČÚZK číselník, jeden na ArcGIS REST API.
|
|
60
|
+
Souřadnice jsou centroidy polygonů územních celků (přesnost ~100 m).
|
|
61
|
+
"""
|
|
62
|
+
obce = parse_obce(download(obce_url))
|
|
63
|
+
coords = fetch_obec_coordinates()
|
|
64
|
+
for obec in obce:
|
|
65
|
+
if obec.kod in coords:
|
|
66
|
+
obec.lat, obec.lon = coords[obec.kod]
|
|
67
|
+
return obce
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stahování souřadnic obcí z ArcGIS REST API ČÚZK.
|
|
3
|
+
|
|
4
|
+
Endpoint vrací polygony územních celků ve WGS84; centroid se počítá průměrem
|
|
5
|
+
vrcholů vnějšího prstence (dostatečná přesnost pro polohový bod obce).
|
|
6
|
+
|
|
7
|
+
URL: https://ags.cuzk.cz/arcgis/rest/services/RUIAN/MapServer/12
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ._http import download
|
|
14
|
+
|
|
15
|
+
_QUERY_URL = (
|
|
16
|
+
"https://ags.cuzk.cz/arcgis/rest/services/RUIAN/MapServer/12/query"
|
|
17
|
+
"?where=platido+IS+NULL"
|
|
18
|
+
"&outFields=kod"
|
|
19
|
+
"&returnGeometry=true"
|
|
20
|
+
"&geometryPrecision=5"
|
|
21
|
+
"&outSR=4326"
|
|
22
|
+
"&f=json"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ring_centroid(ring: list[list[float]]) -> tuple[float, float]:
|
|
27
|
+
"""Průměr souřadnic vrcholů vnějšího prstence polygonu."""
|
|
28
|
+
lons = [pt[0] for pt in ring]
|
|
29
|
+
lats = [pt[1] for pt in ring]
|
|
30
|
+
return sum(lats) / len(lats), sum(lons) / len(lons)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fetch_obec_coordinates(
|
|
34
|
+
url: str = _QUERY_URL,
|
|
35
|
+
) -> dict[str, tuple[float, float]]:
|
|
36
|
+
"""
|
|
37
|
+
Stáhne souřadnice (WGS84) všech platných obcí z ArcGIS REST API ČÚZK.
|
|
38
|
+
|
|
39
|
+
Vrací slovník ``{kod_obce: (lat, lon)}``.
|
|
40
|
+
Jeden HTTP dotaz - vrací ~6 200 obcí najednou.
|
|
41
|
+
"""
|
|
42
|
+
data = json.loads(download(url))
|
|
43
|
+
result: dict[str, tuple[float, float]] = {}
|
|
44
|
+
for feature in data.get("features", []):
|
|
45
|
+
kod = str(feature.get("attributes", {}).get("kod", "")).strip()
|
|
46
|
+
rings = (feature.get("geometry") or {}).get("rings")
|
|
47
|
+
if not kod or not rings or not rings[0]:
|
|
48
|
+
continue
|
|
49
|
+
result[kod] = _ring_centroid(rings[0])
|
|
50
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Převod S-JTSK (EPSG:5514) na WGS84 (EPSG:4326).
|
|
3
|
+
|
|
4
|
+
Vyžaduje volitelnou závislost pyproj; pokud není nainstalována, funkce
|
|
5
|
+
vrací None a koordináty se vynechají.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from pyproj import Transformer as _Transformer
|
|
12
|
+
|
|
13
|
+
_transformer = _Transformer.from_crs("EPSG:5514", "EPSG:4326", always_xy=True)
|
|
14
|
+
_PYPROJ_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
_PYPROJ_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_jtsk(
|
|
20
|
+
first: Optional[float], second: Optional[float]
|
|
21
|
+
) -> tuple[Optional[float], Optional[float]]:
|
|
22
|
+
"""
|
|
23
|
+
Vrátí (X, Y) v S-JTSK. Pořadí sloupců souřadnic není spolehlivé,
|
|
24
|
+
ale rozsahy os se v ČR nepřekrývají (X ⊂ ⟨935, 1230⟩ km, Y ⊂ ⟨430, 905⟩ km).
|
|
25
|
+
"""
|
|
26
|
+
if first is None or second is None:
|
|
27
|
+
return None, None
|
|
28
|
+
return (first, second) if first > second else (second, first)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sjtsk_to_wgs84(
|
|
32
|
+
first: Optional[float], second: Optional[float]
|
|
33
|
+
) -> tuple[Optional[float], Optional[float]]:
|
|
34
|
+
"""
|
|
35
|
+
Převede souřadnice S-JTSK (libovolné pořadí) na (lat, lon) WGS84.
|
|
36
|
+
Vrátí (None, None) pokud nejsou vstupní hodnoty nebo pyproj není nainstalován.
|
|
37
|
+
"""
|
|
38
|
+
if not _PYPROJ_AVAILABLE:
|
|
39
|
+
return None, None
|
|
40
|
+
jtsk_x, jtsk_y = _normalize_jtsk(first, second)
|
|
41
|
+
if jtsk_x is None or jtsk_y is None:
|
|
42
|
+
return None, None
|
|
43
|
+
# EPSG:5514 používá záporné souřadnice
|
|
44
|
+
lon, lat = _transformer.transform(-jtsk_y, -jtsk_x)
|
|
45
|
+
return lat, lon
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
_TIMEOUT = 30
|
|
4
|
+
_RETRIES = 2
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def download(url: str, timeout: int = _TIMEOUT, retries: int = _RETRIES) -> bytes:
|
|
8
|
+
last_error: Exception | None = None
|
|
9
|
+
for _ in range(1 + retries):
|
|
10
|
+
try:
|
|
11
|
+
response = requests.get(url, timeout=timeout)
|
|
12
|
+
response.raise_for_status()
|
|
13
|
+
return response.content
|
|
14
|
+
except requests.RequestException as exc:
|
|
15
|
+
last_error = exc
|
|
16
|
+
raise last_error # type: ignore[misc]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Kraj:
|
|
7
|
+
kod: str
|
|
8
|
+
nazev: str
|
|
9
|
+
nuts_lau: Optional[str]
|
|
10
|
+
# None = platný, str = datum zrušení "YYYY-MM-DD"
|
|
11
|
+
zrusen: Optional[str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Okres:
|
|
16
|
+
kod: str
|
|
17
|
+
nazev: str
|
|
18
|
+
kraj_kod: str
|
|
19
|
+
nuts_lau: Optional[str]
|
|
20
|
+
zrusen: Optional[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Obec:
|
|
25
|
+
kod: str
|
|
26
|
+
nazev: str
|
|
27
|
+
okres_kod: str
|
|
28
|
+
# WGS84; None pokud nejsou k dispozici (UI_OBEC.csv souřadnice neobsahuje;
|
|
29
|
+
# lze doplnit z VDP API nebo vlastní tabulky)
|
|
30
|
+
lat: Optional[float]
|
|
31
|
+
lon: Optional[float]
|
|
32
|
+
zrusen: Optional[str]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parsování CSV číselníků ČÚZK.
|
|
3
|
+
|
|
4
|
+
Zdroje (denní aktualizace):
|
|
5
|
+
Kraje: https://services.cuzk.cz/sestavy/cis/UI_VUSC.zip
|
|
6
|
+
Okresy: https://services.cuzk.cz/sestavy/cis/UI_OKRES.zip
|
|
7
|
+
Obce: https://services.cuzk.cz/sestavy/cis/UI_OBEC.zip
|
|
8
|
+
|
|
9
|
+
Formát: ZIP s jedním CSV souborem, oddělovač `;`.
|
|
10
|
+
Kódování: UTF-8 nebo cp1250 (soubory ČÚZK nemají jednotné kódování).
|
|
11
|
+
Sloupce jsou stabilní ale neúplné - UI_OBEC.csv souřadnice neobsahuje.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import csv
|
|
15
|
+
import io
|
|
16
|
+
import zipfile
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from ._coordinates import sjtsk_to_wgs84
|
|
20
|
+
from ._models import Kraj, Obec, Okres
|
|
21
|
+
|
|
22
|
+
KRAJE_URL = "https://services.cuzk.cz/sestavy/cis/UI_VUSC.zip"
|
|
23
|
+
OKRESY_URL = "https://services.cuzk.cz/sestavy/cis/UI_OKRES.zip"
|
|
24
|
+
OBCE_URL = "https://services.cuzk.cz/sestavy/cis/UI_OBEC.zip"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def decode_csv(data: bytes) -> str:
|
|
28
|
+
"""Dekóduje bytes ze ČÚZK; zkouší UTF-8, padne zpět na cp1250."""
|
|
29
|
+
try:
|
|
30
|
+
text = data.decode("utf-8")
|
|
31
|
+
except UnicodeDecodeError:
|
|
32
|
+
text = data.decode("cp1250")
|
|
33
|
+
return text.lstrip("")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_zipped_csvs(data: bytes) -> list[str]:
|
|
37
|
+
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
|
38
|
+
return [
|
|
39
|
+
decode_csv(zf.read(name))
|
|
40
|
+
for name in zf.namelist()
|
|
41
|
+
if name.lower().endswith(".csv")
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_float(value: Optional[str]) -> Optional[float]:
|
|
46
|
+
value = (value or "").strip().replace(",", ".")
|
|
47
|
+
if not value:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
return float(value)
|
|
51
|
+
except ValueError:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _plati_do(row: dict) -> Optional[str]:
|
|
56
|
+
"""Vrátí datum zrušení jako 'YYYY-MM-DD' nebo None (stále platný)."""
|
|
57
|
+
raw = (row.get("PLATI_DO") or "").strip()
|
|
58
|
+
if not raw:
|
|
59
|
+
return None
|
|
60
|
+
return raw.split(" ")[0]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_kraje(data: bytes) -> list[Kraj]:
|
|
64
|
+
"""
|
|
65
|
+
Sloupce UI_VUSC.csv:
|
|
66
|
+
KOD;NAZEV;REGSOUDR_KOD;NUTS_LAU;PLATI_OD;PLATI_DO;DATUM_VZNIKU
|
|
67
|
+
"""
|
|
68
|
+
result: list[Kraj] = []
|
|
69
|
+
for text in read_zipped_csvs(data):
|
|
70
|
+
for row in csv.DictReader(io.StringIO(text), delimiter=";"):
|
|
71
|
+
kod = (row.get("KOD") or "").strip()
|
|
72
|
+
nazev = (row.get("NAZEV") or "").strip()
|
|
73
|
+
if not kod or not nazev:
|
|
74
|
+
continue
|
|
75
|
+
result.append(
|
|
76
|
+
Kraj(
|
|
77
|
+
kod=kod,
|
|
78
|
+
nazev=nazev,
|
|
79
|
+
nuts_lau=(row.get("NUTS_LAU") or "").strip() or None,
|
|
80
|
+
zrusen=_plati_do(row),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_okresy(data: bytes) -> list[Okres]:
|
|
87
|
+
"""
|
|
88
|
+
Sloupce UI_OKRES.csv:
|
|
89
|
+
KOD;NAZEV;VUSC_KOD;NUTS_LAU;PLATI_OD;PLATI_DO;DATUM_VZNIKU
|
|
90
|
+
"""
|
|
91
|
+
result: list[Okres] = []
|
|
92
|
+
for text in read_zipped_csvs(data):
|
|
93
|
+
for row in csv.DictReader(io.StringIO(text), delimiter=";"):
|
|
94
|
+
kod = (row.get("KOD") or "").strip()
|
|
95
|
+
nazev = (row.get("NAZEV") or "").strip()
|
|
96
|
+
kraj_kod = (row.get("VUSC_KOD") or "").strip()
|
|
97
|
+
if not kod or not nazev or not kraj_kod:
|
|
98
|
+
continue
|
|
99
|
+
result.append(
|
|
100
|
+
Okres(
|
|
101
|
+
kod=kod,
|
|
102
|
+
nazev=nazev,
|
|
103
|
+
kraj_kod=kraj_kod,
|
|
104
|
+
nuts_lau=(row.get("NUTS_LAU") or "").strip() or None,
|
|
105
|
+
zrusen=_plati_do(row),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_obce(data: bytes) -> list[Obec]:
|
|
112
|
+
"""
|
|
113
|
+
Sloupce UI_OBEC.csv:
|
|
114
|
+
KOD;NAZEV;STATUS_KOD;POU_KOD;OKRES_KOD;CLENENI_SM_ROZSAH_KOD;CLENENI_SM_TYP_KOD;PLATI_OD;PLATI_DO;DATUM_VZNIKU
|
|
115
|
+
|
|
116
|
+
Souřadnice v tomto CSV nejsou; lat/lon bude None pokud nejsou dodány externě.
|
|
117
|
+
Pro doplnění souřadnic použij fetch_obec_coordinates() nebo vlastní tabulku.
|
|
118
|
+
"""
|
|
119
|
+
result: list[Obec] = []
|
|
120
|
+
for text in read_zipped_csvs(data):
|
|
121
|
+
for row in csv.DictReader(io.StringIO(text), delimiter=";"):
|
|
122
|
+
kod = (row.get("KOD") or "").strip()
|
|
123
|
+
nazev = (row.get("NAZEV") or "").strip()
|
|
124
|
+
okres_kod = (row.get("OKRES_KOD") or "").strip()
|
|
125
|
+
if not kod or not nazev or not okres_kod:
|
|
126
|
+
continue
|
|
127
|
+
# Pokud by budoucí export souřadnice přidal, zpracujeme je zde
|
|
128
|
+
lat, lon = sjtsk_to_wgs84(
|
|
129
|
+
_parse_float(row.get("SOURADNICE_X")),
|
|
130
|
+
_parse_float(row.get("SOURADNICE_Y")),
|
|
131
|
+
)
|
|
132
|
+
result.append(
|
|
133
|
+
Obec(
|
|
134
|
+
kod=kod,
|
|
135
|
+
nazev=nazev,
|
|
136
|
+
okres_kod=okres_kod,
|
|
137
|
+
lat=lat,
|
|
138
|
+
lon=lon,
|
|
139
|
+
zrusen=_plati_do(row),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
return result
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ruian_import._arcgis import fetch_obec_coordinates
|
|
6
|
+
from ruian_import._parse import parse_kraje, parse_obce, parse_okresy
|
|
7
|
+
|
|
8
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_parse_kraje():
|
|
12
|
+
data = (FIXTURES / "UI_VUSC.zip").read_bytes()
|
|
13
|
+
kraje = parse_kraje(data)
|
|
14
|
+
assert len(kraje) > 0
|
|
15
|
+
kody = {k.kod for k in kraje}
|
|
16
|
+
assert "19" in kody # Hlavní město Praha
|
|
17
|
+
for k in kraje:
|
|
18
|
+
assert k.kod
|
|
19
|
+
assert k.nazev
|
|
20
|
+
# platné kraje nemají datum zrušení
|
|
21
|
+
platne = [k for k in kraje if k.zrusen is None]
|
|
22
|
+
assert len(platne) > 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parse_okresy():
|
|
26
|
+
data = (FIXTURES / "UI_OKRES.zip").read_bytes()
|
|
27
|
+
okresy = parse_okresy(data)
|
|
28
|
+
assert len(okresy) > 0
|
|
29
|
+
for o in okresy:
|
|
30
|
+
assert o.kod
|
|
31
|
+
assert o.nazev
|
|
32
|
+
assert o.kraj_kod
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_parse_obce():
|
|
36
|
+
data = (FIXTURES / "UI_OBEC.zip").read_bytes()
|
|
37
|
+
obce = parse_obce(data)
|
|
38
|
+
assert len(obce) > 5000 # ČR má ~6 200 obcí
|
|
39
|
+
for o in obce:
|
|
40
|
+
assert o.kod
|
|
41
|
+
assert o.nazev
|
|
42
|
+
assert o.okres_kod
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_kraj_nuts():
|
|
46
|
+
data = (FIXTURES / "UI_VUSC.zip").read_bytes()
|
|
47
|
+
kraje = parse_kraje(data)
|
|
48
|
+
nuts = {k.nuts_lau for k in kraje if k.nuts_lau}
|
|
49
|
+
assert any(n.startswith("CZ") for n in nuts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_okres_kraj_vazba():
|
|
53
|
+
kraje_data = (FIXTURES / "UI_VUSC.zip").read_bytes()
|
|
54
|
+
okresy_data = (FIXTURES / "UI_OKRES.zip").read_bytes()
|
|
55
|
+
kraj_kody = {k.kod for k in parse_kraje(kraje_data)}
|
|
56
|
+
for okres in parse_okresy(okresy_data):
|
|
57
|
+
assert okres.kraj_kod in kraj_kody, f"Okres {okres.nazev} má neznámý kraj_kod {okres.kraj_kod}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_obec_okres_vazba():
|
|
61
|
+
okresy_data = (FIXTURES / "UI_OKRES.zip").read_bytes()
|
|
62
|
+
obce_data = (FIXTURES / "UI_OBEC.zip").read_bytes()
|
|
63
|
+
okres_kody = {o.kod for o in parse_okresy(okresy_data)}
|
|
64
|
+
for obec in parse_obce(obce_data):
|
|
65
|
+
assert obec.okres_kod in okres_kody, f"Obec {obec.nazev} má neznámý okres_kod {obec.okres_kod}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.network
|
|
69
|
+
def test_fetch_obec_coordinates():
|
|
70
|
+
coords = fetch_obec_coordinates()
|
|
71
|
+
assert len(coords) > 5000
|
|
72
|
+
# Abertamy - kód 554979
|
|
73
|
+
lat, lon = coords["554979"]
|
|
74
|
+
assert 49 < lat < 51, f"Neočekávaná šířka: {lat}"
|
|
75
|
+
assert 12 < lon < 19, f"Neočekávaná délka: {lon}"
|