splaz 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.
splaz-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 João Braga
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.
splaz-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: splaz
3
+ Version: 0.1.0
4
+ Summary: API para facilitar o acesso a dados LiDAR do GeoSampa
5
+ Author: João Braga
6
+ License-File: LICENSE
7
+ Requires-Dist: requests
8
+ Requires-Dist: geopandas
9
+ Requires-Dist: geopy
10
+ Requires-Dist: shapely
11
+ Requires-Dist: laspy
12
+ Dynamic: license-file
splaz-0.1.0/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # SpLaz (Vagas Verdes)
2
+ A SpLaz é uma biblioteca Python de alto nível projetada para automatizar a obtenção de dados LiDAR (nuvens de pontos) da cidade de São Paulo via portal GeoSampa. Ela resolve complexidades de geocodificação, conversão de sistemas de coordenadas (SIRGAS 2000 / UTM 23S) e instabilidades de download de servidores legados.
3
+
4
+ Este projeto é a base técnica do projeto Vagas Verdes, focado em identificar áreas potenciais para arborização urbana utilizando sensoriamento remoto.
5
+
6
+ ## 🚀 Funcionalidades
7
+ - Geocodificação por Endereço: Identifica automaticamente qual quadrante LiDAR baixar a partir de um endereço textual (ex: "Rua Quatá, 300").
8
+
9
+ - Busca por Bairro: Lista e baixa todos os quadrantes que compõem um bairro específico.
10
+
11
+ - Tratamento de Dados Espaciais: Download e extração automática da grade de articulação (Shapefile) da prefeitura.
12
+
13
+ - Resiliência de Rede: Sistema de correção de encoding (ISO-8859-1) e verificação de integridade de arquivos ZIP.
14
+
15
+ - Gestão de Cache: Armazenamento inteligente na pasta local .geosp_laz_api para evitar downloads repetitivos e poupar banda.
16
+
17
+ ## 📦 Instalação
18
+ O próximo passo do projeto será a publicação no PyPI. Atualmente, a biblioteca pode ser instalada diretamente via pip a partir do diretório raiz:
19
+
20
+ Bash
21
+
22
+ ### Instalação em modo editável (ideal para desenvolvimento)
23
+ pip install -e .
24
+ ### 🛠 Exemplos de Uso
25
+ #### 1. Configuração Inicial
26
+ ```Python
27
+
28
+ from splaz import GeospLidarClient, GeospGeocoder
29
+
30
+ client = SpLaz()
31
+ geo = SpLazGeo(client=client)
32
+ ```
33
+ #### 2. Download por Código do Quadrante
34
+ Ideal quando você já possui o mapeamento da grade de 2020:
35
+
36
+ ```Python
37
+
38
+ quadrante = client.download_quadrante("3316-153")
39
+ quadrante.save("data/raw/laz")
40
+ ```
41
+ #### 3. Download por Endereço (Geocodificação)
42
+ A forma mais intuitiva de acessar os dados para um local específico:
43
+
44
+ ```Python
45
+
46
+ endereco = "Avenida Santo Amaro, 1826"
47
+ codigo = geo.get_quadrant_by_address(endereco)
48
+ print(f"Quadrante identificado: {codigo}")
49
+ client.download_quadrante(codigo).save("data/raw/laz")
50
+ ```
51
+ #### 4. Download por Coordenadas (Lat/Lon)
52
+ Para integração com GPS ou outros sistemas de mapeamento:
53
+
54
+ ```Python
55
+
56
+ lat, lon = -23.598, -46.676
57
+ codigo = geo.get_quadrant_by_coords(lat, lon)
58
+ client.download_quadrante(codigo).save("data/raw/laz")
59
+ ```
60
+ #### 5. Processamento por Bairro
61
+ Obtenha todos os quadrantes que interceptam a área de um bairro:
62
+
63
+ ```Python
64
+
65
+ bairro = "Itaim Bibi"
66
+ codigos = geo.get_quadrants_by_neighborhood(bairro)
67
+
68
+ for cod in codigos:
69
+ client.download_quadrante(cod).save(f"data/raw/laz/{bairro}")
70
+ ```
71
+ #### 6. Manipulando a Classe LidarQuadrante
72
+ A classe abstrai a complexidade dos binários baixados:
73
+
74
+ ```Python
75
+
76
+ quadrante = client.download_quadrante("3316-153")
77
+
78
+ # Atributos úteis
79
+ print(quadrante.codigo) # "3316-153"
80
+ print(quadrante.esta_carregado) # True
81
+ print(len(quadrante.conteudo_binario)) # Tamanho em bytes do arquivo .laz
82
+
83
+ # Salva o arquivo final no disco
84
+ quadrante.save(dest_path="output/lidar")
85
+ ```
86
+ ## 📂 Estrutura do Projeto
87
+ ```Plaintext
88
+
89
+ src/splaz/
90
+ ├── entities.py # Representação de objetos LiDAR
91
+ ├── downloader.py # Lógica de comunicação com GeoSampa
92
+ ├── geocoder.py # Cálculos espaciais e geolocalização
93
+ └── constants.py # EPSG, URLs e parâmetros de API
94
+ ```
95
+ ## 🧪 Testes Automatizados
96
+ A biblioteca possui uma suíte de testes robusta (Unitários, Integração e E2E) que valida desde a lógica de coordenadas até o download real. Para rodar:
97
+
98
+ ```Bash
99
+ pytest
100
+ ```
101
+
102
+ ### 📄 Licença
103
+ Distribuído sob a licença MIT. Veja o arquivo LICENSE para mais detalhes.
104
+ ### 🎓 Contexto Acadêmico
105
+ Este projeto foi desenvolvido como parte de um projeto de Iniciação Científica no Insper.
106
+
107
+ Autor: João Braga
108
+
109
+ Perfil: Aluno do 7° semestre de Engenharia da Computação.
110
+
111
+ Objetivo: Apoiar a análise de dados geoespaciais para o projeto ambiental "Vagas Verdes".
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splaz"
7
+ version = "0.1.0"
8
+ description = "API para facilitar o acesso a dados LiDAR do GeoSampa"
9
+ authors = [
10
+ {name = "João Braga"}
11
+ ]
12
+ dependencies = [
13
+ "requests",
14
+ "geopandas",
15
+ "geopy",
16
+ "shapely",
17
+ "laspy",
18
+ ]
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
splaz-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """
2
+ splaz: Uma biblioteca para facilitar o acesso e geocodificação
3
+ de dados LiDAR (MDS/MDT) do portal GeoSampa da cidade de São Paulo.
4
+ """
5
+
6
+ from .entities import LidarQuadrante
7
+ from .downloader import SpLaz
8
+ from .geocoder import SpLazGeo
9
+
10
+ __all__ = [
11
+ "LidarQuadrante",
12
+ "SpLaz",
13
+ "SpLazGeo",
14
+ ]
15
+
16
+ __version__ = "0.1.0"
17
+ __author__ = "João Braga"
@@ -0,0 +1,18 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ CACHE_DIR = Path.home() / ".geosp_laz_api"
5
+ GRID_CACHE_PATH = CACHE_DIR / "articulacao_2020"
6
+ SHP_FILE_NAME = "SIRGAS_SHP_quadriculamdt.shp"
7
+
8
+ GEOSAMPA_DOWNLOAD_URL = "https://download.geosampa.prefeitura.sp.gov.br/PaginasPublicas/downloadArquivo.aspx"
9
+
10
+ GRID_MDT_PARAMS_STR = "orig=DownloadCamadas&arq=21_Articulacao%20de%20Imagens%5C%5CArticula%E7%E3o%20MDT%5C%5CShapefile%5C%5CSIRGAS_SHP_quadriculamdt&arqTipo=Shapefile"
11
+
12
+ LAZ_DOWNLOAD_PARAMS = {
13
+ "orig": "DownloadMapaArticulacao",
14
+ "arq_prefix": "MDS_2020",
15
+ "arq_tipo": "MAPA_ARTICULACAO"
16
+ }
17
+
18
+ EPSG_SAO_PAULO = 31983
@@ -0,0 +1,105 @@
1
+ import requests
2
+ import zipfile
3
+ import io
4
+ import os
5
+ from datetime import datetime
6
+ from .entities import LidarQuadrante
7
+ from .constants import (
8
+ GEOSAMPA_DOWNLOAD_URL,
9
+ LAZ_DOWNLOAD_PARAMS,
10
+ GRID_MDT_PARAMS_STR,
11
+ GRID_CACHE_PATH,
12
+ )
13
+
14
+ class SpLaz:
15
+ """
16
+ Cliente para acessar e baixar dados LIDAR do portal GeoSampa.
17
+ Gerencia o download e cache da grade de articulação, além dos quadrantes específicos.
18
+ """
19
+ def __init__(self):
20
+ self.session = requests.Session()
21
+ self.session.headers.update({
22
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
23
+ })
24
+
25
+ def ensure_grid_data(self) -> str:
26
+ """Garante que a grade exista, baixando-a se necessário, e encontra o arquivo .shp."""
27
+ download_necessario = False
28
+
29
+ shp_files = list(GRID_CACHE_PATH.rglob("*.shp"))
30
+ shp_path = shp_files[0] if shp_files else None
31
+
32
+ if not shp_path:
33
+ download_necessario = True
34
+ else:
35
+ try:
36
+ response = self.session.head(f"{GEOSAMPA_DOWNLOAD_URL}?{GRID_MDT_PARAMS_STR}")
37
+ if response.status_code == 200:
38
+ server_last_modified = response.headers.get('Last-Modified')
39
+ if server_last_modified:
40
+ local_time = datetime.fromtimestamp(os.path.getmtime(shp_path))
41
+ server_time = datetime.strptime(server_last_modified, '%a, %d %b %Y %H:%M:%S GMT')
42
+ if server_time > local_time:
43
+ download_necessario = True
44
+ except Exception as e:
45
+ print(f"Aviso: Não foi possível checar atualizações ({e}). Usando cache local.")
46
+
47
+ if download_necessario:
48
+ self._download_and_extract_grid()
49
+
50
+ shp_files = list(GRID_CACHE_PATH.rglob("*.shp"))
51
+ if not shp_files:
52
+ raise FileNotFoundError("Nenhum arquivo .shp foi encontrado dentro do ZIP baixado.")
53
+ shp_path = shp_files[0]
54
+
55
+ return str(shp_path)
56
+
57
+ def _download_and_extract_grid(self):
58
+ print("Baixando grade de articulação...")
59
+ response = self.session.get(GEOSAMPA_DOWNLOAD_URL, params=GRID_MDT_PARAMS_STR)
60
+ response.raise_for_status()
61
+
62
+ if not response.content.startswith(b'PK'):
63
+ conteudo_texto = response.text[:200]
64
+ raise ValueError(
65
+ f"O GeoSampa não retornou um arquivo ZIP. Verifique os parâmetros.\n"
66
+ f"Resposta do servidor: {conteudo_texto}"
67
+ )
68
+
69
+ os.makedirs(GRID_CACHE_PATH, exist_ok=True)
70
+ with zipfile.ZipFile(io.BytesIO(response.content)) as z:
71
+ z.extractall(GRID_CACHE_PATH)
72
+ print("Grade baixada e extraída com sucesso.")
73
+
74
+ def download_quadrante(self, codigo_quadra: str) -> LidarQuadrante:
75
+ """
76
+ Baixa um quadrante LIDAR específico do GeoSampa.
77
+
78
+ Args:
79
+ codigo_quadra (str): Código do quadrante a ser baixado.
80
+
81
+ Returns:
82
+ LidarQuadrante: Objeto contendo os dados do quadrante baixado.
83
+ """
84
+ params = {
85
+ "orig": LAZ_DOWNLOAD_PARAMS["orig"],
86
+ "arq": f"{LAZ_DOWNLOAD_PARAMS['arq_prefix']}\\{codigo_quadra}.zip",
87
+ "arqTipo": LAZ_DOWNLOAD_PARAMS["arq_tipo"]
88
+ }
89
+
90
+ response = self.session.get(GEOSAMPA_DOWNLOAD_URL, params=params)
91
+ response.raise_for_status()
92
+
93
+ return self._processar_zip(codigo_quadra, response.content)
94
+
95
+ def _processar_zip(self, codigo: str, conteudo_zip: bytes) -> LidarQuadrante:
96
+ try:
97
+ with zipfile.ZipFile(io.BytesIO(conteudo_zip)) as z:
98
+ nome_laz = z.namelist()[0]
99
+ dados_laz = z.read(nome_laz)
100
+ return LidarQuadrante(codigo, dados_laz, nome_laz)
101
+ except zipfile.BadZipFile:
102
+ raise ValueError(f"Conteudo baixado para o quadrante {codigo} invalido.")
103
+
104
+ def download_lista(self, codigos: list) -> list:
105
+ return [self.download_quadrante(c) for c in codigos]
@@ -0,0 +1,50 @@
1
+ import os
2
+ import io
3
+
4
+ class LidarQuadrante:
5
+ """
6
+ Representa um quadrante LiDAR (MDS 2020) de São Paulo.
7
+ Encapsula o conteúdo binário e metadados do arquivo .laz.
8
+ """
9
+ def __init__(self, codigo: str, conteudo_binario: bytes = None, nome_arquivo: str = None):
10
+ self.codigo = codigo
11
+ self.conteudo_binario = conteudo_binario
12
+ self.nome_arquivo = nome_arquivo
13
+
14
+ @property
15
+ def esta_carregado(self) -> bool:
16
+ """Verifica se o conteúdo binário já está na memória."""
17
+ return self.conteudo_binario is not None
18
+
19
+ def save(self, pasta_destino: str) -> str:
20
+ """
21
+ Salva o conteúdo binário em disco.
22
+ Retorna o caminho completo do arquivo salvo.
23
+ """
24
+ if not self.esta_carregado:
25
+ raise ValueError(f"O quadrante {self.codigo} não possui dados em memória.")
26
+
27
+ os.makedirs(pasta_destino, exist_ok=True)
28
+ caminho_final = os.path.join(pasta_destino, self.nome_arquivo)
29
+
30
+ with open(caminho_final, 'wb') as f:
31
+ f.write(self.conteudo_binario)
32
+
33
+ return caminho_final
34
+
35
+ def to_laspy(self):
36
+ """
37
+ Retorna um objeto laspy.read() diretamente do conteúdo em memória.
38
+ Requer a biblioteca 'laspy' instalada.
39
+ """
40
+ try:
41
+ import laspy
42
+ if self.esta_carregado:
43
+ return laspy.read(io.BytesIO(self.conteudo_binario))
44
+ except ImportError:
45
+ raise ImportError("A biblioteca 'laspy' é necessária para converter dados em memória.")
46
+ return None
47
+
48
+ def __repr__(self):
49
+ status = "Carregado" if self.esta_carregado else "Vazio"
50
+ return f"<LidarQuadrante {self.codigo} | {status}>"
@@ -0,0 +1,90 @@
1
+ import geopandas as gpd
2
+ from geopy.geocoders import Nominatim
3
+ from shapely.geometry import Point, box
4
+ from .constants import EPSG_SAO_PAULO
5
+ from .downloader import SpLaz
6
+
7
+ class SpLazGeo:
8
+ """
9
+ Gerenciador de geocodificação para dados LIDAR do GeoSampa.
10
+ """
11
+ def __init__(self, client: SpLaz = None):
12
+ self.client = client or SpLaz()
13
+ self._grid = None
14
+ self.geolocator = Nominatim(user_agent="geosp_laz_api_vagas_verdes")
15
+ @property
16
+ def grid(self) -> gpd.GeoDataFrame:
17
+ """
18
+ Carrega e prepara o Shapefile de articulação automaticamente.
19
+ Mantém os dados no Ryzen 7 para buscas rápidas.
20
+ """
21
+ if self._grid is None:
22
+
23
+ shp_path = self.client.ensure_grid_data()
24
+
25
+
26
+ gdf = gpd.read_file(shp_path)
27
+
28
+
29
+ if gdf.crs is None:
30
+
31
+ gdf = gdf.set_crs(epsg=EPSG_SAO_PAULO)
32
+ elif gdf.crs.to_epsg() != EPSG_SAO_PAULO:
33
+
34
+ gdf = gdf.to_crs(epsg=EPSG_SAO_PAULO)
35
+
36
+ self._grid = gdf
37
+ return self._grid
38
+
39
+ def get_quadrant_by_coords(self, lat: float, lon: float) -> str:
40
+ """
41
+ Determina o quadrante LIDAR correspondente a coordenadas geográficas.
42
+ Args:
43
+ lat (float): Latitude em graus decimais.
44
+ lon (float): Longitude em graus decimais.
45
+ Returns:
46
+ str: Código do quadrante LIDAR correspondente.
47
+ """
48
+ ponto_gps = gpd.GeoSeries([Point(lon, lat)], crs="EPSG:4326")
49
+ ponto_sp = ponto_gps.to_crs(epsg=EPSG_SAO_PAULO).iloc[0]
50
+ resultado = self.grid[self.grid.contains(ponto_sp)]
51
+ if resultado.empty:
52
+ raise ValueError(f"Coordenadas ({lat}, {lon}) fora da cobertura de SP.")
53
+ return str(resultado.iloc[0]['qmdt_cod'])
54
+
55
+ def get_quadrant_by_address(self, address: str) -> str:
56
+ """
57
+ Determina o quadrante LIDAR correspondente a um endereço.
58
+ Args:
59
+ address (str): Endereço completo ou parcial em São Paulo.
60
+ Returns:
61
+ str: Código do quadrante LIDAR correspondente.
62
+ """
63
+ search_query = f"{address}, São Paulo, SP, Brazil"
64
+ location = self.geolocator.geocode(search_query)
65
+ if not location:
66
+ raise ValueError(f"Endereço não localizado: {address}")
67
+ return self.get_quadrant_by_coords(location.latitude, location.longitude)
68
+
69
+ def get_quadrants_by_neighborhood(self, neighborhood: str) -> list[str]:
70
+ """
71
+ Retorna uma lista de quadrantes LIDAR que intersectam um bairro específico.
72
+ Args:
73
+ neighborhood (str): Nome do bairro em São Paulo.
74
+ Returns:
75
+ list[str]: Lista de códigos de quadrantes LIDAR que intersectam o bairro.
76
+ """
77
+ query = f"{neighborhood}, São Paulo, SP, Brazil"
78
+ location = self.geolocator.geocode(query)
79
+ if not location:
80
+ raise ValueError(f"Bairro não encontrado: {neighborhood}")
81
+
82
+ bbox = location.raw['boundingbox']
83
+ lat_min, lat_max, lon_min, lon_max = float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])
84
+
85
+ area_gps = gpd.GeoSeries([box(lon_min, lat_min, lon_max, lat_max)], crs="EPSG:4326")
86
+ area_sp = area_gps.to_crs(epsg=EPSG_SAO_PAULO).iloc[0]
87
+
88
+ intersecao = self.grid[self.grid.intersects(area_sp)]
89
+
90
+ return intersecao['qmdt_cod'].unique().tolist()
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: splaz
3
+ Version: 0.1.0
4
+ Summary: API para facilitar o acesso a dados LiDAR do GeoSampa
5
+ Author: João Braga
6
+ License-File: LICENSE
7
+ Requires-Dist: requests
8
+ Requires-Dist: geopandas
9
+ Requires-Dist: geopy
10
+ Requires-Dist: shapely
11
+ Requires-Dist: laspy
12
+ Dynamic: license-file
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/splaz/__init__.py
5
+ src/splaz/constants.py
6
+ src/splaz/downloader.py
7
+ src/splaz/entities.py
8
+ src/splaz/geocoder.py
9
+ src/splaz.egg-info/PKG-INFO
10
+ src/splaz.egg-info/SOURCES.txt
11
+ src/splaz.egg-info/dependency_links.txt
12
+ src/splaz.egg-info/requires.txt
13
+ src/splaz.egg-info/top_level.txt
14
+ tests/test_downloader.py
15
+ tests/test_end2end.py
16
+ tests/test_entities.py
17
+ tests/test_geocoder.py
@@ -0,0 +1,5 @@
1
+ requests
2
+ geopandas
3
+ geopy
4
+ shapely
5
+ laspy
@@ -0,0 +1 @@
1
+ splaz
@@ -0,0 +1,52 @@
1
+ import pytest
2
+ import requests_mock
3
+ import io
4
+ import zipfile
5
+ import os
6
+ from splaz.downloader import SpLaz
7
+ from splaz.entities import LidarQuadrante
8
+ from splaz.constants import GEOSAMPA_DOWNLOAD_URL
9
+
10
+
11
+ @pytest.fixture
12
+ def client():
13
+ return SpLaz()
14
+
15
+ @pytest.fixture
16
+ def fake_zip_content():
17
+ buf = io.BytesIO()
18
+ with zipfile.ZipFile(buf,"w") as z:
19
+ z.writestr("3316-153.laz", b"fake_laz_binary_content")
20
+ return buf.getvalue()
21
+
22
+ def test_download_quadrante_sucesso(client, requests_mock, fake_zip_content):
23
+ requests_mock.get(GEOSAMPA_DOWNLOAD_URL, content=fake_zip_content, status_code = 200)
24
+
25
+ resultado = client.download_quadrante("3316-153")
26
+
27
+ assert isinstance(resultado,LidarQuadrante)
28
+ assert resultado.codigo == "3316-153"
29
+ assert resultado.esta_carregado is True
30
+ assert resultado.conteudo_binario == b"fake_laz_binary_content"
31
+
32
+
33
+ def test_download_quadrante_erro_404(client,requests_mock):
34
+ requests_mock.get(GEOSAMPA_DOWNLOAD_URL, status_code = 404 )
35
+ import requests
36
+
37
+ with pytest.raises(requests.exceptions.HTTPError):
38
+ client.download_quadrante("codigo-inexistente")
39
+
40
+ def test_processar_zip_invalido(client):
41
+ codigo = "test"
42
+ with pytest.raises(ValueError, match= f"Conteudo baixado para o quadrante {codigo} invalido."):
43
+ client._processar_zip(codigo, b"not_a_zip_file")
44
+
45
+ def test_download_lista_chama_multiplos(client, requests_mock, fake_zip_content):
46
+ requests_mock.get(GEOSAMPA_DOWNLOAD_URL, content=fake_zip_content)
47
+
48
+ codigos = ["1111-111", "2222-222"]
49
+ resultados = client.download_lista(codigos)
50
+
51
+ assert len(resultados) == 2
52
+ assert requests_mock.call_count == 2
@@ -0,0 +1,34 @@
1
+ import pytest
2
+ from splaz import SpLazGeo, SpLaz
3
+
4
+ def test_fluxo_completo_insper_vagas_verdes():
5
+ """
6
+ TESTE DE SISTEMA (E2E):
7
+ Valida se a biblioteca consegue converter um endereço real,
8
+ identificar o quadrante e baixar o arquivo binário sem erros.
9
+ """
10
+ # 1. Inicialização (Sem mocks!)
11
+ client = SpLaz()
12
+ geo = SpLazGeo(client=client)
13
+
14
+ # Endereço real do Insper
15
+ endereco = "Avenida Santo Amaro, 1826"
16
+
17
+ # 2. Execução do fluxo
18
+ # O Geocoder deve baixar a grade automaticamente no primeiro uso
19
+ codigo_quadrante = geo.get_quadrant_by_address(endereco)
20
+
21
+ # 3. Verificação do Geocoding
22
+ # Sabemos que o Insper fica no quadrante 3316-153
23
+ assert codigo_quadrante == "3316-153"
24
+
25
+ # 4. Download Real
26
+ # Baixamos apenas um para não sobrecarregar sua rede
27
+ quadrante_obj = client.download_quadrante(codigo_quadrante)
28
+
29
+ # 5. Verificações Finais
30
+ assert quadrante_obj.esta_carregado is True
31
+ assert len(quadrante_obj.conteudo_binario) > 0
32
+ assert quadrante_obj.nome_arquivo.endswith(".laz")
33
+
34
+ print(f"\n[E2E] Sucesso: Quadrante {codigo_quadrante} baixado com {len(quadrante_obj.conteudo_binario)} bytes.")
@@ -0,0 +1,37 @@
1
+ import pytest
2
+ import os
3
+ from splaz.entities import LidarQuadrante
4
+
5
+ def test_lidar_quadrante_inicializacao_vazia():
6
+ quad = LidarQuadrante("3316-153")
7
+ assert quad.codigo == "3316-153"
8
+ assert quad.esta_carregado is False
9
+ assert "Vazio" in repr(quad)
10
+
11
+ def test_lidar_quadrante_inicializa_com_dados():
12
+ dados_falsos = b"LAZ_BINARY_DATA"
13
+ quad = LidarQuadrante(codigo="3316-153", conteudo_binario=dados_falsos, nome_arquivo="3316-153.laz")
14
+
15
+ assert quad.esta_carregado is True
16
+ assert quad.conteudo_binario == dados_falsos
17
+ assert "Carregado" in repr(quad)
18
+
19
+ def test_save_com_sucesso(tmp_path):
20
+ d = tmp_path / "data_test"
21
+ d.mkdir()
22
+ nome_arq = "test_vagas_verdes.laz"
23
+ dados = b"01010101"
24
+
25
+ quad = LidarQuadrante("3316-153", dados, nome_arq)
26
+ caminho_salvo = quad.save(str(d))
27
+
28
+ assert os.path.exists(caminho_salvo)
29
+ with open(caminho_salvo, "rb") as f:
30
+ assert f.read() == dados
31
+
32
+ def test_save_sem_dados_levanta_erro():
33
+ codigo = "3316-153"
34
+ quad = LidarQuadrante(codigo)
35
+ with pytest.raises(ValueError, match=f"O quadrante {codigo} não possui dados em memória."):
36
+ quad.save("pasta_qualquer")
37
+
@@ -0,0 +1,73 @@
1
+ import pytest
2
+ import geopandas as gpd
3
+ import pandas as pd
4
+ from shapely.geometry import Point, Polygon, box
5
+ from unittest.mock import MagicMock, patch
6
+ from splaz.geocoder import SpLazGeo
7
+
8
+
9
+ @pytest.fixture
10
+ def mock_client():
11
+ client = MagicMock()
12
+ client.ensure_grid_data.return_value = "caminho/falso/grade.shp"
13
+ return client
14
+
15
+ @pytest.fixture
16
+ def mock_grid_gdf():
17
+ """Cria um GeoDataFrame minúsculo para simular a grade de SP."""
18
+ # Quadrado simples representando um quadrante fictício
19
+ poly = Polygon([(0, 0), (1000, 0), (1000, 1000), (0, 1000)])
20
+ gdf = gpd.GeoDataFrame(
21
+ {'qmdt_cod': ['3316-153'], 'geometry': [poly]},
22
+ crs="EPSG:31983"
23
+ )
24
+ return gdf
25
+
26
+
27
+ def test_get_quadrant_by_coords_sucesso(mock_client, mock_grid_gdf):
28
+ with patch("geopandas.read_file", return_value = mock_grid_gdf):
29
+ geo = SpLazGeo(client=mock_client)
30
+
31
+ with patch.object(gpd.GeoSeries, "to_crs") as mock_to_crs:
32
+ mock_to_crs.return_value = gpd.GeoSeries([Point(500,500)])
33
+
34
+ codigo = geo.get_quadrant_by_coords(-23.5,-46.6)
35
+ assert codigo == '3316-153'
36
+
37
+ def test_get_quadrant_by_address_sucesso(mock_client, mock_grid_gdf):
38
+ with patch("geopandas.read_file", return_value=mock_grid_gdf):
39
+ geo = SpLazGeo(client=mock_client)
40
+
41
+ # Mock do retorno do Nominatim (Geopy)
42
+ mock_location = MagicMock()
43
+ mock_location.latitude = -23.59
44
+ mock_location.longitude = -46.67
45
+
46
+ with patch.object(geo.geolocator, "geocode", return_value=mock_location):
47
+ with patch.object(geo, "get_quadrant_by_coords", return_value="3316-153"):
48
+ codigo = geo.get_quadrant_by_address("Rua Quatá, 300")
49
+ assert codigo == "3316-153"
50
+
51
+ def test_get_quadrants_by_neighborhood_bbox(mock_client,mock_grid_gdf):
52
+ with patch("geopandas.read_file", return_value=mock_grid_gdf):
53
+ geo = SpLazGeo(client=mock_client)
54
+
55
+ # 1. Simula o retorno do Nominatim (Bairro real)
56
+ mock_location = MagicMock()
57
+ mock_location.raw = {
58
+ 'boundingbox': ['-23.6', '-23.5', '-46.7', '-46.6']
59
+ }
60
+
61
+ with patch.object(geo.geolocator, "geocode", return_value=mock_location):
62
+ # 2. Mockamos a conversão de coordenadas para que a área resultante
63
+ # coincida com o nosso quadrante de teste (0 a 1000)
64
+ with patch.object(gpd.GeoSeries, "to_crs") as mock_to_crs:
65
+ # Criamos um polígono que sobrepõe o nosso mock_grid_gdf (0,0 a 1000,1000)
66
+ overlap_box = box(100, 100, 500, 500)
67
+ mock_to_crs.return_value = gpd.GeoSeries([overlap_box])
68
+
69
+ quadrantes = geo.get_quadrants_by_neighborhood("Vila Olímpia")
70
+
71
+ # Agora deve funcionar, pois as geometrias se interceptam!
72
+ assert isinstance(quadrantes, list)
73
+ assert "3316-153" in quadrantes