ESCatastroLib 0.0.1rc5__tar.gz → 1.0.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.
Files changed (28) hide show
  1. escatastrolib-0.0.1rc5/README.md → escatastrolib-1.0.0/PKG-INFO +58 -0
  2. escatastrolib-0.0.1rc5/PKG-INFO → escatastrolib-1.0.0/README.md +28 -28
  3. escatastrolib-1.0.0/TODO_Python314_Compatibility.md +40 -0
  4. escatastrolib-1.0.0/TODO_Refactorizacion.md +34 -0
  5. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/pyproject.toml +26 -10
  6. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/__about__.py +1 -1
  7. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/models/InfoCatastral.py +306 -207
  8. escatastrolib-0.0.1rc5/src/escatastrolib/utils/test_plantas.py +0 -109
  9. escatastrolib-0.0.1rc5/src/escatastrolib/utils/test_valor_catastral_tierras.py +0 -136
  10. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/.gitignore +0 -0
  11. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/CODE_OF_CONDUCT.md +0 -0
  12. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/CONTRIBUTING.md +0 -0
  13. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/LICENSE +0 -0
  14. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/__init__.py +0 -0
  15. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/models/Calle.py +0 -0
  16. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/models/Municipio.py +0 -0
  17. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/models/__init__.py +0 -0
  18. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/utils/__init__.py +0 -0
  19. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/utils/converters.py +0 -0
  20. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/utils/exceptions.py +0 -0
  21. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/utils/statics.py +0 -0
  22. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/src/escatastrolib/utils/utils.py +0 -0
  23. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/tests/__init__.py +0 -0
  24. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/tests/test_calle.py +0 -0
  25. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/tests/test_import.py +0 -0
  26. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/tests/test_info_catastral.py +0 -0
  27. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/tests/test_municipio.py +0 -0
  28. {escatastrolib-0.0.1rc5 → escatastrolib-1.0.0}/tests/test_utils.py +0 -0
@@ -1,3 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: ESCatastroLib
3
+ Version: 1.0.0
4
+ Summary: Una librería de Python para consultar los datos del Catastro COMO OBJETOS. Incluye información geográfica.
5
+ Project-URL: Documentation, https://github.com/IvanitiX/ESCatastroLib#readme
6
+ Project-URL: Issues, https://github.com/IvanitiX/ESCatastroLib/issues
7
+ Project-URL: Source, https://github.com/IvanitiX/ESCatastroLib
8
+ Author-email: "Iván V.R" <IvanVR@protonmail.com>
9
+ License: Apache License (2.0)
10
+ License-File: LICENSE
11
+ Keywords: catastro,espana,gis,spain
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Natural Language :: Spanish
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Dist: folium
20
+ Requires-Dist: geopandas<0.14; python_version < '3.9'
21
+ Requires-Dist: geopandas>=0.14; python_version >= '3.9'
22
+ Requires-Dist: geopy
23
+ Requires-Dist: pyarrow<14; python_version < '3.9'
24
+ Requires-Dist: pyarrow>=14; python_version >= '3.9'
25
+ Requires-Dist: requests
26
+ Requires-Dist: shapely<2; python_version < '3.9'
27
+ Requires-Dist: shapely>=2; python_version >= '3.9'
28
+ Requires-Dist: xmltodict
29
+ Description-Content-Type: text/markdown
30
+
1
31
  # ESCatastroLib
2
32
 
3
33
  La librería ESCatastroLib proporciona una interfaz sencilla para consultar el Catastro de España y obtener información geográfica precisa. Con esta librería, puedes acceder a datos como la ubicación de parcelas, características de construcción y valores geográficos.
@@ -88,6 +118,34 @@ referencia a una MetaParcela.
88
118
  * `regiones (list)`: Una lista de regiones en la parcela con una descripción y superficie.
89
119
  * `centroide (dict)`: Las coordenadas del centroide de la parcela. (Latitud y longitud)
90
120
  * `geometria (list)`: Una lista de puntos que representan la geometría de la parcela. (Latitud y longitud)
121
+ * `superficie_total (float)`: La superficie total de la parcela en metros cuadrados.
122
+ * `superficie_construida (float)`: La superficie construida en metros cuadrados.
123
+ * `superficie (float)`: La superficie total de las regiones en metros cuadrados.
124
+
125
+ **Métodos:**
126
+
127
+ * `distancias_aristas` (property): Calcula las distancias entre puntos consecutivos de la geometría de la parcela.
128
+ * Returns: `Optional[List[float]]` - Lista de distancias entre aristas consecutivas en metros, o None si no hay geometría.
129
+
130
+ * `perimetro` (property): Calcula el perímetro total de la geometría de la parcela.
131
+ * Returns: `Optional[float]` - Perímetro total en metros, o None si no hay geometría.
132
+
133
+ * `valor_catastral_urbano_m2(anio: int) -> Optional[float]`: Obtiene el valor catastral por metro cuadrado para parcelas urbanas.
134
+ * Args: `anio` (int) - Año del valor catastral a consultar.
135
+ * Returns: Valor catastral en €/m², 0 si es rústica, o None si hay algún error.
136
+
137
+ * `valor_catastral_rustico_m2(anio: str) -> Optional[Dict]`: Obtiene los valores catastrales de tierras para parcelas rústicas.
138
+ * Args: `anio` (str) - Año del valor catastral a consultar.
139
+ * Returns: Diccionario con `region`, `nombre_region` y `modulos_€/ha`, o None si no se encuentran.
140
+
141
+ * `numero_plantas` (property): Obtiene el número de plantas de un edificio.
142
+ * Returns: `Dict[str, Any]` con claves `plantas`, `sotanos` y `total`.
143
+
144
+ * `to_dataframe()`: Convierte la parcela en un GeoDataFrame de GeoPandas.
145
+ * `to_json(filename: Optional[str] = None) -> str`: Convierte la parcela a JSON.
146
+ * `to_csv(filename: Optional[str] = None) -> str`: Convierte la parcela a CSV.
147
+ * `to_shapefile(filename: str)`: Guarda la parcela como Shapefile.
148
+ * `to_parquet(filename: str)`: Guarda la parcela como Parquet.
91
149
 
92
150
 
93
151
  ## Clase MetaParcela
@@ -1,31 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: ESCatastroLib
3
- Version: 0.0.1rc5
4
- Summary: Una librería de Python para consultar los datos del Catastro COMO OBJETOS. Incluye información geográfica.
5
- Project-URL: Documentation, https://github.com/IvanitiX/ESCatastroLib#readme
6
- Project-URL: Issues, https://github.com/IvanitiX/ESCatastroLib/issues
7
- Project-URL: Source, https://github.com/IvanitiX/ESCatastroLib
8
- Author-email: "Iván V.R" <IvanVR@protonmail.com>
9
- License: Apache License (2.0)
10
- License-File: LICENSE
11
- Keywords: catastro,espana,gis,spain
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: Apache Software License
15
- Classifier: Natural Language :: Spanish
16
- Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Topic :: Software Development :: Build Tools
19
- Requires-Dist: folium
20
- Requires-Dist: geopandas
21
- Requires-Dist: geopy
22
- Requires-Dist: pyarrow
23
- Requires-Dist: pytest
24
- Requires-Dist: requests
25
- Requires-Dist: shapely
26
- Requires-Dist: xmltodict
27
- Description-Content-Type: text/markdown
28
-
29
1
  # ESCatastroLib
30
2
 
31
3
  La librería ESCatastroLib proporciona una interfaz sencilla para consultar el Catastro de España y obtener información geográfica precisa. Con esta librería, puedes acceder a datos como la ubicación de parcelas, características de construcción y valores geográficos.
@@ -116,6 +88,34 @@ referencia a una MetaParcela.
116
88
  * `regiones (list)`: Una lista de regiones en la parcela con una descripción y superficie.
117
89
  * `centroide (dict)`: Las coordenadas del centroide de la parcela. (Latitud y longitud)
118
90
  * `geometria (list)`: Una lista de puntos que representan la geometría de la parcela. (Latitud y longitud)
91
+ * `superficie_total (float)`: La superficie total de la parcela en metros cuadrados.
92
+ * `superficie_construida (float)`: La superficie construida en metros cuadrados.
93
+ * `superficie (float)`: La superficie total de las regiones en metros cuadrados.
94
+
95
+ **Métodos:**
96
+
97
+ * `distancias_aristas` (property): Calcula las distancias entre puntos consecutivos de la geometría de la parcela.
98
+ * Returns: `Optional[List[float]]` - Lista de distancias entre aristas consecutivas en metros, o None si no hay geometría.
99
+
100
+ * `perimetro` (property): Calcula el perímetro total de la geometría de la parcela.
101
+ * Returns: `Optional[float]` - Perímetro total en metros, o None si no hay geometría.
102
+
103
+ * `valor_catastral_urbano_m2(anio: int) -> Optional[float]`: Obtiene el valor catastral por metro cuadrado para parcelas urbanas.
104
+ * Args: `anio` (int) - Año del valor catastral a consultar.
105
+ * Returns: Valor catastral en €/m², 0 si es rústica, o None si hay algún error.
106
+
107
+ * `valor_catastral_rustico_m2(anio: str) -> Optional[Dict]`: Obtiene los valores catastrales de tierras para parcelas rústicas.
108
+ * Args: `anio` (str) - Año del valor catastral a consultar.
109
+ * Returns: Diccionario con `region`, `nombre_region` y `modulos_€/ha`, o None si no se encuentran.
110
+
111
+ * `numero_plantas` (property): Obtiene el número de plantas de un edificio.
112
+ * Returns: `Dict[str, Any]` con claves `plantas`, `sotanos` y `total`.
113
+
114
+ * `to_dataframe()`: Convierte la parcela en un GeoDataFrame de GeoPandas.
115
+ * `to_json(filename: Optional[str] = None) -> str`: Convierte la parcela a JSON.
116
+ * `to_csv(filename: Optional[str] = None) -> str`: Convierte la parcela a CSV.
117
+ * `to_shapefile(filename: str)`: Guarda la parcela como Shapefile.
118
+ * `to_parquet(filename: str)`: Guarda la parcela como Parquet.
119
119
 
120
120
 
121
121
  ## Clase MetaParcela
@@ -0,0 +1,40 @@
1
+ # TODO: Python 3.8-3.14 Compatibility Update
2
+
3
+ ## Objective
4
+ Update the ESCatastroLib library to be compatible with Python versions 3.8 through 3.14.
5
+
6
+ ## Tasks
7
+
8
+ ### 1. Update pyproject.toml
9
+ - [x] Add "3.14" to the test matrix under `[tool.hatch.envs.test.matrix]`
10
+ - [ ] Ensure all dependencies support Python 3.8-3.14 range
11
+
12
+ ### 2. Code Review and Updates
13
+ - [x] Review `src/escatastrolib/models/InfoCatastral.py` for deprecated patterns
14
+ - [x] Review `src/escatastrolib/utils/utils.py` for deprecated patterns
15
+ - [x] Check for any `list(dict.values())[0]` patterns that may need updating
16
+ - [x] Verify `typing` imports are compatible with all Python versions
17
+
18
+ ### 3. Testing
19
+ - [x] Verify the library can be imported successfully
20
+ - [x] Run existing tests to ensure compatibility
21
+ - [x] Check for any deprecation warnings
22
+
23
+ ### 4. Documentation
24
+ - [ ] Update README.md if needed
25
+ - [ ] Verify classifiers in pyproject.toml are accurate
26
+
27
+ ## Notes
28
+ - Must maintain backward compatibility with Python 3.8
29
+ - Python 3.14 may have stricter type checking and deprecation warnings
30
+ - Some patterns may work but trigger deprecation warnings
31
+
32
+ ## Progress
33
+ - [x] Analyzed codebase structure
34
+ - [x] Reviewed pyproject.toml configuration
35
+ - [x] Reviewed main source files for potential compatibility issues
36
+ - [x] Implemented updates for Python 3.14 compatibility
37
+ - [x] Updated pyproject.toml to include Python 3.14 in test matrix
38
+ - [x] Verified library imports successfully
39
+ - [x] Ran tests to verify compatibility
40
+
@@ -0,0 +1,34 @@
1
+ # TODO: Refactorización de InfoCatastral.py
2
+
3
+ ## Objetivo
4
+ Refactorizar el archivo `src/escatastrolib/models/InfoCatastral.py` para mejorar:
5
+ - Eliminación de código duplicado entre `ParcelaCatastral` y `MetaParcela`
6
+ - Mejor manejo de errores
7
+ - Type hints en todos los métodos
8
+ - Docstrings mejorados
9
+
10
+ ## Cambios de Nomenclatura
11
+ - `BaseParcela` → `ParcelaHelper`
12
+ - `_make_api_request` → `_llamar_a_api`
13
+
14
+ ## Estado Actual (Actualizado: 2024)
15
+ - ✅ Fase 1: Extracción de funcionalidades comunes - COMPLETADA
16
+ - ✅ Fase 2: Refactorización de ParcelaCatastral - COMPLETADA
17
+ - ✅ Fase 3: Refactorización de MetaParcela - COMPLETADA
18
+ - ✅ Fase 4: Validación - COMPLETADA (10 tests pasan)
19
+ - ✅ Fase 5: Type Hints y Docstrings Faltantes - COMPLETADA
20
+ - ✅ `__create_regions()` - type hints y docstring agregados
21
+ - ✅ `__create_geometry()` - type hints y docstring agregados, usa `_llamar_a_api()`
22
+ - ✅ `distancias_aristas` - type hints agregados (`Optional[List[float]]`)
23
+ - ✅ `perimetro` - type hints agregados (`Optional[float]`)
24
+ - ✅ `valor_catastral_urbano_m2()` - type hints agregados, usa `_llamar_a_api()`
25
+ - ✅ `numero_plantas` - type hints y docstring mejorados
26
+ - ✅ `valor_catastral_rustico_m2()` - type hints en parámetro
27
+
28
+ ## Tests
29
+ - ✅ Todos los 10 tests pasan sin regresiones
30
+
31
+ ## Notas
32
+ - Mantener compatibilidad con la API existente
33
+ - No cambiar la firma pública de los métodos
34
+
@@ -21,15 +21,25 @@ classifiers = [
21
21
  "Operating System :: OS Independent",
22
22
  "Natural Language :: Spanish"
23
23
  ]
24
+
24
25
  dependencies = [
25
- "geopandas",
26
- "shapely",
27
- "pyarrow",
28
- "pytest",
29
- "requests",
30
- "xmltodict",
31
- "folium",
32
- "geopy"
26
+ # comunes (siempre)
27
+ "requests",
28
+ "xmltodict",
29
+ "folium",
30
+ "geopy",
31
+
32
+ # --- shapely ---
33
+ "shapely<2; python_version<'3.9'",
34
+ "shapely>=2; python_version>='3.9'",
35
+
36
+ # --- geopandas ---
37
+ "geopandas<0.14; python_version<'3.9'",
38
+ "geopandas>=0.14; python_version>='3.9'",
39
+
40
+ # --- pyarrow ---
41
+ "pyarrow<14; python_version<'3.9'",
42
+ "pyarrow>=14; python_version>='3.9'",
33
43
  ]
34
44
 
35
45
 
@@ -45,11 +55,17 @@ path = "src/escatastrolib/__about__.py"
45
55
  extra-dependencies = [
46
56
  "mypy>=1.0.0",
47
57
  ]
58
+
48
59
  [tool.hatch.envs.types.scripts]
49
60
  check = "mypy --install-types --non-interactive {args:src/escatastrolib tests}"
50
61
 
51
- [[tool.hatch.envs.test.matrix]]
52
- python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
62
+ [tool.hatch.envs.hatch-test]
63
+ dependencies = [
64
+ "pytest",
65
+ ]
66
+
67
+ [[tool.hatch.envs.hatch-test.matrix]]
68
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
53
69
 
54
70
  [tool.coverage.run]
55
71
  source_pkgs = ["escatastrolib", "tests"]
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Iván V.R <IvanVR@protonmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = '0.0.1rc5'
4
+ __version__ = '1.0.0'
@@ -3,8 +3,8 @@ import json
3
3
  import xmltodict
4
4
  from datetime import datetime
5
5
 
6
- from shapely import Point
7
- from typing import Union
6
+ from shapely.geometry import Point
7
+ from typing import Union, Dict, Any, Optional, List
8
8
  from pyproj import Transformer
9
9
 
10
10
  from ..utils.statics import URL_BASE_CALLEJERO, URL_BASE_GEOGRAFIA, URL_BASE_CROQUIS_DATOS, URL_BASE_MAPA_VALORES_URBANOS, URL_BASE_MAPA_VALORES_RUSTICOS, URL_BASE_WFS_EDIFICIOS, CULTIVOS
@@ -13,7 +13,98 @@ from ..utils.exceptions import ErrorServidorCatastro
13
13
  from ..utils import converters
14
14
  from .Calle import Calle, Municipio
15
15
 
16
- class ParcelaCatastral:
16
+
17
+ class ParcelaHelper:
18
+ """Clase base con métodos compartidos para operaciones de parcelas catastrales."""
19
+
20
+ def _llamar_a_api(self, url: str, params: Dict[str, Any], timeout: int = 30) -> requests.Response:
21
+ """Realiza una petición HTTP a la API del Catastro.
22
+
23
+ Args:
24
+ url: URL del endpoint a consultar
25
+ params: Parámetros de la petición HTTP
26
+ timeout: Tiempo máximo de espera en segundos
27
+
28
+ Returns:
29
+ Objeto Response con la respuesta del servidor
30
+
31
+ Raises:
32
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
33
+ """
34
+ response = requests.get(url, params=params, timeout=timeout)
35
+
36
+ if len(response.content) == 0:
37
+ raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacía")
38
+
39
+ return response
40
+
41
+ def _parsear_respuesta(self, response: requests.Response) -> Dict[str, Any]:
42
+ """Parsea la respuesta JSON de la API del Catastro.
43
+
44
+ Args:
45
+ response: Objeto Response de la petición HTTP
46
+
47
+ Returns:
48
+ Diccionario con los datos parseados
49
+
50
+ Raises:
51
+ ErrorServidorCatastro: Si el servidor no devuelve un JSON válido
52
+ """
53
+ try:
54
+ return response.json()
55
+ except json.JSONDecodeError:
56
+ raise ErrorServidorCatastro(
57
+ mensaje=f"El servidor no devuelve un JSON. Mensaje en bruto: {response.content}"
58
+ )
59
+
60
+ def _parametrizar_peticion(self, **kwargs) -> Dict[str, Any]:
61
+ """Construye un diccionario de parámetros para la petición HTTP.
62
+
63
+ Args:
64
+ **kwargs: Parámetros clave-valor para la petición
65
+
66
+ Returns:
67
+ Diccionario con los parámetros filtrados (solo valores no nulos)
68
+ """
69
+ return {k: v for k, v in kwargs.items() if v is not None}
70
+
71
+ def _comprobar_errores_catastro(self, info_cadastre: Dict[str, Any]) -> bool:
72
+ """Verifica si la respuesta del Catastro contiene errores.
73
+
74
+ Args:
75
+ info_cadastre: Diccionario con la respuesta del Catastro
76
+
77
+ Returns:
78
+ True si no hay errores, False si hay errores
79
+ """
80
+ return comprobar_errores(info_cadastre)
81
+
82
+ def _obtener_numero_parcelas(self, info_cadastre: Dict[str, Any], consulta_key: str) -> int:
83
+ """Obtiene el número de parcelas de una respuesta del Catastro.
84
+
85
+ Args:
86
+ info_cadastre: Diccionario con la respuesta del Catastro
87
+ consulta_key: Clave de la consulta ('dnprc', 'dnppp', 'dnploc')
88
+
89
+ Returns:
90
+ Número de parcelas encontradas
91
+ """
92
+ result_key = f"consulta_{consulta_key}Result"
93
+ return info_cadastre.get(result_key, {}).get("control", {}).get("cudnp", 1)
94
+
95
+ def _extraer_rc_from_dict(self, rc_dict: Dict[str, Any]) -> str:
96
+ """Extrae la referencia catastral de un diccionario.
97
+
98
+ Args:
99
+ rc_dict: Diccionario que contiene la referencia catastral
100
+
101
+ Returns:
102
+ Referencia catastral como string
103
+ """
104
+ return ''.join(rc_dict.values())
105
+
106
+
107
+ class ParcelaCatastral(ParcelaHelper):
17
108
  """
18
109
  Clase que representa una parcela catastral.
19
110
  Args:
@@ -48,7 +139,15 @@ class ParcelaCatastral:
48
139
  geometria (list): Una lista de puntos que representan la geometría de la parcela.
49
140
  """
50
141
 
51
- def __create_regions(self, info_cadastre: dict):
142
+ def __create_regions(self, info_cadastre: Dict[str, Any]) -> None:
143
+ """Crea la lista de regiones de la parcela a partir de los datos del Catastro.
144
+
145
+ Args:
146
+ info_cadastre: Diccionario con la respuesta del Catastro.
147
+
148
+ Attributes:
149
+ regiones: Lista de diccionarios con 'descripcion' y 'superficie' de cada región.
150
+ """
52
151
  self.regiones = []
53
152
  if self.tipo == 'Urbano':
54
153
  iterator = list(info_cadastre.values())[0].get('bico').get('lcons')
@@ -67,18 +166,26 @@ class ParcelaCatastral:
67
166
  })
68
167
 
69
168
 
70
- def __create_geometry(self, projection: str = 'EPSG:4326'):
71
- geometry_request = requests.get(f'{URL_BASE_GEOGRAFIA}',
72
- params={
73
- 'service':'wfs',
74
- 'version':'2',
75
- 'request':'getfeature',
76
- 'STOREDQUERIE_ID':'GetParcel',
77
- 'refcat': self.rc,
78
- 'srsname': projection
79
- })
80
-
81
- geometry = xmltodict.parse(geometry_request.content)
169
+ def __create_geometry(self, projection: str = 'EPSG:4326') -> None:
170
+ """Crea la geometría de la parcela consultando el servicio WFS.
171
+
172
+ Args:
173
+ projection: Sistema de referencia espacial (EPSG). Por defecto 'EPSG:4326'.
174
+
175
+ Raises:
176
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error.
177
+ """
178
+ params = self._parametrizar_peticion(
179
+ service='wfs',
180
+ version='2',
181
+ request='getfeature',
182
+ STOREDQUERIE_ID='GetParcel',
183
+ refcat=self.rc,
184
+ srsname=projection
185
+ )
186
+ response = self._llamar_a_api(f'{URL_BASE_GEOGRAFIA}', params)
187
+
188
+ geometry = xmltodict.parse(response.content)
82
189
  geoposition = geometry.get('FeatureCollection').get('member').get('cp:CadastralParcel').get('cp:referencePoint').get('gml:Point').get('gml:pos').split(' ')
83
190
  self.centroide = {
84
191
  'x': geoposition[1],
@@ -95,67 +202,56 @@ class ParcelaCatastral:
95
202
 
96
203
  def __create_from_rc(self, rc: str, projection: str):
97
204
  """Create an instance of InfoCatastral from a RC (Referencia Catastral) string."""
98
- req1 = requests.get(f'{URL_BASE_CALLEJERO}/Consulta_DNPRC',
99
- params={'RefCat': rc})
205
+ params = self._parametrizar_peticion(RefCat=rc)
206
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPRC', params)
207
+ info_cadastre = self._parsear_respuesta(response)
100
208
 
101
- if len(req1.content) > 0:
102
- try:
103
- info_cadastre = json.loads(req1.content)
104
- except:
105
- raise ErrorServidorCatastro(mensaje=f"El servidor no devuelve un JSON. Mensaje en bruto: {req1.content}")
106
- if comprobar_errores(info_cadastre):
107
- cudnp = info_cadastre.get("consulta_dnprcResult", {}).get("control", {}).get("cudnp", 1)
108
-
109
- if cudnp > 1:
110
- raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
111
- else:
112
- self.rc = ''.join(info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('idbi').get('rc').values())
113
- self.url_croquis = requests.get(URL_BASE_CROQUIS_DATOS, params={'refcat': self.rc}).url
114
- self.municipio = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('nm')
115
- self.provincia = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('np')
116
- self.tipo = 'Rústico' if info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('idbi').get('cn') == 'RU' else 'Urbano'
117
- if self.tipo == 'Urbano':
118
- self.calle = f"{info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('tv')} {info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('nv')}"
119
- self.numero = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('pnp')
120
- self.antiguedad = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('debi').get('ant')
121
- self.uso = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('debi').get('luso')
122
- elif self.tipo == 'Rústico':
123
- self.parcela = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('cpp').get('cpa')
124
- self.poligono = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('cpp').get('cpo')
125
- self.nombre_paraje = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('npa')
126
-
127
- self.__create_regions(info_cadastre)
128
- self.__create_geometry(projection)
129
-
130
- self.superficie_construida = sum(float(region.get('superficie')) for region in self.regiones)
131
- self.superficie = sum(float(region.get('superficie')) for region in self.regiones)
132
- else:
133
- raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
209
+ if self._comprobar_errores_catastro(info_cadastre):
210
+ cudnp = self._obtener_numero_parcelas(info_cadastre, 'dnprc')
211
+
212
+ if cudnp > 1:
213
+ raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
214
+ else:
215
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('idbi').get('rc'))
216
+ self.url_croquis = requests.get(URL_BASE_CROQUIS_DATOS, params={'refcat': self.rc}).url
217
+ self.municipio = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('nm')
218
+ self.provincia = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('np')
219
+ self.tipo = 'Rústico' if info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('idbi').get('cn') == 'RU' else 'Urbano'
220
+ if self.tipo == 'Urbano':
221
+ self.calle = f"{info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('tv')} {info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('nv')}"
222
+ self.numero = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('pnp')
223
+ self.antiguedad = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('debi').get('ant')
224
+ self.uso = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('debi').get('luso')
225
+ elif self.tipo == 'Rústico':
226
+ self.parcela = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('cpp').get('cpa')
227
+ self.poligono = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('cpp').get('cpo')
228
+ self.nombre_paraje = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('npa')
229
+
230
+ self.__create_regions(info_cadastre)
231
+ self.__create_geometry(projection)
134
232
 
233
+ self.superficie_construida = sum(float(region.get('superficie')) for region in self.regiones)
234
+ self.superficie = sum(float(region.get('superficie')) for region in self.regiones)
235
+
135
236
  def __create_from_parcel(self, provincia: Union[str,None], municipio: Union[str,None], poligono: Union[str,None], parcela: Union[str,None], projection: str):
136
237
  """Create an instance of InfoCatastral from a parcela string."""
137
- req = requests.get(f'{URL_BASE_CALLEJERO}/Consulta_DNPPP',
138
- params={
139
- 'Provincia': provincia,
140
- 'Municipio': municipio,
141
- 'Poligono': poligono,
142
- 'Parcela': parcela
143
- })
144
- if len(req.content) > 0:
145
- try:
146
- info_cadastre = json.loads(req.content)
147
- except:
148
- raise ErrorServidorCatastro(mensaje=f"El servidor no devuelve un JSON. Mensaje en bruto: {req1.content}")
149
- if comprobar_errores(info_cadastre):
150
- cudnp = info_cadastre.get("consulta_dnpppResult", {}).get("control", {}).get("cudnp", 1)
238
+ params = self._parametrizar_peticion(
239
+ Provincia=provincia,
240
+ Municipio=municipio,
241
+ Poligono=poligono,
242
+ Parcela=parcela
243
+ )
244
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPPP', params)
245
+ info_cadastre = self._parsear_respuesta(response)
246
+
247
+ if self._comprobar_errores_catastro(info_cadastre):
248
+ cudnp = self._obtener_numero_parcelas(info_cadastre, 'dnppp')
151
249
 
152
- if cudnp > 1:
153
- raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
154
- else:
155
- self.rc = ''.join(info_cadastre.get('consulta_dnpppResult').get('bico').get('bi').get('idbi').get('rc').values())
156
- self.__create_from_rc(self.rc, projection)
157
- else:
158
- raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
250
+ if cudnp > 1:
251
+ raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
252
+ else:
253
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnpppResult').get('bico').get('bi').get('idbi').get('rc'))
254
+ self.__create_from_rc(self.rc, projection)
159
255
 
160
256
  def __create_from_address(self, provincia: Union[str,None], municipio: Union[str,None], tipo_via: Union[str,None], calle: Union[str,None], numero: Union[str,None], projection: str):
161
257
  """Create an instance of InfoCatastral from an address string."""
@@ -169,37 +265,31 @@ class ParcelaCatastral:
169
265
  )
170
266
 
171
267
  if info_calle:
172
- req = requests.get(f'{URL_BASE_CALLEJERO}/Consulta_DNPLOC',
173
- params={
174
- 'Provincia': info_calle.municipio.provincia,
175
- 'Municipio': info_calle.municipio.municipio,
176
- 'Sigla': info_calle.tipo_via,
177
- 'Calle': info_calle.calle,
178
- 'Numero': numero
179
- })
180
-
181
- if req.status_code == 200 and len(req.content) > 0 and comprobar_errores(req.json()):
182
- try:
183
- info_cadastre = json.loads(req1.content)
184
- except:
185
- raise ErrorServidorCatastro(mensaje=f"El servidor no devuelve un JSON. Mensaje en bruto: {req1.content}")
186
- cudnp = info_cadastre.get("consulta_dnplocResult", {}).get("control", {}).get("cudnp", 1)
268
+ params = self._parametrizar_peticion(
269
+ Provincia=info_calle.municipio.provincia,
270
+ Municipio=info_calle.municipio.municipio,
271
+ Sigla=info_calle.tipo_via,
272
+ Calle=info_calle.calle,
273
+ Numero=numero
274
+ )
275
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPLOC', params)
276
+ info_cadastre = self._parsear_respuesta(response)
277
+
278
+ if self._comprobar_errores_catastro(info_cadastre):
279
+ cudnp = self._obtener_numero_parcelas(info_cadastre, 'dnploc')
187
280
 
188
281
  if cudnp > 1:
189
282
  raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
190
283
  else:
191
284
  if 'lrcdnp' in info_cadastre.get('consulta_dnplocResult'):
192
- self.rc = ''.join(info_cadastre.get('consulta_dnplocResult').get('lrcdnp').get('rcdnp')[0].get('rc').values())
285
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnplocResult').get('lrcdnp').get('rcdnp')[0].get('rc'))
193
286
  elif 'bico' in info_cadastre.get('consulta_dnplocResult'):
194
- self.rc = ''.join(info_cadastre.get('consulta_dnplocResult').get('bico').get('bi').get('idbi').get('rc').values())
287
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnplocResult').get('bico').get('bi').get('idbi').get('rc'))
195
288
  self.__create_from_rc(self.rc, projection)
196
- elif 'lerr' in json.loads(req.content).get('consulta_dnplocResult') and json.loads(req.content)['consulta_dnplocResult']['lerr'][0]['cod'] == '43':
197
- info_cadastre = json.loads(req.content)
289
+ elif 'lerr' in info_cadastre.get('consulta_dnplocResult') and info_cadastre['consulta_dnplocResult']['lerr'][0]['cod'] == '43':
198
290
  raise Exception(f"Ese número no existe. Prueba con alguno de estos: {[num.get('num').get('pnp') for num in info_cadastre.get('consulta_dnplocResult').get('numerero').get('nump')]}")
199
291
  else:
200
292
  raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
201
-
202
-
203
293
  else:
204
294
  raise Exception('La calle no existe.')
205
295
 
@@ -225,9 +315,11 @@ class ParcelaCatastral:
225
315
  raise ValueError("No se ha proporcionado suficiente información para realizar la búsqueda")
226
316
 
227
317
  @property
228
- def distancias_aristas(self):
229
- """
230
- Calcula las distancias entre dos puntos de la geometría, par a par.
318
+ def distancias_aristas(self) -> Optional[List[float]]:
319
+ """Calcula las distancias entre puntos consecutivos de la geometría de la parcela.
320
+
321
+ Returns:
322
+ Lista de distancias entre aristas consecutivas en metros, o None si no hay geometría.
231
323
  """
232
324
  if self.geometria:
233
325
  distancias = []
@@ -243,9 +335,11 @@ class ParcelaCatastral:
243
335
  return None
244
336
 
245
337
  @property
246
- def perimetro(self):
247
- """
248
- Calcula el perímetro de la geometría
338
+ def perimetro(self) -> Optional[float]:
339
+ """Calcula el perímetro total de la geometría de la parcela.
340
+
341
+ Returns:
342
+ Perímetro total en metros, o None si no hay geometría.
249
343
  """
250
344
  distancias = self.distancias_aristas
251
345
  if distancias:
@@ -253,19 +347,27 @@ class ParcelaCatastral:
253
347
  else:
254
348
  return None
255
349
 
256
- def valor_catastral_urbano_m2(self, anio):
350
+ def valor_catastral_urbano_m2(self, anio: int) -> Optional[float]:
351
+ """Obtiene el valor catastral por metro cuadrado para parcelas urbanas.
352
+
353
+ Args:
354
+ anio: Año del valor catastral a consultar.
355
+
356
+ Returns:
357
+ Valor catastral en €/m², 0 si es rústica, o None si hay algún error.
358
+ """
257
359
  if self.tipo == 'Rústico':
258
360
  return 0
259
361
 
260
- req = requests.get(f'{URL_BASE_MAPA_VALORES_URBANOS}',
261
- params={
262
- "huso":"4326",
263
- "x":self.centroide['x'],
264
- "y":self.centroide['y'],
265
- "anyoZV":anio,
266
- "suelo": "N",
267
- "tipo_mapa":"vivienda"
268
- })
362
+ params = self._parametrizar_peticion(
363
+ huso="4326",
364
+ x=self.centroide['x'],
365
+ y=self.centroide['y'],
366
+ anyoZV=anio,
367
+ suelo="N",
368
+ tipo_mapa="vivienda"
369
+ )
370
+ req = self._llamar_a_api(f'{URL_BASE_MAPA_VALORES_URBANOS}', params)
269
371
 
270
372
  values_map = converters.gpd.read_file(req.content)
271
373
  centroide_point = Point(self.centroide['x'],self.centroide['y'])
@@ -274,36 +376,15 @@ class ParcelaCatastral:
274
376
  return selected_polygon['Ptipo1'].iloc[0].get('val_tipo_m2')
275
377
 
276
378
  @property
277
- def numero_plantas(self):
278
- """
279
- Obtiene el número de plantas de un edificio a partir de su
280
- referencia catastral usando el WFS INSPIRE de Catastro.
281
-
282
- Criterio:
283
- - Se consultan las BuildingPart
284
- - Se toma el máximo número de plantas sobre rasante
285
- - Se devuelve también el máximo bajo rasante
286
-
287
- Parameters
288
- ----------
289
- refcat : str
290
- Referencia catastral completa (ej: '9795702WG1499N0001AY')
291
- timeout : int
292
- Timeout de la petición HTTP en segundos
293
-
294
- Returns
295
- -------
296
- dict
297
- {
298
- 'refcat': str,
299
- 'parts': int,
300
- 'max_above_ground': int | None,
301
- 'max_below_ground': int,
302
- 'total_floors': int | None,
303
- 'parts_detail': list
304
- }
379
+ def numero_plantas(self) -> Dict[str, Any]:
380
+ """Obtiene el número de plantas de un edificio a partir de su referencia catastral.
381
+
382
+ Returns:
383
+ Diccionario con:
384
+ - 'plantas': Máximo número de plantas sobre rasante (o None)
385
+ - 'sotanos': Máximo número de plantas bajo rasante
386
+ - 'total': Suma de plantas sobre y bajo rasante (o None)
305
387
  """
306
-
307
388
  if self.tipo == 'Rústico':
308
389
  return 0
309
390
 
@@ -355,7 +436,6 @@ class ParcelaCatastral:
355
436
  })
356
437
 
357
438
  return {
358
- "partes": parts_detail,
359
439
  "plantas": max(above_floors) if above_floors else None,
360
440
  "sotanos": max(below_floors) if below_floors else 0,
361
441
  "total": (
@@ -489,7 +569,7 @@ class ParcelaCatastral:
489
569
  """
490
570
  converters.to_parquet([self], filename)
491
571
 
492
- class MetaParcela:
572
+ class MetaParcela(ParcelaHelper):
493
573
  """
494
574
  Clase que representa una MetaParcela, es decir, una gran parcela catastral con
495
575
  varias referencias catastrales (Parcelas Catastrales más pequeñas).
@@ -510,45 +590,71 @@ class MetaParcela:
510
590
 
511
591
  """
512
592
 
513
- def __create_from_rc(self, rc: str):
514
- """Create an instance of InfoCatastral from a RC (Referencia Catastral) string."""
515
- req1 = requests.get(f'{URL_BASE_CALLEJERO}/Consulta_DNPRC',
516
- params={'RefCat': rc})
517
-
518
- if len(req1.content) > 0:
519
- info_cadastre = json.loads(req1.content)
520
- if comprobar_errores(info_cadastre):
521
- self.parcelas = []
522
- num_parcelas = info_cadastre.get("consulta_dnprcResult", {}).get("control", {}).get("cudnp", 1)
523
- for idx in range(num_parcelas):
524
- rc = ''.join(info_cadastre.get('consulta_dnprcResult').get('lrcdnp').get('rcdnp')[idx].get('rc').values())
593
+ def __create_from_rc(self, rc: str) -> None:
594
+ """Create an instance of MetaParcela from a RC (Referencia Catastral) string.
595
+
596
+ Args:
597
+ rc: Referencia catastral de la metaparcela
598
+
599
+ Raises:
600
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
601
+ """
602
+ params = self._parametrizar_peticion(RefCat=rc)
603
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPRC', params)
604
+ info_cadastre = self._parsear_respuesta(response)
605
+
606
+ if self._comprobar_errores_catastro(info_cadastre):
607
+ self.parcelas = []
608
+ num_parcelas = self._obtener_numero_parcelas(info_cadastre, 'dnprc')
609
+ for idx in range(num_parcelas):
610
+ rc_dict = info_cadastre.get('consulta_dnprcResult').get('lrcdnp').get('rcdnp')[idx].get('rc')
611
+ rc = self._extraer_rc_from_dict(rc_dict)
525
612
  self.parcelas.append(ParcelaCatastral(rc=rc))
526
- else:
527
- raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
528
613
 
529
614
 
530
- def __create_from_parcel(self, provincia: Union[str,None], municipio: Union[str,None], poligono: Union[str,None], parcela: Union[str,None]):
531
- """Create an instance of InfoCatastral from a parcela string."""
532
- req = requests.get(f'{URL_BASE_CALLEJERO}/Consulta_DNPPP',
533
- params={
534
- 'Provincia': provincia,
535
- 'Municipio': municipio,
536
- 'Poligono': poligono,
537
- 'Parcela': parcela
538
- })
539
- if len(req.content) > 0:
540
- info_cadastre = json.loads(req.content)
541
- if comprobar_errores(info_cadastre):
542
- self.parcelas = []
543
- num_parcelas = info_cadastre.get("consulta_dnpppResult", {}).get("control", {}).get("cudnp", 1)
544
- for idx in range(num_parcelas):
545
- rc = ''.join(info_cadastre.get('consulta_dnpppResult').get('lrcdnp').get('rcdnp')[idx].get('rc').values())
546
- self.parcelas.append(ParcelaCatastral(rc=rc))
547
- else:
548
- raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
615
+ def __create_from_parcel(self, provincia: Union[str,None], municipio: Union[str,None], poligono: Union[str,None], parcela: Union[str,None]) -> None:
616
+ """Create an instance of MetaParcela from parcel data.
617
+
618
+ Args:
619
+ provincia: Provincia donde se encuentra la parcela
620
+ municipio: Municipio donde se encuentra la parcela
621
+ poligono: Número de polígono
622
+ parcela: Número de parcela
623
+
624
+ Raises:
625
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
626
+ """
627
+ params = self._parametrizar_peticion(
628
+ Provincia=provincia,
629
+ Municipio=municipio,
630
+ Poligono=poligono,
631
+ Parcela=parcela
632
+ )
633
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPPP', params)
634
+ info_cadastre = self._parsear_respuesta(response)
635
+
636
+ if self._comprobar_errores_catastro(info_cadastre):
637
+ self.parcelas = []
638
+ num_parcelas = self._obtener_numero_parcelas(info_cadastre, 'dnppp')
639
+ for idx in range(num_parcelas):
640
+ rc_dict = info_cadastre.get('consulta_dnpppResult').get('lrcdnp').get('rcdnp')[idx].get('rc')
641
+ rc = self._extraer_rc_from_dict(rc_dict)
642
+ self.parcelas.append(ParcelaCatastral(rc=rc))
549
643
 
550
- def __create_from_address(self, provincia: Union[str,None], municipio: Union[str,None], tipo_via: Union[str,None], calle: Union[str,None], numero: Union[str,None]):
551
- """Create an instance of InfoCatastral from an address string."""
644
+ def __create_from_address(self, provincia: Union[str,None], municipio: Union[str,None], tipo_via: Union[str,None], calle: Union[str,None], numero: Union[str,None]) -> None:
645
+ """Create an instance of MetaParcela from an address.
646
+
647
+ Args:
648
+ provincia: Provincia donde se encuentra la dirección
649
+ municipio: Municipio donde se encuentra la dirección
650
+ tipo_via: Tipo de vía (calle, avenida, etc.)
651
+ calle: Nombre de la calle
652
+ numero: Número de la dirección
653
+
654
+ Raises:
655
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
656
+ Exception: Si la calle no existe
657
+ """
552
658
  info_calle = Calle(
553
659
  municipio=Municipio(
554
660
  provincia=provincia,
@@ -559,29 +665,27 @@ class MetaParcela:
559
665
  )
560
666
 
561
667
  if info_calle:
562
- req = requests.get(f'{URL_BASE_CALLEJERO}/Consulta_DNPLOC',
563
- params={
564
- 'Provincia': info_calle.municipio.provincia,
565
- 'Municipio': info_calle.municipio.municipio,
566
- 'Sigla': info_calle.tipo_via,
567
- 'Calle': info_calle.calle,
568
- 'Numero': numero
569
- })
668
+ params = self._parametrizar_peticion(
669
+ Provincia=info_calle.municipio.provincia,
670
+ Municipio=info_calle.municipio.municipio,
671
+ Sigla=info_calle.tipo_via,
672
+ Calle=info_calle.calle,
673
+ Numero=numero
674
+ )
675
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPLOC', params)
676
+ info_cadastre = self._parsear_respuesta(response)
570
677
 
571
- if req.status_code == 200 and len(req.content) > 0 and comprobar_errores(req.json()):
572
- info_cadastre = json.loads(req.content)
678
+ if self._comprobar_errores_catastro(info_cadastre):
573
679
  self.parcelas = []
574
- num_parcelas = info_cadastre.get("consulta_dnplocResult", {}).get("control", {}).get("cudnp", 1)
680
+ num_parcelas = self._obtener_numero_parcelas(info_cadastre, 'dnploc')
575
681
  for idx in range(num_parcelas):
576
- rc = ''.join(info_cadastre.get('consulta_dnplocResult').get('lrcdnp').get('rcdnp')[idx].get('rc').values())
682
+ rc_dict = info_cadastre.get('consulta_dnplocResult').get('lrcdnp').get('rcdnp')[idx].get('rc')
683
+ rc = self._extraer_rc_from_dict(rc_dict)
577
684
  self.parcelas.append(ParcelaCatastral(rc=rc))
578
- else:
579
- raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
580
-
581
685
  else:
582
686
  raise Exception('La calle no existe.')
583
687
 
584
- def __init__(self, rc: Union[str,None] = None, provincia: Union[int,str,None] = None, municipio: Union[int,str,None] = None, poligono: Union[int,None] = None, parcela: Union[int,None] = None, tipo_via: Union[str,None] = None, calle: Union[str,None] = None, numero: Union[str,None] = None):
688
+ def __init__(self, rc: Union[str,None] = None, provincia: Union[int,str,None] = None, municipio: Union[int,str,None] = None, poligono: Union[int,None] = None, parcela: Union[int,None] = None, tipo_via: Union[str,None] = None, calle: Union[str,None] = None, numero: Union[str,None] = None) -> None:
585
689
  if rc:
586
690
  self.rc = rc
587
691
  self.__create_from_rc(rc)
@@ -602,8 +706,7 @@ class MetaParcela:
602
706
 
603
707
 
604
708
  def to_dataframe(self):
605
- """
606
- Convierte la MetaParcela en un DataFrame de pandas.
709
+ """Convierte la MetaParcela en un DataFrame de pandas.
607
710
 
608
711
  Returns:
609
712
  pd.DataFrame: Un DataFrame que contiene las parcelas de la MetaParcela.
@@ -611,11 +714,10 @@ class MetaParcela:
611
714
  return converters.to_geodataframe(self.parcelas)
612
715
 
613
716
  def to_json(self, filename: Union[str,None] = None) -> str:
614
- """
615
- Convierte la MetaParcela en un JSON.
717
+ """Convierte la MetaParcela en un JSON.
616
718
 
617
719
  Args:
618
- filename (Union[str,None], optional): Nombre del archivo donde guardar el JSON. Defaults to None.
720
+ filename: Nombre del archivo donde guardar el JSON. Defaults to None.
619
721
 
620
722
  Returns:
621
723
  str: Una cadena JSON que contiene las parcelas de la MetaParcela.
@@ -623,32 +725,29 @@ class MetaParcela:
623
725
  return converters.to_json(self.parcelas, filename)
624
726
 
625
727
  def to_csv(self, filename: Union[str,None] = None) -> str:
626
- """
627
- Convierte la MetaParcela en un CSV.
728
+ """Convierte la MetaParcela en un CSV.
628
729
 
629
730
  Args:
630
- filename (Union[str,None], optional): Nombre del archivo donde guardar el CSV. Defaults to None.
731
+ filename: Nombre del archivo donde guardar el CSV. Defaults to None.
631
732
 
632
733
  Returns:
633
734
  str: Una cadena CSV que contiene las parcelas de la MetaParcela.
634
735
  """
635
736
  return converters.to_csv(self.parcelas, filename)
636
737
 
637
- def to_shapefile(self, filename: str):
638
- """
639
- Guarda la MetaParcela como un archivo Shapefile.
738
+ def to_shapefile(self, filename: str) -> None:
739
+ """Guarda la MetaParcela como un archivo Shapefile.
640
740
 
641
741
  Args:
642
- filename (str): El nombre del archivo Shapefile a guardar.
742
+ filename: El nombre del archivo Shapefile a guardar.
643
743
  """
644
744
  converters.to_shapefile(self.parcelas, filename)
645
745
 
646
- def to_parquet(self, filename: str):
647
- """
648
- Guarda la MetaParcela como un archivo Parquet.
746
+ def to_parquet(self, filename: str) -> None:
747
+ """Guarda la MetaParcela como un archivo Parquet.
649
748
 
650
749
  Args:
651
- filename (str): El nombre del archivo Parquet a guardar.
750
+ filename: El nombre del archivo Parquet a guardar.
652
751
  """
653
752
  converters.to_parquet(self.parcelas, filename)
654
753
 
@@ -1,109 +0,0 @@
1
- import requests
2
- import xmltodict
3
-
4
-
5
- WFS_URL = "https://ovc.catastro.meh.es/INSPIRE/wfsBU.aspx"
6
-
7
-
8
- def get_building_floors_by_refcat(refcat, timeout=30):
9
- """
10
- Obtiene el número de plantas de un edificio a partir de su
11
- referencia catastral usando el WFS INSPIRE de Catastro.
12
-
13
- Criterio:
14
- - Se consultan las BuildingPart
15
- - Se toma el máximo número de plantas sobre rasante
16
- - Se devuelve también el máximo bajo rasante
17
-
18
- Parameters
19
- ----------
20
- refcat : str
21
- Referencia catastral completa (ej: '9795702WG1499N0001AY')
22
- timeout : int
23
- Timeout de la petición HTTP en segundos
24
-
25
- Returns
26
- -------
27
- dict
28
- {
29
- 'refcat': str,
30
- 'parts': int,
31
- 'max_above_ground': int | None,
32
- 'max_below_ground': int,
33
- 'total_floors': int | None,
34
- 'parts_detail': list
35
- }
36
- """
37
-
38
- params = {
39
- "service": "WFS",
40
- "version": "2.0.0",
41
- "request": "GetFeature",
42
- "STOREDQUERIE_ID": "GetBuildingPartByParcel",
43
- "REFCAT": refcat,
44
- "srsname": "EPSG:4326"
45
- }
46
-
47
- response = requests.get(WFS_URL, params=params, timeout=timeout)
48
- response.raise_for_status()
49
-
50
- data = xmltodict.parse(response.content)
51
-
52
- feature_collection = data.get("gml:FeatureCollection", {})
53
- members = feature_collection.get("gml:featureMember", [])
54
-
55
- if isinstance(members, dict):
56
- members = [members]
57
-
58
- above_floors = []
59
- below_floors = []
60
- parts_detail = []
61
-
62
- for member in members:
63
- bp = member.get("bu-ext2d:BuildingPart")
64
- if not bp:
65
- continue
66
-
67
- part_id = bp.get("@gml:id")
68
-
69
- above = bp.get("bu-ext2d:numberOfFloorsAboveGround")
70
- below = bp.get("bu-ext2d:numberOfFloorsBelowGround")
71
-
72
- above_i = int(above) if above is not None else None
73
- below_i = int(below) if below is not None else 0
74
-
75
- if above_i is not None:
76
- above_floors.append(above_i)
77
- below_floors.append(below_i)
78
-
79
- parts_detail.append({
80
- "id": part_id,
81
- "floors_above_ground": above_i,
82
- "floors_below_ground": below_i
83
- })
84
-
85
- return {
86
- "refcat": refcat,
87
- "parts": len(parts_detail),
88
- "max_above_ground": max(above_floors) if above_floors else None,
89
- "max_below_ground": max(below_floors) if below_floors else 0,
90
- "total_floors": (
91
- max(above_floors) + max(below_floors)
92
- if above_floors else None
93
- ),
94
- "parts_detail": parts_detail
95
- }
96
-
97
-
98
- # -------------------------------------------------------
99
- # Ejemplo de uso
100
- # -------------------------------------------------------
101
- if __name__ == "__main__":
102
- refcat = "6225302WG2662E0024ZJ"
103
- result = get_building_floors_by_refcat(refcat)
104
-
105
- print("Referencia:", result["refcat"])
106
- print("Partes:", result["parts"])
107
- print("Plantas sobre rasante:", result["max_above_ground"])
108
- print("Plantas bajo rasante:", result["max_below_ground"])
109
- print("Total plantas:", result["total_floors"])
@@ -1,136 +0,0 @@
1
- import requests
2
- from escatastrolib import ParcelaCatastral
3
- from pyproj import Transformer
4
-
5
- WMS_URL = "https://www1.sedecatastro.gob.es/Cartografia/GeoServerWMS.aspx"
6
- LAYER = "IAMIR26:athiamir26"
7
- WIDTH = 101
8
- HEIGHT = 101
9
- I = WIDTH // 2
10
- J = HEIGHT // 2
11
-
12
- CULTIVOS = {
13
- "ARZ": "Arrozal",
14
- "CBP": "Cultivos bajo plástico",
15
- "CIT": "Cítricos",
16
- "FCR": "Frutal carnoso regadío",
17
- "FCS": "Frutal carnoso secano",
18
- "FSR": "Frutos secos regadío",
19
- "FSS": "Frutos secos secano",
20
- "HUE": "Huerta",
21
- "OLR": "Olivar regadío",
22
- "OLS": "Olivar secano",
23
- "PLT": "Plátanos",
24
- "SBT": "Cultivos tropicales y subtropicales",
25
- "TAR": "Tierras arables regadío",
26
- "TAS": "Tierras arables secano",
27
- "VIP": "Viñedo parral",
28
- "VIR": "Viñedo regadío",
29
- "VIS": "Viñedo secano",
30
- "VOL": "Viña olivar",
31
- "PAR": "Pasto con arbolado",
32
- "PRD": "Prados",
33
- "PST": "Pastizal",
34
- "CON": "Coníferas",
35
- "FRL": "Frondosas de crecimiento lento",
36
- "FRR": "Frondosas de crecimiento rápido",
37
- "MTR": "Matorral"
38
- }
39
-
40
-
41
- def obtener_valor_catastral_tierras(referencia_catastral):
42
- """
43
- Obtiene los valores catastrales de tierras a partir de una referencia catastral.
44
-
45
- Args:
46
- referencia_catastral (str): Referencia catastral de la parcela.
47
-
48
- Returns:
49
- dict: Datos de la parcela con región y módulos €/ha, o None si no se encuentran.
50
- """
51
- parcela = ParcelaCatastral(referencia_catastral)
52
- geometria = parcela.geometria # EPSG:4381
53
-
54
- # Transformar geometría EPSG:4381 → EPSG:3857
55
- transformer = Transformer.from_crs("EPSG:4381", "EPSG:3857", always_xy=True)
56
- xs_3857 = []
57
- ys_3857 = []
58
- for p in geometria:
59
- x, y = transformer.transform(p["x"], p["y"])
60
- xs_3857.append(x)
61
- ys_3857.append(y)
62
-
63
- # BBOX EPSG:3857
64
- bbox = f"{min(xs_3857)},{min(ys_3857)},{max(xs_3857)},{max(ys_3857)}"
65
-
66
- params = {
67
- "SERVICE": "WMS",
68
- "VERSION": "1.3.0",
69
- "REQUEST": "GetFeatureInfo",
70
- "LAYERS": LAYER,
71
- "QUERY_LAYERS": LAYER,
72
- "STYLES": "",
73
- "CRS": "EPSG:3857",
74
- "SRS": "EPSG:3857",
75
- "BBOX": bbox,
76
- "WIDTH": WIDTH,
77
- "HEIGHT": HEIGHT,
78
- "FORMAT": "image/png",
79
- "TRANSPARENT": "true",
80
- "I": I,
81
- "J": J,
82
- "INFO_FORMAT": "application/json"
83
- }
84
-
85
- r = requests.get(WMS_URL, params=params, timeout=15)
86
- r.raise_for_status()
87
- data = r.json()
88
-
89
- if not data.get("features"):
90
- return None
91
-
92
- props = data["features"][0]["properties"]
93
-
94
- modulos = {
95
- "region": props.get("REGIONAL"),
96
- "nombre_region": props.get("NOMBRE"),
97
- "modulos_€/ha": {}
98
- }
99
-
100
- for cod, desc in CULTIVOS.items():
101
- val = props.get(cod)
102
- if isinstance(val, (int, float)) and val > 0:
103
- modulos["modulos_€/ha"][desc] = val
104
-
105
- return {
106
- "referencia_catastral": referencia_catastral,
107
- "provincia": parcela.provincia,
108
- "municipio": parcela.municipio,
109
- "superficie_m2": parcela.superficie,
110
- "region": modulos["region"],
111
- "nombre_region": modulos["nombre_region"],
112
- "modulos_€/ha": modulos["modulos_€/ha"]
113
- }
114
-
115
-
116
- # =========================================================
117
- # EJEMPLO DE USO
118
- # =========================================================
119
-
120
- if __name__ == "__main__":
121
- rc = "18030A014004610000RH" # ejemplo
122
- resultado = obtener_valor_catastral_tierras(rc)
123
-
124
- if resultado:
125
- print(f"RC: {resultado['referencia_catastral']}")
126
- print(f"{resultado['municipio']} ({resultado['provincia']})")
127
- print(f"Superficie: {resultado['superficie_m2']:.0f} m²")
128
- print(f"Región: {resultado['region']} – {resultado['nombre_region']}\n")
129
- for uso, valor in sorted(
130
- resultado["modulos_€/ha"].items(),
131
- key=lambda x: x[1],
132
- reverse=True
133
- ):
134
- print(f"{uso}: {valor:,.0f} €/Ha")
135
- else:
136
- print("No se encontraron módulos catastrales para esta parcela.")
File without changes