geocodebr 0.1.0__py3-none-any.whl
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.
- geocodebr/__init__.py +26 -0
- geocodebr/_heap.py +119 -0
- geocodebr/_heap_patch.py +130 -0
- geocodebr/cache.py +159 -0
- geocodebr/cep.py +117 -0
- geocodebr/constants.py +21 -0
- geocodebr/db.py +89 -0
- geocodebr/download_cnefe.py +108 -0
- geocodebr/errors.py +11 -0
- geocodebr/fields.py +136 -0
- geocodebr/geo.py +61 -0
- geocodebr/geocode.py +362 -0
- geocodebr/match_types.py +140 -0
- geocodebr/matching.py +838 -0
- geocodebr/messages.py +47 -0
- geocodebr/py.typed +0 -0
- geocodebr/reverse.py +253 -0
- geocodebr/standardize.py +237 -0
- geocodebr/string_dist.py +54 -0
- geocodebr/tables.py +101 -0
- geocodebr/utils.py +51 -0
- geocodebr-0.1.0.dist-info/METADATA +436 -0
- geocodebr-0.1.0.dist-info/RECORD +25 -0
- geocodebr-0.1.0.dist-info/WHEEL +4 -0
- geocodebr-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
geocodebr/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .cache import (
|
|
2
|
+
definir_pasta_cache,
|
|
3
|
+
deletar_pasta_cache,
|
|
4
|
+
listar_dados_cache,
|
|
5
|
+
listar_pasta_cache,
|
|
6
|
+
)
|
|
7
|
+
from .download_cnefe import download_cnefe
|
|
8
|
+
from .fields import definir_campos
|
|
9
|
+
from .geocode import geocode
|
|
10
|
+
from .cep import busca_por_cep
|
|
11
|
+
from .reverse import geocode_reverso
|
|
12
|
+
from .standardize import enderecobr_padronizar_enderecos
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"busca_por_cep",
|
|
16
|
+
"definir_campos",
|
|
17
|
+
"definir_pasta_cache",
|
|
18
|
+
"deletar_pasta_cache",
|
|
19
|
+
"download_cnefe",
|
|
20
|
+
"geocode",
|
|
21
|
+
"geocode_reverso",
|
|
22
|
+
"listar_dados_cache",
|
|
23
|
+
"listar_pasta_cache",
|
|
24
|
+
"enderecobr_padronizar_enderecos",
|
|
25
|
+
]
|
|
26
|
+
|
geocodebr/_heap.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Detecção do heap NT do interpretador Python no Windows.
|
|
2
|
+
|
|
3
|
+
O python.exe embute no recurso RT_MANIFEST a declaração de heap; sem
|
|
4
|
+
<heapType>SegmentHeap</heapType>, o processo roda no heap NT legacy, que
|
|
5
|
+
degrada sob alocação/liberação multithread intensa do DuckDB
|
|
6
|
+
(duckdb/duckdb#24027; diagnóstico em quality_reports/diagnoses/).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
NOME_COPIA_PATCH = "python-geocodebr-sh.exe"
|
|
16
|
+
N_CORES_HEAP_LEGACY = 4
|
|
17
|
+
|
|
18
|
+
_aviso_emitido = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_exe_base() -> str:
|
|
22
|
+
"""Venv: o exe real é o da base (pegadinha do launcher); demais casos, sys.executable."""
|
|
23
|
+
if sys.platform == "win32" and sys.prefix != sys.base_prefix:
|
|
24
|
+
base = Path(sys.base_prefix) / "python.exe"
|
|
25
|
+
if base.is_file():
|
|
26
|
+
return str(base)
|
|
27
|
+
return sys.executable
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tem_segment_heap(exe: str) -> bool | None:
|
|
31
|
+
"""True/False no Windows; None fora dele ou se o exe não puder ser lido.
|
|
32
|
+
|
|
33
|
+
Procura o elemento XML completo, não a palavra solta: a string
|
|
34
|
+
"SegmentHeap" fora do manifesto daria falso positivo e silenciaria o aviso.
|
|
35
|
+
"""
|
|
36
|
+
if sys.platform != "win32":
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
with open(exe, "rb") as fh:
|
|
40
|
+
return b"SegmentHeap</heapType>" in fh.read()
|
|
41
|
+
except OSError:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cores_disponiveis() -> int:
|
|
46
|
+
"""Núcleos disponíveis ao processo no Windows.
|
|
47
|
+
|
|
48
|
+
Respeita a affinity mask via GetProcessAffinityMask: um processo pinado
|
|
49
|
+
em menos núcleos (ou rodando em container/VM com restrição) não deve
|
|
50
|
+
receber mais workers que ela permite. Chamado apenas pelo
|
|
51
|
+
n_cores_efetivo(), que só ativa no Windows.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
import ctypes
|
|
55
|
+
|
|
56
|
+
kernel32 = ctypes.windll.kernel32
|
|
57
|
+
kernel32.GetCurrentProcess.restype = ctypes.c_void_p
|
|
58
|
+
kernel32.GetProcessAffinityMask.restype = ctypes.c_int
|
|
59
|
+
kernel32.GetProcessAffinityMask.argtypes = [
|
|
60
|
+
ctypes.c_void_p,
|
|
61
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
62
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
63
|
+
]
|
|
64
|
+
mascara = ctypes.c_size_t()
|
|
65
|
+
sistema = ctypes.c_size_t()
|
|
66
|
+
if kernel32.GetProcessAffinityMask(
|
|
67
|
+
kernel32.GetCurrentProcess(),
|
|
68
|
+
ctypes.byref(mascara),
|
|
69
|
+
ctypes.byref(sistema),
|
|
70
|
+
) and mascara.value:
|
|
71
|
+
return bin(mascara.value).count("1")
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return os.cpu_count() or 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def n_cores_efetivo(n_cores: int | None) -> int | None:
|
|
78
|
+
"""Aplica a mitigação de threads no Windows sem Segment Heap.
|
|
79
|
+
|
|
80
|
+
No heap legacy a contenção de alocação multithread faz a curva tempo x
|
|
81
|
+
threads ter mínimo em ~4-6 threads. Quando o usuário não definiu
|
|
82
|
+
n_cores, limita ao ótimo empírico sem exceder os núcleos disponíveis ao
|
|
83
|
+
processo — `SET threads` acima dos núcleos não é clampeado pelo DuckDB
|
|
84
|
+
e só gera oversubscription. Valor explícito é respeitado. O aviso é
|
|
85
|
+
emitido uma vez por sessão, mesmo com n_cores explícito.
|
|
86
|
+
"""
|
|
87
|
+
global _aviso_emitido
|
|
88
|
+
if sys.platform != "win32":
|
|
89
|
+
return n_cores
|
|
90
|
+
exe = resolve_exe_base()
|
|
91
|
+
if tem_segment_heap(exe) is not False:
|
|
92
|
+
return n_cores
|
|
93
|
+
original = n_cores
|
|
94
|
+
if original is None:
|
|
95
|
+
n_cores = min(N_CORES_HEAP_LEGACY, cores_disponiveis())
|
|
96
|
+
if not _aviso_emitido:
|
|
97
|
+
_aviso_emitido = True
|
|
98
|
+
print(_mensagem_aviso(exe, original, n_cores), file=sys.stderr)
|
|
99
|
+
return n_cores
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _mensagem_aviso(exe: str, n_cores_original: int | None, n_cores_efetivo: int | None) -> str:
|
|
103
|
+
linhas = [
|
|
104
|
+
"[geocodebr] interpretador Python com heap NT legacy detectado "
|
|
105
|
+
f"({exe}): no Windows, o DuckDB pode ter queda de performance e "
|
|
106
|
+
"deterioração entre chamadas sucessivas de geocode().",
|
|
107
|
+
]
|
|
108
|
+
if n_cores_original is None and n_cores_efetivo is not None:
|
|
109
|
+
linhas.append(
|
|
110
|
+
f"Mitigação aplicada: threads do DuckDB limitadas a "
|
|
111
|
+
f"{n_cores_efetivo} nesta sessão (n_cores não definido)."
|
|
112
|
+
)
|
|
113
|
+
linhas.append(
|
|
114
|
+
"Aceleração real: gere um interpretador com Segment Heap com "
|
|
115
|
+
"python -m geocodebr._heap_patch\n"
|
|
116
|
+
"e inicie a sessão pela cópia gerada (python-geocodebr-sh.exe). "
|
|
117
|
+
'Veja a seção "Windows e performance" do README.'
|
|
118
|
+
)
|
|
119
|
+
return "\n".join(linhas)
|
geocodebr/_heap_patch.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Cria cópia do interpretador Python base com Segment Heap habilitado.
|
|
2
|
+
|
|
3
|
+
Uso (Windows):
|
|
4
|
+
|
|
5
|
+
python -m geocodebr._heap_patch
|
|
6
|
+
|
|
7
|
+
Cria `python-geocodebr-sh.exe` ao lado do interpretador base, patcheando o
|
|
8
|
+
recurso RT_MANIFEST com <heapType>SegmentHeap</heapType> — o original nunca é
|
|
9
|
+
alterado. O ganho de performance do DuckDB vale apenas para sessões iniciadas
|
|
10
|
+
pela cópia: o heap é fixado no image load, não muda em runtime.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ._heap import NOME_COPIA_PATCH, resolve_exe_base, tem_segment_heap
|
|
20
|
+
|
|
21
|
+
_ELEMENTO_HEAP = (
|
|
22
|
+
b'<heapType xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">'
|
|
23
|
+
b"SegmentHeap</heapType>"
|
|
24
|
+
)
|
|
25
|
+
_ABRE_ASSEMBLY = b"<assembly"
|
|
26
|
+
_FECHA_ASSEMBLY = b"</assembly>"
|
|
27
|
+
_NS_ASSEMBLY = b"urn:schemas-microsoft-com:asm.v1"
|
|
28
|
+
_BLOCO_APPLICATION = (
|
|
29
|
+
b'<application xmlns="urn:schemas-microsoft-com:asm.v3">'
|
|
30
|
+
b"<windowsSettings>" + _ELEMENTO_HEAP + b"</windowsSettings></application>"
|
|
31
|
+
)
|
|
32
|
+
_COMPIME_ENTRE_TAGS = re.compile(rb">\s+<")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main(argv: list[str] | None = None) -> int:
|
|
36
|
+
if argv:
|
|
37
|
+
print("[geocodebr] uso: python -m geocodebr._heap_patch (sem argumentos).", file=sys.stderr)
|
|
38
|
+
return 2
|
|
39
|
+
if sys.platform != "win32":
|
|
40
|
+
print("[geocodebr] o patch de Segment Heap e exclusivo do Windows.", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
exe = resolve_exe_base()
|
|
44
|
+
print(f"[geocodebr] interpretador base: {exe}")
|
|
45
|
+
|
|
46
|
+
heap = tem_segment_heap(exe)
|
|
47
|
+
if heap is True:
|
|
48
|
+
print("[geocodebr] o interpretador base ja declara SegmentHeap; nada a fazer.")
|
|
49
|
+
return 0
|
|
50
|
+
if heap is None:
|
|
51
|
+
print("[geocodebr] nao foi possivel ler o interpretador base.", file=sys.stderr)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
destino = str(Path(exe).with_name(NOME_COPIA_PATCH))
|
|
55
|
+
try:
|
|
56
|
+
criar_copia(exe, destino)
|
|
57
|
+
except ValueError as e:
|
|
58
|
+
print(f"[geocodebr] {e}", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
except OSError as e:
|
|
61
|
+
print(
|
|
62
|
+
f"[geocodebr] falha ao escrever a copia ({e}). Se o interpretador estiver "
|
|
63
|
+
"em pasta protegida (ex.: Program Files), use um terminal como "
|
|
64
|
+
"administrador ou uma instalacao por usuario.",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
print(f"[geocodebr] copia criada: {destino}")
|
|
70
|
+
print(
|
|
71
|
+
"[geocodebr] para acelerar o geocode(), inicie a sessao pela copia "
|
|
72
|
+
"(ex.: \"" + destino + "\" script.py). A copia usa os pacotes do ambiente "
|
|
73
|
+
"base; veja a secao \"Windows e performance\" do README."
|
|
74
|
+
)
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def criar_copia(exe: str, destino: str) -> None:
|
|
79
|
+
"""Lê o exe, patcheia o manifesto e grava a cópia; lança ValueError/OSError."""
|
|
80
|
+
dados = Path(exe).read_bytes()
|
|
81
|
+
ini, fim = encontrar_manifesto(dados)
|
|
82
|
+
novo = patch_manifesto(dados[ini:fim])
|
|
83
|
+
patcheado = dados[:ini] + novo + dados[fim:]
|
|
84
|
+
Path(destino).write_bytes(patcheado)
|
|
85
|
+
if tem_segment_heap(destino) is not True:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"a copia gerada nao declara SegmentHeap; o patch foi abortado "
|
|
88
|
+
"(o arquivo original nao foi alterado)."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def encontrar_manifesto(dados: bytes) -> tuple[int, int]:
|
|
93
|
+
"""Localiza a região XML do RT_MANIFEST (start, end) no exe."""
|
|
94
|
+
ini = dados.find(_ABRE_ASSEMBLY)
|
|
95
|
+
while ini != -1:
|
|
96
|
+
fim = dados.find(_FECHA_ASSEMBLY, ini)
|
|
97
|
+
if fim != -1 and _NS_ASSEMBLY in dados[ini:fim]:
|
|
98
|
+
return ini, fim + len(_FECHA_ASSEMBLY)
|
|
99
|
+
ini = dados.find(_ABRE_ASSEMBLY, ini + 1)
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"manifesto de aplicacao nao encontrado no interpretador; "
|
|
102
|
+
"exe nao suportado pelo patch."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def patch_manifesto(manifesto: bytes) -> bytes:
|
|
107
|
+
"""Insere <heapType>SegmentHeap</heapType> devolvendo EXATAMENTE o mesmo
|
|
108
|
+
tamanho em bytes — o recurso RT_MANIFEST tem tamanho fixo no PE. O espaço
|
|
109
|
+
entre tags é insignificante no XML: comprime a indentação e preenche o
|
|
110
|
+
resto com espaços antes de </assembly>."""
|
|
111
|
+
if b"<heapType" in manifesto:
|
|
112
|
+
raise ValueError("o manifesto ja declara heapType; nada a fazer.")
|
|
113
|
+
if b"</windowsSettings>" in manifesto:
|
|
114
|
+
novo = manifesto.replace(b"</windowsSettings>", _ELEMENTO_HEAP + b"</windowsSettings>", 1)
|
|
115
|
+
else:
|
|
116
|
+
novo = manifesto.replace(_FECHA_ASSEMBLY, _BLOCO_APPLICATION + _FECHA_ASSEMBLY, 1)
|
|
117
|
+
novo = _COMPIME_ENTRE_TAGS.sub(b"><", novo)
|
|
118
|
+
faltam = len(manifesto) - len(novo)
|
|
119
|
+
if faltam < 0:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"manifesto sem folga para o patch size-preserving; "
|
|
122
|
+
"reporte em https://github.com/ipeaGIT/geocodebr/issues"
|
|
123
|
+
)
|
|
124
|
+
if faltam > 0:
|
|
125
|
+
novo = novo.replace(_FECHA_ASSEMBLY, b" " * faltam + _FECHA_ASSEMBLY, 1)
|
|
126
|
+
return novo
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__": # pragma: no cover
|
|
130
|
+
sys.exit(main(sys.argv[1:]))
|
geocodebr/cache.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from platformdirs import user_cache_dir, user_config_dir
|
|
7
|
+
|
|
8
|
+
from .messages import confirm
|
|
9
|
+
from .constants import DATA_RELEASE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def caminho_parquet(nome_tabela: str, pasta_dados: str | None = None) -> str:
|
|
13
|
+
"""Monta o caminho de um arquivo parquet do CNEFE no disco.
|
|
14
|
+
|
|
15
|
+
Espelha ``caminho_parquet()`` em ``r-package/R/cache.R``. ``pasta_dados`` e
|
|
16
|
+
o ``data_release`` vigente já foram resolvidos pelo chamador (via
|
|
17
|
+
``download_cnefe``), não são redescobertos aqui. O arquivo não precisa
|
|
18
|
+
existir.
|
|
19
|
+
"""
|
|
20
|
+
if pasta_dados is None:
|
|
21
|
+
pasta_dados = listar_pasta_cache()
|
|
22
|
+
|
|
23
|
+
path = Path(pasta_dados) / f"geocodebr_data_release_{DATA_RELEASE}" / f"{nome_tabela}.parquet"
|
|
24
|
+
return path.as_posix()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def listar_pasta_cache_padrao() -> str:
|
|
28
|
+
return str(Path(user_cache_dir("geocodebr")))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def listar_arquivo_config() -> str:
|
|
32
|
+
return str(Path(user_config_dir("geocodebr")) / "cache_dir")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def definir_pasta_cache(path: str | None, verboso: bool = True) -> str:
|
|
36
|
+
"""Define a pasta de cache do geocodebr.
|
|
37
|
+
|
|
38
|
+
Define o diretório de cache para os dados do geocodebr. Essa configuração
|
|
39
|
+
é persistente entre sessões do Python.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
path : str ou None
|
|
44
|
+
O caminho para o diretório usado para armazenar os dados em cache.
|
|
45
|
+
Se `None`, o pacote usará o diretório padrão do pacote.
|
|
46
|
+
verboso : bool, opcional
|
|
47
|
+
Indica se uma mensagem de confirmação deve ser exibida. O padrão é
|
|
48
|
+
`True`.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
str
|
|
53
|
+
O caminho do diretório de cache configurado.
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
>>> definir_pasta_cache("D:/dados/geocodebr-cache")
|
|
58
|
+
|
|
59
|
+
# retoma pasta padrão do pacote
|
|
60
|
+
>>> definir_pasta_cache(path=None)
|
|
61
|
+
"""
|
|
62
|
+
cache_dir = Path(listar_pasta_cache_padrao()) if path is None else Path(path)
|
|
63
|
+
cache_dir = cache_dir.expanduser()
|
|
64
|
+
|
|
65
|
+
config_file = Path(listar_arquivo_config())
|
|
66
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
config_file.write_text(str(cache_dir), encoding="utf-8")
|
|
68
|
+
|
|
69
|
+
if verboso:
|
|
70
|
+
confirm(f"Definido como pasta de cache {cache_dir}.")
|
|
71
|
+
|
|
72
|
+
return str(cache_dir)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def listar_pasta_cache() -> str:
|
|
76
|
+
"""Obtém a pasta de cache usada pelo geocodebr.
|
|
77
|
+
|
|
78
|
+
Obtém o caminho da pasta utilizada para armazenar em cache os dados do
|
|
79
|
+
geocodebr. Útil para inspecionar a pasta configurada com
|
|
80
|
+
`definir_pasta_cache()` em uma sessão anterior. Retorna a pasta de cache
|
|
81
|
+
padrão caso nenhuma pasta personalizada tenha sido configurada
|
|
82
|
+
anteriormente.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
str
|
|
87
|
+
O caminho da pasta de cache.
|
|
88
|
+
"""
|
|
89
|
+
config_file = Path(listar_arquivo_config())
|
|
90
|
+
if config_file.exists():
|
|
91
|
+
value = config_file.read_text(encoding="utf-8").strip()
|
|
92
|
+
if value:
|
|
93
|
+
return str(Path(value).expanduser())
|
|
94
|
+
return listar_pasta_cache_padrao()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def listar_dados_cache(print_tree: bool = False) -> list[str]:
|
|
98
|
+
"""Lista os dados salvos localmente na pasta de cache.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
print_tree : bool, opcional
|
|
103
|
+
Indica se o conteúdo da pasta de cache deve ser exibido em um formato
|
|
104
|
+
de árvore. O padrão é `False`.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
list[str]
|
|
109
|
+
Os caminhos para os arquivos em cache.
|
|
110
|
+
"""
|
|
111
|
+
cache_dir = Path(listar_pasta_cache())
|
|
112
|
+
if not cache_dir.exists():
|
|
113
|
+
confirm("Nenhum dado em cache local")
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
files = sorted(str(path) for path in cache_dir.rglob("*") if path.is_file())
|
|
117
|
+
if print_tree:
|
|
118
|
+
_print_tree(cache_dir)
|
|
119
|
+
return files
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def deletar_pasta_cache() -> str:
|
|
123
|
+
"""Deleta todos os arquivos da pasta de cache do geocodebr.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
str
|
|
128
|
+
O caminho do diretório de cache deletado.
|
|
129
|
+
"""
|
|
130
|
+
cache_dir = Path(listar_pasta_cache())
|
|
131
|
+
if cache_dir.exists():
|
|
132
|
+
shutil.rmtree(cache_dir)
|
|
133
|
+
confirm(f"Deletada a pasta de cache que se encontrava em {cache_dir}.")
|
|
134
|
+
return str(cache_dir)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def apaga_data_release_antigo(data_release: str) -> str:
|
|
138
|
+
cache_dir = Path(listar_pasta_cache())
|
|
139
|
+
if not cache_dir.exists():
|
|
140
|
+
return str(cache_dir)
|
|
141
|
+
|
|
142
|
+
release_dirs = [
|
|
143
|
+
path
|
|
144
|
+
for path in cache_dir.iterdir()
|
|
145
|
+
if path.is_dir() and path.name.startswith("geocodebr_data_release_")
|
|
146
|
+
]
|
|
147
|
+
expected = cache_dir / f"geocodebr_data_release_{data_release}"
|
|
148
|
+
stale_dirs = [path for path in release_dirs if path != expected]
|
|
149
|
+
for path in stale_dirs:
|
|
150
|
+
shutil.rmtree(path)
|
|
151
|
+
return str(cache_dir)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _print_tree(root: Path) -> None:
|
|
155
|
+
print(root)
|
|
156
|
+
for path in sorted(root.rglob("*")):
|
|
157
|
+
depth = len(path.relative_to(root).parts)
|
|
158
|
+
prefix = " " * depth
|
|
159
|
+
print(f"{prefix}{path.name}")
|
geocodebr/cep.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enderecobr
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import duckdb
|
|
8
|
+
import pyarrow as pa
|
|
9
|
+
|
|
10
|
+
from .cache import caminho_parquet
|
|
11
|
+
from .db import close_geocodebr_db, create_geocodebr_db
|
|
12
|
+
from .download_cnefe import download_cnefe
|
|
13
|
+
from .errors import SemCorrespondenciaError
|
|
14
|
+
from .geo import arrow_to_geodataframe
|
|
15
|
+
from .matching import add_h3_columns
|
|
16
|
+
from .utils import (
|
|
17
|
+
normalize_h3_res,
|
|
18
|
+
sql_string
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import geopandas as gpd
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def busca_por_cep(
|
|
26
|
+
cep: int | str | list[str|int],
|
|
27
|
+
h3_res: int | list[int] | tuple[int, ...] | None = None,
|
|
28
|
+
resultado_gpd: bool = False,
|
|
29
|
+
verboso: bool = True,
|
|
30
|
+
cache: bool = True,
|
|
31
|
+
) -> pa.Table | gpd.GeoDataFrame:
|
|
32
|
+
"""Busca endereços e coordenadas a partir de CEPs.
|
|
33
|
+
|
|
34
|
+
Recebe um CEP (ou uma lista de CEPs) e retorna os endereços associados a
|
|
35
|
+
cada CEP presentes no CNEFE, com suas coordenadas geográficas. As
|
|
36
|
+
coordenadas de output utilizam o sistema de referência SIRGAS 2000,
|
|
37
|
+
EPSG 4674.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
cep : int, str ou list[int | str]
|
|
42
|
+
Um CEP ou uma lista de CEPs. CEPs duplicados são eliminados antes da
|
|
43
|
+
consulta, e o output não guarda relação de 1 para 1 com o input.
|
|
44
|
+
h3_res : int, list[int] ou None, opcional
|
|
45
|
+
Número que indica a resolução espacial das células hexagonais H3 da
|
|
46
|
+
localização dos pontos retornados. Também aceita uma lista de
|
|
47
|
+
números, e.g. `[8, 10]`. Por padrão, é `None`. Detalhes sobre as
|
|
48
|
+
resoluções disponíveis em https://h3geo.org/docs/core-library/restable/
|
|
49
|
+
resultado_gpd : bool, opcional
|
|
50
|
+
Indica se o retorno deve ser um `geopandas.GeoDataFrame` de pontos no
|
|
51
|
+
CRS SIRGAS 2000 (EPSG 4674), equivalente ao `sf` do R. Por padrão, é
|
|
52
|
+
`False`, e o retorno é um `pyarrow.Table`. Requer o extra `geo`
|
|
53
|
+
(`pip install geocodebr[geo]`).
|
|
54
|
+
verboso : bool, opcional
|
|
55
|
+
Indica se barras de progresso e mensagens devem ser exibidas durante
|
|
56
|
+
o download dos dados do CNEFE. O padrão é `True`.
|
|
57
|
+
cache : bool, opcional
|
|
58
|
+
Indica se os dados do CNEFE devem ser salvos ou lidos do cache,
|
|
59
|
+
reduzindo o tempo de processamento em chamadas futuras. O padrão é
|
|
60
|
+
`True`. Quando `False`, os dados do CNEFE são baixados para um
|
|
61
|
+
diretório temporário.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
pyarrow.Table or geopandas.GeoDataFrame
|
|
66
|
+
Os endereços presentes nos CEPs informados, com as colunas `cep`,
|
|
67
|
+
`estado`, `municipio`, `logradouro`, `localidade`, `lon` e `lat`. Um
|
|
68
|
+
mesmo CEP pode cobrir vários logradouros/localidades, de modo que o
|
|
69
|
+
output pode ter mais linhas que o número de CEPs informados. CEPs sem
|
|
70
|
+
correspondência retornam como linhas com apenas a coluna `cep`
|
|
71
|
+
preenchida. Se nenhum CEP for encontrado, a função interrompe com
|
|
72
|
+
erro.
|
|
73
|
+
"""
|
|
74
|
+
h3_values = normalize_h3_res(h3_res)
|
|
75
|
+
ceps = _normalize_ceps(cep)
|
|
76
|
+
|
|
77
|
+
cnefe_dir = download_cnefe("municipio_logradouro_cep_localidade", verboso=verboso, cache=cache)
|
|
78
|
+
con = create_geocodebr_db()
|
|
79
|
+
try:
|
|
80
|
+
path_to_parquet = caminho_parquet(
|
|
81
|
+
"municipio_logradouro_cep_localidade", cnefe_dir
|
|
82
|
+
)
|
|
83
|
+
unique_ceps = ", ".join(sql_string(value) for value in sorted(set(ceps)))
|
|
84
|
+
con.execute(
|
|
85
|
+
f"""
|
|
86
|
+
CREATE OR REPLACE TEMP TABLE output_df AS
|
|
87
|
+
SELECT cep, estado, municipio, logradouro, localidade, lon, lat
|
|
88
|
+
FROM read_parquet('{path_to_parquet}')
|
|
89
|
+
WHERE cep IN ({unique_ceps})
|
|
90
|
+
"""
|
|
91
|
+
)
|
|
92
|
+
found_ceps = {
|
|
93
|
+
row[0]
|
|
94
|
+
for row in con.execute("SELECT DISTINCT cep FROM output_df").fetchall()
|
|
95
|
+
}
|
|
96
|
+
missing = sorted(set(ceps) - found_ceps)
|
|
97
|
+
if len(missing) == len(set(ceps)):
|
|
98
|
+
raise SemCorrespondenciaError("Nenhum CEP foi encontrado.")
|
|
99
|
+
if missing:
|
|
100
|
+
values = ", ".join(f"({sql_string(value)})" for value in missing)
|
|
101
|
+
con.execute(f"INSERT INTO output_df (cep) VALUES {values}")
|
|
102
|
+
add_h3_columns(con, "output_df", h3_values)
|
|
103
|
+
result = con.execute("SELECT * FROM output_df").to_arrow_table()
|
|
104
|
+
|
|
105
|
+
if resultado_gpd:
|
|
106
|
+
return arrow_to_geodataframe(result)
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
finally:
|
|
110
|
+
close_geocodebr_db(con)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _normalize_ceps(cep: int | str | list[str|int]) -> list[str]:
|
|
114
|
+
values = cep if isinstance(cep, list) else [cep]
|
|
115
|
+
out = [enderecobr.padronizar_cep_numerico(c) if isinstance(c, int) else enderecobr.padronizar_cep(str(c)) for c in values]
|
|
116
|
+
|
|
117
|
+
return sorted(set(out))
|
geocodebr/constants.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
DATA_RELEASE = "v0.5.0"
|
|
2
|
+
|
|
3
|
+
ALL_CNEFE_FILES = [
|
|
4
|
+
"municipio_logradouro_numero_localidade.parquet",
|
|
5
|
+
"municipio_logradouro_numero_cep_localidade.parquet",
|
|
6
|
+
"municipio.parquet",
|
|
7
|
+
"municipio_cep.parquet",
|
|
8
|
+
"municipio_cep_localidade.parquet",
|
|
9
|
+
"municipio_localidade.parquet",
|
|
10
|
+
"municipio_logradouro_cep_localidade.parquet",
|
|
11
|
+
"municipio_logradouro_localidade.parquet",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
RESERVED_COLUMN_NAMES = [
|
|
15
|
+
"tempidgeocodebr", "lat", "lon", "precisao", "tipo_resultado",
|
|
16
|
+
"desvio_metros", "endereco_encontrado", "logradouro_encontrado",
|
|
17
|
+
"numero_encontrado", "cep_encontrado", "localidade_encontrada",
|
|
18
|
+
"municipio_encontrado", "estado_encontrado", "similaridade_logradouro",
|
|
19
|
+
"contagem_cnefe", "empate", "cod_setor"
|
|
20
|
+
]
|
|
21
|
+
|
geocodebr/db.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import duckdb
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_geocodebr_db(
|
|
10
|
+
db_path: str = "tempdir",
|
|
11
|
+
n_cores: int | None = None,
|
|
12
|
+
load_spatial: bool = False,
|
|
13
|
+
) -> duckdb.DuckDBPyConnection:
|
|
14
|
+
if db_path == "tempdir":
|
|
15
|
+
handle = tempfile.NamedTemporaryFile(prefix="geocodebr", suffix=".duckdb", delete=True)
|
|
16
|
+
db_file = handle.name
|
|
17
|
+
handle.close()
|
|
18
|
+
Path(db_file).unlink(missing_ok=True)
|
|
19
|
+
elif db_path == "memory":
|
|
20
|
+
db_file = ":memory:"
|
|
21
|
+
else:
|
|
22
|
+
db_file = db_path
|
|
23
|
+
|
|
24
|
+
con = duckdb.connect(db_file)
|
|
25
|
+
if n_cores is not None:
|
|
26
|
+
con.execute(f"SET threads = {n_cores}")
|
|
27
|
+
con.execute("SET enable_progress_bar = false")
|
|
28
|
+
|
|
29
|
+
if load_spatial:
|
|
30
|
+
con.execute("INSTALL spatial")
|
|
31
|
+
con.execute("LOAD spatial")
|
|
32
|
+
|
|
33
|
+
return con
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def close_geocodebr_db(con: duckdb.DuckDBPyConnection) -> None:
|
|
37
|
+
"""Fecha a conexão e apaga o arquivo do banco se for temporário do pacote.
|
|
38
|
+
|
|
39
|
+
Com ``db_path="tempdir"`` (o padrão), o DuckDB recria o arquivo no
|
|
40
|
+
``connect`` mesmo após o ``unlink`` do placeholder do NamedTemporaryFile,
|
|
41
|
+
e o arquivo permanece no disco após ``con.close()`` — um `.duckdb` por
|
|
42
|
+
chamada se acumula no diretório temporário do sistema. Bancos em memória
|
|
43
|
+
não têm arquivo associado e caminhos customizados pelo usuário são
|
|
44
|
+
preservados.
|
|
45
|
+
"""
|
|
46
|
+
paths = [
|
|
47
|
+
row[0]
|
|
48
|
+
for row in con.execute(
|
|
49
|
+
"SELECT path FROM duckdb_databases() WHERE path IS NOT NULL AND path != ''"
|
|
50
|
+
).fetchall()
|
|
51
|
+
]
|
|
52
|
+
con.close()
|
|
53
|
+
for path in paths:
|
|
54
|
+
_remove_temp_db_file(path)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _remove_temp_db_file(path: str) -> None:
|
|
58
|
+
arquivo = Path(path)
|
|
59
|
+
no_diretorio_temporario = _mesmo_diretorio(
|
|
60
|
+
arquivo.parent, Path(tempfile.gettempdir())
|
|
61
|
+
)
|
|
62
|
+
if not (
|
|
63
|
+
no_diretorio_temporario
|
|
64
|
+
and arquivo.name.startswith("geocodebr")
|
|
65
|
+
and arquivo.suffix == ".duckdb"
|
|
66
|
+
):
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
arquivo.unlink(missing_ok=True)
|
|
70
|
+
# WAL do DuckDB, caso tenha sobrado de um fechamento anormal
|
|
71
|
+
Path(str(arquivo) + ".wal").unlink(missing_ok=True)
|
|
72
|
+
except OSError:
|
|
73
|
+
# remocao cosmetica: nao deve interromper o fluxo do usuario
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _mesmo_diretorio(a: Path, b: Path) -> bool:
|
|
78
|
+
"""Compara dois diretórios resolvendo symlinks e nomes curtos.
|
|
79
|
+
|
|
80
|
+
O DuckDB canonicaliza o caminho do banco no connect: no macOS resolve
|
|
81
|
+
o symlink /var -> /private/var e no Windows pode expandir nomes curtos
|
|
82
|
+
8.3 do TEMP, de modo que a comparação textual com tempfile.gettempdir()
|
|
83
|
+
falha mesmo tratando-se do mesmo diretório.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
return a.resolve() == b.resolve()
|
|
87
|
+
except OSError:
|
|
88
|
+
return a.absolute() == b.absolute()
|
|
89
|
+
|