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 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)
@@ -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
+