giga-spatial 0.6.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.
- giga_spatial-0.6.0.dist-info/METADATA +141 -0
- giga_spatial-0.6.0.dist-info/RECORD +47 -0
- giga_spatial-0.6.0.dist-info/WHEEL +5 -0
- giga_spatial-0.6.0.dist-info/licenses/LICENSE +661 -0
- giga_spatial-0.6.0.dist-info/top_level.txt +1 -0
- gigaspatial/__init__.py +1 -0
- gigaspatial/config.py +226 -0
- gigaspatial/core/__init__.py +0 -0
- gigaspatial/core/io/__init__.py +5 -0
- gigaspatial/core/io/adls_data_store.py +325 -0
- gigaspatial/core/io/data_api.py +113 -0
- gigaspatial/core/io/data_store.py +147 -0
- gigaspatial/core/io/local_data_store.py +92 -0
- gigaspatial/core/io/readers.py +265 -0
- gigaspatial/core/io/writers.py +128 -0
- gigaspatial/core/schemas/__init__.py +0 -0
- gigaspatial/core/schemas/entity.py +244 -0
- gigaspatial/generators/__init__.py +2 -0
- gigaspatial/generators/poi.py +636 -0
- gigaspatial/generators/zonal/__init__.py +3 -0
- gigaspatial/generators/zonal/base.py +370 -0
- gigaspatial/generators/zonal/geometry.py +439 -0
- gigaspatial/generators/zonal/mercator.py +78 -0
- gigaspatial/grid/__init__.py +1 -0
- gigaspatial/grid/mercator_tiles.py +286 -0
- gigaspatial/handlers/__init__.py +40 -0
- gigaspatial/handlers/base.py +761 -0
- gigaspatial/handlers/boundaries.py +305 -0
- gigaspatial/handlers/ghsl.py +772 -0
- gigaspatial/handlers/giga.py +145 -0
- gigaspatial/handlers/google_open_buildings.py +472 -0
- gigaspatial/handlers/hdx.py +241 -0
- gigaspatial/handlers/mapbox_image.py +208 -0
- gigaspatial/handlers/maxar_image.py +291 -0
- gigaspatial/handlers/microsoft_global_buildings.py +548 -0
- gigaspatial/handlers/ookla_speedtest.py +199 -0
- gigaspatial/handlers/opencellid.py +290 -0
- gigaspatial/handlers/osm.py +356 -0
- gigaspatial/handlers/overture.py +126 -0
- gigaspatial/handlers/rwi.py +157 -0
- gigaspatial/handlers/unicef_georepo.py +806 -0
- gigaspatial/handlers/worldpop.py +266 -0
- gigaspatial/processing/__init__.py +4 -0
- gigaspatial/processing/geo.py +1054 -0
- gigaspatial/processing/sat_images.py +39 -0
- gigaspatial/processing/tif_processor.py +477 -0
- gigaspatial/processing/utils.py +49 -0
@@ -0,0 +1,806 @@
|
|
1
|
+
import requests
|
2
|
+
|
3
|
+
from gigaspatial.config import config
|
4
|
+
|
5
|
+
|
6
|
+
class GeoRepoClient:
|
7
|
+
"""
|
8
|
+
A client for interacting with the GeoRepo API.
|
9
|
+
|
10
|
+
GeoRepo is a platform for managing and accessing geospatial administrative
|
11
|
+
boundary data. This client provides methods to search, retrieve, and work
|
12
|
+
with modules, datasets, views, and administrative entities.
|
13
|
+
|
14
|
+
Attributes:
|
15
|
+
base_url (str): The base URL for the GeoRepo API
|
16
|
+
api_key (str): The API key for authentication
|
17
|
+
email (str): The email address associated with the API key
|
18
|
+
headers (dict): HTTP headers used for API requests
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, api_key=None, email=None):
|
22
|
+
"""
|
23
|
+
Initialize the GeoRepo client.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
api_key (str, optional): GeoRepo API key. If not provided, will use
|
27
|
+
the GEOREPO_API_KEY environment variable from config.
|
28
|
+
email (str, optional): Email address associated with the API key.
|
29
|
+
If not provided, will use the GEOREPO_USER_EMAIL environment
|
30
|
+
variable from config.
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
ValueError: If api_key or email is not provided and cannot be found
|
34
|
+
in environment variables.
|
35
|
+
"""
|
36
|
+
self.base_url = "https://georepo.unicef.org/api/v1"
|
37
|
+
self.api_key = api_key or config.GEOREPO_API_KEY
|
38
|
+
self.email = email or config.GEOREPO_USER_EMAIL
|
39
|
+
self.logger = config.get_logger(self.__class__.__name__)
|
40
|
+
|
41
|
+
if not self.api_key:
|
42
|
+
raise ValueError(
|
43
|
+
"API Key is required. Provide it as a parameter or set GEOREPO_API_KEY environment variable."
|
44
|
+
)
|
45
|
+
|
46
|
+
if not self.email:
|
47
|
+
raise ValueError(
|
48
|
+
"Email is required. Provide it as a parameter or set GEOREPO_USER_EMAIL environment variable."
|
49
|
+
)
|
50
|
+
|
51
|
+
self.headers = {
|
52
|
+
"Accept": "application/json",
|
53
|
+
"Authorization": f"Token {self.api_key}",
|
54
|
+
"GeoRepo-User-Key": self.email,
|
55
|
+
}
|
56
|
+
|
57
|
+
def _make_request(self, method, endpoint, params=None, data=None):
|
58
|
+
"""Internal method to handle making HTTP requests."""
|
59
|
+
try:
|
60
|
+
response = requests.request(
|
61
|
+
method, endpoint, headers=self.headers, params=params, json=data
|
62
|
+
)
|
63
|
+
response.raise_for_status()
|
64
|
+
return response
|
65
|
+
except requests.exceptions.RequestException as e:
|
66
|
+
raise requests.exceptions.HTTPError(f"API request failed: {e}")
|
67
|
+
|
68
|
+
def check_connection(self):
|
69
|
+
"""
|
70
|
+
Checks if the API connection is valid by making a simple request.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
bool: True if the connection is valid, False otherwise.
|
74
|
+
"""
|
75
|
+
endpoint = f"{self.base_url}/search/module/list/"
|
76
|
+
try:
|
77
|
+
self._make_request("GET", endpoint)
|
78
|
+
return True
|
79
|
+
except requests.exceptions.HTTPError as e:
|
80
|
+
return False
|
81
|
+
except requests.exceptions.RequestException as e:
|
82
|
+
raise requests.exceptions.RequestException(
|
83
|
+
f"Connection check encountered a network error: {e}"
|
84
|
+
)
|
85
|
+
|
86
|
+
def list_modules(self):
|
87
|
+
"""
|
88
|
+
List all available modules in GeoRepo.
|
89
|
+
|
90
|
+
A module is a top-level organizational unit that contains datasets.
|
91
|
+
Examples include "Admin Boundaries", "Health Facilities", etc.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
dict: JSON response containing a list of modules with their metadata.
|
95
|
+
Each module includes 'uuid', 'name', 'description', and other properties.
|
96
|
+
|
97
|
+
Raises:
|
98
|
+
requests.HTTPError: If the API request fails.
|
99
|
+
"""
|
100
|
+
endpoint = f"{self.base_url}/search/module/list/"
|
101
|
+
response = self._make_request("GET", endpoint)
|
102
|
+
return response.json()
|
103
|
+
|
104
|
+
def list_datasets_by_module(self, module_uuid):
|
105
|
+
"""
|
106
|
+
List all datasets within a specific module.
|
107
|
+
|
108
|
+
A dataset represents a collection of related geographic entities,
|
109
|
+
such as administrative boundaries for a specific country or region.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
module_uuid (str): The UUID of the module to query.
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
dict: JSON response containing a list of datasets with their metadata.
|
116
|
+
Each dataset includes 'uuid', 'name', 'description', creation date, etc.
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
requests.HTTPError: If the API request fails or module_uuid is invalid.
|
120
|
+
"""
|
121
|
+
endpoint = f"{self.base_url}/search/module/{module_uuid}/dataset/list/"
|
122
|
+
response = self._make_request("GET", endpoint)
|
123
|
+
return response.json()
|
124
|
+
|
125
|
+
def get_dataset_details(self, dataset_uuid):
|
126
|
+
"""
|
127
|
+
Get detailed information about a specific dataset.
|
128
|
+
|
129
|
+
This includes metadata about the dataset and information about
|
130
|
+
available administrative levels (e.g., country, province, district).
|
131
|
+
|
132
|
+
Args:
|
133
|
+
dataset_uuid (str): The UUID of the dataset to query.
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
dict: JSON response containing dataset details including:
|
137
|
+
- Basic metadata (name, description, etc.)
|
138
|
+
- Available administrative levels and their properties
|
139
|
+
- Temporal information and data sources
|
140
|
+
|
141
|
+
Raises:
|
142
|
+
requests.HTTPError: If the API request fails or dataset_uuid is invalid.
|
143
|
+
"""
|
144
|
+
endpoint = f"{self.base_url}/search/dataset/{dataset_uuid}/"
|
145
|
+
response = self._make_request("GET", endpoint)
|
146
|
+
return response.json()
|
147
|
+
|
148
|
+
def list_views_by_dataset(self, dataset_uuid, page=1, page_size=50):
|
149
|
+
"""
|
150
|
+
List views for a dataset with pagination support.
|
151
|
+
|
152
|
+
A view represents a specific version or subset of a dataset.
|
153
|
+
Views may be tagged as 'latest' or represent different time periods.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
dataset_uuid (str): The UUID of the dataset to query.
|
157
|
+
page (int, optional): Page number for pagination. Defaults to 1.
|
158
|
+
page_size (int, optional): Number of results per page. Defaults to 50.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
dict: JSON response containing paginated list of views with metadata.
|
162
|
+
Includes 'results', 'total_page', 'current_page', and 'count' fields.
|
163
|
+
Each view includes 'uuid', 'name', 'tags', and other properties.
|
164
|
+
|
165
|
+
Raises:
|
166
|
+
requests.HTTPError: If the API request fails or dataset_uuid is invalid.
|
167
|
+
"""
|
168
|
+
endpoint = f"{self.base_url}/search/dataset/{dataset_uuid}/view/list/"
|
169
|
+
params = {"page": page, "page_size": page_size}
|
170
|
+
response = self._make_request("GET", endpoint, params=params)
|
171
|
+
return response.json()
|
172
|
+
|
173
|
+
def list_entities_by_admin_level(
|
174
|
+
self,
|
175
|
+
view_uuid,
|
176
|
+
admin_level,
|
177
|
+
geom="no_geom",
|
178
|
+
format="json",
|
179
|
+
page=1,
|
180
|
+
page_size=50,
|
181
|
+
):
|
182
|
+
"""
|
183
|
+
List entities at a specific administrative level within a view.
|
184
|
+
|
185
|
+
Administrative levels typically follow a hierarchy:
|
186
|
+
- Level 0: Countries
|
187
|
+
- Level 1: States/Provinces/Regions
|
188
|
+
- Level 2: Districts/Counties
|
189
|
+
- Level 3: Sub-districts/Municipalities
|
190
|
+
- And so on...
|
191
|
+
|
192
|
+
Args:
|
193
|
+
view_uuid (str): The UUID of the view to query.
|
194
|
+
admin_level (int): The administrative level to retrieve (0, 1, 2, etc.).
|
195
|
+
geom (str, optional): Geometry inclusion level. Options:
|
196
|
+
- "no_geom": No geometry data
|
197
|
+
- "centroid": Only centroid points
|
198
|
+
- "full_geom": Complete boundary geometries
|
199
|
+
Defaults to "no_geom".
|
200
|
+
format (str, optional): Response format ("json" or "geojson").
|
201
|
+
Defaults to "json".
|
202
|
+
page (int, optional): Page number for pagination. Defaults to 1.
|
203
|
+
page_size (int, optional): Number of results per page. Defaults to 50.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
tuple: A tuple containing:
|
207
|
+
- dict: JSON/GeoJSON response with entity data
|
208
|
+
- dict: Metadata with pagination info (page, total_page, total_count)
|
209
|
+
|
210
|
+
Raises:
|
211
|
+
requests.HTTPError: If the API request fails or parameters are invalid.
|
212
|
+
"""
|
213
|
+
endpoint = (
|
214
|
+
f"{self.base_url}/search/view/{view_uuid}/entity/level/{admin_level}/"
|
215
|
+
)
|
216
|
+
params = {"page": page, "page_size": page_size, "geom": geom, "format": format}
|
217
|
+
response = self._make_request("GET", endpoint, params=params)
|
218
|
+
|
219
|
+
metadata = {
|
220
|
+
"page": int(response.headers.get("page", 1)),
|
221
|
+
"total_page": int(response.headers.get("total_page", 1)),
|
222
|
+
"total_count": int(response.headers.get("count", 0)),
|
223
|
+
}
|
224
|
+
|
225
|
+
return response.json(), metadata
|
226
|
+
|
227
|
+
def get_entity_by_ucode(self, ucode, geom="full_geom", format="geojson"):
|
228
|
+
"""
|
229
|
+
Get detailed information about a specific entity using its Ucode.
|
230
|
+
|
231
|
+
A Ucode (Universal Code) is a unique identifier for geographic entities
|
232
|
+
within the GeoRepo system, typically in the format "ISO3_LEVEL_NAME".
|
233
|
+
|
234
|
+
Args:
|
235
|
+
ucode (str): The unique code identifier for the entity.
|
236
|
+
geom (str, optional): Geometry inclusion level. Options:
|
237
|
+
- "no_geom": No geometry data
|
238
|
+
- "centroid": Only centroid points
|
239
|
+
- "full_geom": Complete boundary geometries
|
240
|
+
Defaults to "full_geom".
|
241
|
+
format (str, optional): Response format ("json" or "geojson").
|
242
|
+
Defaults to "geojson".
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
dict: JSON/GeoJSON response containing entity details including
|
246
|
+
geometry, properties, administrative level, and metadata.
|
247
|
+
|
248
|
+
Raises:
|
249
|
+
requests.HTTPError: If the API request fails or ucode is invalid.
|
250
|
+
"""
|
251
|
+
endpoint = f"{self.base_url}/search/entity/ucode/{ucode}/"
|
252
|
+
params = {"geom": geom, "format": format}
|
253
|
+
response = self._make_request("GET", endpoint, params=params)
|
254
|
+
return response.json()
|
255
|
+
|
256
|
+
def list_entity_children(
|
257
|
+
self, view_uuid, entity_ucode, geom="no_geom", format="json"
|
258
|
+
):
|
259
|
+
"""
|
260
|
+
List direct children of an entity in the administrative hierarchy.
|
261
|
+
|
262
|
+
For example, if given a country entity, this will return its states/provinces.
|
263
|
+
If given a state entity, this will return its districts/counties.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
view_uuid (str): The UUID of the view containing the entity.
|
267
|
+
entity_ucode (str): The Ucode of the parent entity.
|
268
|
+
geom (str, optional): Geometry inclusion level. Options:
|
269
|
+
- "no_geom": No geometry data
|
270
|
+
- "centroid": Only centroid points
|
271
|
+
- "full_geom": Complete boundary geometries
|
272
|
+
Defaults to "no_geom".
|
273
|
+
format (str, optional): Response format ("json" or "geojson").
|
274
|
+
Defaults to "json".
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
dict: JSON/GeoJSON response containing list of child entities
|
278
|
+
with their properties and optional geometry data.
|
279
|
+
|
280
|
+
Raises:
|
281
|
+
requests.HTTPError: If the API request fails or parameters are invalid.
|
282
|
+
"""
|
283
|
+
endpoint = (
|
284
|
+
f"{self.base_url}/search/view/{view_uuid}/entity/{entity_ucode}/children/"
|
285
|
+
)
|
286
|
+
params = {"geom": geom, "format": format}
|
287
|
+
response = self._make_request("GET", endpoint, params=params)
|
288
|
+
return response.json()
|
289
|
+
|
290
|
+
def search_entities_by_name(self, view_uuid, name, page=1, page_size=50):
|
291
|
+
"""
|
292
|
+
Search for entities by name using fuzzy matching.
|
293
|
+
|
294
|
+
This performs a similarity-based search to find entities whose names
|
295
|
+
match or are similar to the provided search term.
|
296
|
+
|
297
|
+
Args:
|
298
|
+
view_uuid (str): The UUID of the view to search within.
|
299
|
+
name (str): The name or partial name to search for.
|
300
|
+
page (int, optional): Page number for pagination. Defaults to 1.
|
301
|
+
page_size (int, optional): Number of results per page. Defaults to 50.
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
dict: JSON response containing paginated search results with
|
305
|
+
matching entities and their similarity scores.
|
306
|
+
|
307
|
+
Raises:
|
308
|
+
requests.HTTPError: If the API request fails or parameters are invalid.
|
309
|
+
"""
|
310
|
+
endpoint = f"{self.base_url}/search/view/{view_uuid}/entity/{name}/"
|
311
|
+
params = {"page": page, "page_size": page_size}
|
312
|
+
response = self._make_request("GET", endpoint, params=params)
|
313
|
+
return response.json()
|
314
|
+
|
315
|
+
def get_admin_boundaries(
|
316
|
+
self, view_uuid, admin_level=None, geom="full_geom", format="geojson"
|
317
|
+
):
|
318
|
+
"""
|
319
|
+
Get administrative boundaries for a specific level or all levels.
|
320
|
+
|
321
|
+
This is a convenience method that can retrieve boundaries for a single
|
322
|
+
administrative level or attempt to fetch all available levels.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
view_uuid (str): The UUID of the view to query.
|
326
|
+
admin_level (int, optional): Administrative level to retrieve
|
327
|
+
(0=country, 1=region, etc.). If None, attempts to fetch all levels.
|
328
|
+
geom (str, optional): Geometry inclusion level. Options:
|
329
|
+
- "no_geom": No geometry data
|
330
|
+
- "centroid": Only centroid points
|
331
|
+
- "full_geom": Complete boundary geometries
|
332
|
+
Defaults to "full_geom".
|
333
|
+
format (str, optional): Response format ("json" or "geojson").
|
334
|
+
Defaults to "geojson".
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
dict: JSON/GeoJSON response containing administrative boundaries
|
338
|
+
in the specified format. For GeoJSON, returns a FeatureCollection.
|
339
|
+
|
340
|
+
Raises:
|
341
|
+
requests.HTTPError: If the API request fails or parameters are invalid.
|
342
|
+
"""
|
343
|
+
# Construct the endpoint based on whether admin_level is provided
|
344
|
+
if admin_level is not None:
|
345
|
+
endpoint = (
|
346
|
+
f"{self.base_url}/search/view/{view_uuid}/entity/level/{admin_level}/"
|
347
|
+
)
|
348
|
+
else:
|
349
|
+
# For all levels, we need to fetch level 0 and then get children for each entity
|
350
|
+
endpoint = f"{self.base_url}/search/view/{view_uuid}/entity/list/"
|
351
|
+
|
352
|
+
params = {
|
353
|
+
"geom": geom,
|
354
|
+
"format": format,
|
355
|
+
"page_size": 100,
|
356
|
+
}
|
357
|
+
|
358
|
+
response = self._make_request("GET", endpoint, params=params)
|
359
|
+
return response.json()
|
360
|
+
|
361
|
+
def get_vector_tiles_url(self, view_info):
|
362
|
+
"""
|
363
|
+
Generate an authenticated URL for accessing vector tiles.
|
364
|
+
|
365
|
+
Vector tiles are used for efficient map rendering and can be consumed
|
366
|
+
by mapping libraries like Mapbox GL JS or OpenLayers.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
view_info (dict): Dictionary containing view information that must
|
370
|
+
include a 'vector_tiles' key with the base vector tiles URL.
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
str: Fully authenticated vector tiles URL with API key and user email
|
374
|
+
parameters appended for access control.
|
375
|
+
|
376
|
+
Raises:
|
377
|
+
ValueError: If 'vector_tiles' key is not found in view_info.
|
378
|
+
"""
|
379
|
+
if "vector_tiles" not in view_info:
|
380
|
+
raise ValueError("Vector tiles URL not found in view information")
|
381
|
+
|
382
|
+
vector_tiles_url = view_info["vector_tiles"]
|
383
|
+
|
384
|
+
# Parse out the timestamp parameter if it exists
|
385
|
+
if "?t=" in vector_tiles_url:
|
386
|
+
base_url, timestamp = vector_tiles_url.split("?t=")
|
387
|
+
return f"{base_url}?t={timestamp}&token={self.api_key}&georepo_user_key={self.email}"
|
388
|
+
else:
|
389
|
+
return (
|
390
|
+
f"{vector_tiles_url}?token={self.api_key}&georepo_user_key={self.email}"
|
391
|
+
)
|
392
|
+
|
393
|
+
def find_country_by_iso3(self, view_uuid, iso3_code):
|
394
|
+
"""
|
395
|
+
Find a country entity using its ISO3 country code.
|
396
|
+
|
397
|
+
This method searches through all level-0 (country) entities to find
|
398
|
+
one that matches the provided ISO3 code. It checks both the entity's
|
399
|
+
Ucode and any external codes stored in the ext_codes field.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
view_uuid (str): The UUID of the view to search within.
|
403
|
+
iso3_code (str): The ISO3 country code to search for (e.g., 'USA', 'KEN', 'BRA').
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
dict or None: Entity information dictionary for the matching country
|
407
|
+
if found, including properties like name, ucode, admin_level, etc.
|
408
|
+
Returns None if no matching country is found.
|
409
|
+
|
410
|
+
Note:
|
411
|
+
This method handles pagination automatically to search through all
|
412
|
+
available countries in the dataset, which may involve multiple API calls.
|
413
|
+
|
414
|
+
Raises:
|
415
|
+
requests.HTTPError: If the API request fails or view_uuid is invalid.
|
416
|
+
"""
|
417
|
+
# Admin level 0 represents countries
|
418
|
+
endpoint = f"{self.base_url}/search/view/{view_uuid}/entity/level/0/"
|
419
|
+
params = {
|
420
|
+
"page_size": 100,
|
421
|
+
"geom": "no_geom",
|
422
|
+
}
|
423
|
+
|
424
|
+
# need to paginate since it can be a large dataset
|
425
|
+
all_countries = []
|
426
|
+
page = 1
|
427
|
+
|
428
|
+
while True:
|
429
|
+
params["page"] = page
|
430
|
+
response = self._make_request("GET", endpoint, params=params)
|
431
|
+
data = response.json()
|
432
|
+
|
433
|
+
countries = data.get("results", [])
|
434
|
+
all_countries.extend(countries)
|
435
|
+
|
436
|
+
# check if there are more pages
|
437
|
+
if page >= data.get("total_page", 1):
|
438
|
+
break
|
439
|
+
|
440
|
+
page += 1
|
441
|
+
|
442
|
+
# Search by ISO3 code
|
443
|
+
for country in all_countries:
|
444
|
+
# Check if ISO3 code is in the ucode (typically at the beginning)
|
445
|
+
if country["ucode"].startswith(iso3_code + "_"):
|
446
|
+
return country
|
447
|
+
|
448
|
+
# Also check in ext_codes which may contain the ISO3 code
|
449
|
+
ext_codes = country.get("ext_codes", {})
|
450
|
+
if ext_codes:
|
451
|
+
# Check if ISO3 is directly in ext_codes
|
452
|
+
if (
|
453
|
+
ext_codes.get("PCode", "") == iso3_code
|
454
|
+
or ext_codes.get("default", "") == iso3_code
|
455
|
+
):
|
456
|
+
return country
|
457
|
+
|
458
|
+
return None
|
459
|
+
|
460
|
+
|
461
|
+
def find_admin_boundaries_module():
|
462
|
+
"""
|
463
|
+
Find and return the UUID of the Admin Boundaries module.
|
464
|
+
|
465
|
+
This is a convenience function that searches through all available modules
|
466
|
+
to locate the one named "Admin Boundaries", which typically contains
|
467
|
+
administrative boundary datasets.
|
468
|
+
|
469
|
+
Returns:
|
470
|
+
str: The UUID of the Admin Boundaries module.
|
471
|
+
|
472
|
+
Raises:
|
473
|
+
ValueError: If the Admin Boundaries module is not found.
|
474
|
+
"""
|
475
|
+
client = GeoRepoClient()
|
476
|
+
modules = client.list_modules()
|
477
|
+
|
478
|
+
for module in modules.get("results", []):
|
479
|
+
if module["name"] == "Admin Boundaries":
|
480
|
+
return module["uuid"]
|
481
|
+
|
482
|
+
raise ValueError("Admin Boundaries module not found")
|
483
|
+
|
484
|
+
|
485
|
+
def get_country_boundaries_by_iso3(
|
486
|
+
iso3_code, client: GeoRepoClient = None, admin_level=None
|
487
|
+
):
|
488
|
+
"""
|
489
|
+
Get administrative boundaries for a specific country using its ISO3 code.
|
490
|
+
|
491
|
+
This function provides a high-level interface to retrieve country boundaries
|
492
|
+
by automatically finding the appropriate module, dataset, and view, then
|
493
|
+
fetching the requested administrative boundaries.
|
494
|
+
|
495
|
+
The function will:
|
496
|
+
1. Find the Admin Boundaries module
|
497
|
+
2. Locate a global dataset within that module
|
498
|
+
3. Find the latest view of that dataset
|
499
|
+
4. Search for the country using the ISO3 code
|
500
|
+
5. Look for a country-specific view if available
|
501
|
+
6. Retrieve boundaries at the specified admin level or all levels
|
502
|
+
|
503
|
+
Args:
|
504
|
+
iso3_code (str): The ISO3 country code (e.g., 'USA', 'KEN', 'BRA').
|
505
|
+
admin_level (int, optional): The administrative level to retrieve:
|
506
|
+
- 0: Country level
|
507
|
+
- 1: State/Province/Region level
|
508
|
+
- 2: District/County level
|
509
|
+
- 3: Sub-district/Municipality level
|
510
|
+
- etc.
|
511
|
+
If None, retrieves all available administrative levels.
|
512
|
+
|
513
|
+
Returns:
|
514
|
+
dict: A GeoJSON FeatureCollection containing the requested boundaries.
|
515
|
+
Each feature includes geometry and properties for the administrative unit.
|
516
|
+
|
517
|
+
Raises:
|
518
|
+
ValueError: If the Admin Boundaries module, datasets, views, or country
|
519
|
+
cannot be found.
|
520
|
+
requests.HTTPError: If any API requests fail.
|
521
|
+
|
522
|
+
Note:
|
523
|
+
This function may make multiple API calls and can take some time for
|
524
|
+
countries with many administrative units. It handles pagination
|
525
|
+
automatically and attempts to use country-specific views when available
|
526
|
+
for better performance.
|
527
|
+
|
528
|
+
Example:
|
529
|
+
>>> # Get all administrative levels for Kenya
|
530
|
+
>>> boundaries = get_country_boundaries_by_iso3('KEN')
|
531
|
+
>>>
|
532
|
+
>>> # Get only province-level boundaries for Kenya
|
533
|
+
>>> provinces = get_country_boundaries_by_iso3('KEN', admin_level=1)
|
534
|
+
"""
|
535
|
+
client = client or GeoRepoClient()
|
536
|
+
|
537
|
+
client.logger.info("Finding Admin Boundaries module...")
|
538
|
+
modules = client.list_modules()
|
539
|
+
admin_module_uuid = None
|
540
|
+
|
541
|
+
for module in modules.get("results", []):
|
542
|
+
if "Admin Boundaries" in module["name"]:
|
543
|
+
admin_module_uuid = module["uuid"]
|
544
|
+
client.logger.info(
|
545
|
+
f"Found Admin Boundaries module: {module['name']} ({admin_module_uuid})"
|
546
|
+
)
|
547
|
+
break
|
548
|
+
|
549
|
+
if not admin_module_uuid:
|
550
|
+
raise ValueError("Admin Boundaries module not found")
|
551
|
+
|
552
|
+
client.logger.info(f"Finding datasets in the Admin Boundaries module...")
|
553
|
+
datasets = client.list_datasets_by_module(admin_module_uuid)
|
554
|
+
global_dataset_uuid = None
|
555
|
+
|
556
|
+
for dataset in datasets.get("results", []):
|
557
|
+
if any(keyword in dataset["name"].lower() for keyword in ["global"]):
|
558
|
+
global_dataset_uuid = dataset["uuid"]
|
559
|
+
client.logger.info(
|
560
|
+
f"Found global dataset: {dataset['name']} ({global_dataset_uuid})"
|
561
|
+
)
|
562
|
+
break
|
563
|
+
|
564
|
+
if not global_dataset_uuid:
|
565
|
+
if datasets.get("results"):
|
566
|
+
global_dataset_uuid = datasets["results"][0]["uuid"]
|
567
|
+
client.logger.info(
|
568
|
+
f"Using first available dataset: {datasets['results'][0]['name']} ({global_dataset_uuid})"
|
569
|
+
)
|
570
|
+
else:
|
571
|
+
raise ValueError("No datasets found in the Admin Boundaries module")
|
572
|
+
|
573
|
+
client.logger.info(f"Finding views in the dataset...")
|
574
|
+
views = client.list_views_by_dataset(global_dataset_uuid)
|
575
|
+
latest_view_uuid = None
|
576
|
+
|
577
|
+
for view in views.get("results", []):
|
578
|
+
if "tags" in view and "latest" in view["tags"]:
|
579
|
+
latest_view_uuid = view["uuid"]
|
580
|
+
client.logger.info(
|
581
|
+
f"Found latest view: {view['name']} ({latest_view_uuid})"
|
582
|
+
)
|
583
|
+
break
|
584
|
+
|
585
|
+
if not latest_view_uuid:
|
586
|
+
if views.get("results"):
|
587
|
+
latest_view_uuid = views["results"][0]["uuid"]
|
588
|
+
client.logger.info(
|
589
|
+
f"Using first available view: {views['results'][0]['name']} ({latest_view_uuid})"
|
590
|
+
)
|
591
|
+
else:
|
592
|
+
raise ValueError("No views found in the dataset")
|
593
|
+
|
594
|
+
# Search for the country by ISO3 code
|
595
|
+
client.logger.info(f"Searching for country with ISO3 code: {iso3_code}...")
|
596
|
+
country_entity = client.find_country_by_iso3(latest_view_uuid, iso3_code)
|
597
|
+
|
598
|
+
if not country_entity:
|
599
|
+
raise ValueError(f"Country with ISO3 code '{iso3_code}' not found")
|
600
|
+
|
601
|
+
country_ucode = country_entity["ucode"]
|
602
|
+
country_name = country_entity["name"]
|
603
|
+
client.logger.info(f"Found country: {country_name} (Ucode: {country_ucode})")
|
604
|
+
|
605
|
+
# Search for country-specific view
|
606
|
+
client.logger.info(f"Checking for country-specific view...")
|
607
|
+
country_view_uuid = None
|
608
|
+
all_views = []
|
609
|
+
|
610
|
+
# Need to fetch all pages of views
|
611
|
+
page = 1
|
612
|
+
while True:
|
613
|
+
views_page = client.list_views_by_dataset(global_dataset_uuid, page=page)
|
614
|
+
all_views.extend(views_page.get("results", []))
|
615
|
+
if page >= views_page.get("total_page", 1):
|
616
|
+
break
|
617
|
+
page += 1
|
618
|
+
|
619
|
+
# Look for a view specifically for this country
|
620
|
+
for view in all_views:
|
621
|
+
if country_name.lower() in view["name"].lower() and "latest" in view.get(
|
622
|
+
"tags", []
|
623
|
+
):
|
624
|
+
country_view_uuid = view["uuid"]
|
625
|
+
client.logger.info(
|
626
|
+
f"Found country-specific view: {view['name']} ({country_view_uuid})"
|
627
|
+
)
|
628
|
+
break
|
629
|
+
|
630
|
+
# Get boundaries based on admin level
|
631
|
+
if country_view_uuid:
|
632
|
+
client.logger.info(country_view_uuid)
|
633
|
+
# If we found a view specific to this country, use it
|
634
|
+
client.logger.info(f"Getting admin boundaries from country-specific view...")
|
635
|
+
if admin_level is not None:
|
636
|
+
client.logger.info(f"Fetching admin level {admin_level} boundaries...")
|
637
|
+
|
638
|
+
# Handle pagination for large datasets
|
639
|
+
all_features = []
|
640
|
+
page = 1
|
641
|
+
while True:
|
642
|
+
result, meta = client.list_entities_by_admin_level(
|
643
|
+
country_view_uuid,
|
644
|
+
admin_level,
|
645
|
+
geom="full_geom",
|
646
|
+
format="geojson",
|
647
|
+
page=page,
|
648
|
+
page_size=50,
|
649
|
+
)
|
650
|
+
|
651
|
+
# Add features to our collection
|
652
|
+
if "features" in result:
|
653
|
+
all_features.extend(result["features"])
|
654
|
+
elif "results" in result:
|
655
|
+
# Convert entities to GeoJSON features if needed
|
656
|
+
for entity in result["results"]:
|
657
|
+
if "geometry" in entity:
|
658
|
+
feature = {
|
659
|
+
"type": "Feature",
|
660
|
+
"properties": {
|
661
|
+
k: v for k, v in entity.items() if k != "geometry"
|
662
|
+
},
|
663
|
+
"geometry": entity["geometry"],
|
664
|
+
}
|
665
|
+
all_features.append(feature)
|
666
|
+
|
667
|
+
# Check if there are more pages
|
668
|
+
if page >= meta["total_page"]:
|
669
|
+
break
|
670
|
+
|
671
|
+
page += 1
|
672
|
+
|
673
|
+
boundaries = {"type": "FeatureCollection", "features": all_features}
|
674
|
+
else:
|
675
|
+
# Get all admin levels by fetching each level separately
|
676
|
+
boundaries = {"type": "FeatureCollection", "features": []}
|
677
|
+
|
678
|
+
# Get dataset details to find available admin levels
|
679
|
+
dataset_details = client.get_dataset_details(global_dataset_uuid)
|
680
|
+
max_level = 0
|
681
|
+
|
682
|
+
for level_info in dataset_details.get("dataset_levels", []):
|
683
|
+
if isinstance(level_info.get("level"), int):
|
684
|
+
max_level = max(max_level, level_info["level"])
|
685
|
+
|
686
|
+
client.logger.info(f"Dataset has admin levels from 0 to {max_level}")
|
687
|
+
|
688
|
+
# Fetch each admin level
|
689
|
+
for level in range(max_level + 1):
|
690
|
+
client.logger.info(f"Fetching admin level {level}...")
|
691
|
+
try:
|
692
|
+
level_data, meta = client.list_entities_by_admin_level(
|
693
|
+
country_view_uuid, level, geom="full_geom", format="geojson"
|
694
|
+
)
|
695
|
+
|
696
|
+
if "features" in level_data:
|
697
|
+
boundaries["features"].extend(level_data["features"])
|
698
|
+
elif "results" in level_data:
|
699
|
+
# Process each page of results
|
700
|
+
page = 1
|
701
|
+
while True:
|
702
|
+
result, meta = client.list_entities_by_admin_level(
|
703
|
+
country_view_uuid,
|
704
|
+
level,
|
705
|
+
geom="full_geom",
|
706
|
+
format="geojson",
|
707
|
+
page=page,
|
708
|
+
)
|
709
|
+
|
710
|
+
if "features" in result:
|
711
|
+
boundaries["features"].extend(result["features"])
|
712
|
+
|
713
|
+
# Check for more pages
|
714
|
+
if page >= meta["total_page"]:
|
715
|
+
break
|
716
|
+
|
717
|
+
page += 1
|
718
|
+
|
719
|
+
except Exception as e:
|
720
|
+
client.logger.warning(f"Error fetching admin level {level}: {e}")
|
721
|
+
else:
|
722
|
+
# Use the global view with filtering
|
723
|
+
client.logger.info(f"Using global view and filtering by country...")
|
724
|
+
|
725
|
+
# Function to recursively get all descendants
|
726
|
+
def get_all_children(
|
727
|
+
parent_ucode, view_uuid, level=1, max_depth=5, admin_level_filter=None
|
728
|
+
):
|
729
|
+
"""
|
730
|
+
Recursively retrieve all child entities of a parent entity.
|
731
|
+
|
732
|
+
Args:
|
733
|
+
parent_ucode (str): The Ucode of the parent entity.
|
734
|
+
view_uuid (str): The UUID of the view to query.
|
735
|
+
level (int): Current recursion level (for depth limiting).
|
736
|
+
max_depth (int): Maximum recursion depth to prevent infinite loops.
|
737
|
+
admin_level_filter (int, optional): If specified, only return
|
738
|
+
entities at this specific administrative level.
|
739
|
+
|
740
|
+
Returns:
|
741
|
+
list: List of GeoJSON features for all child entities.
|
742
|
+
"""
|
743
|
+
if level > max_depth:
|
744
|
+
return []
|
745
|
+
|
746
|
+
try:
|
747
|
+
children = client.list_entity_children(view_uuid, parent_ucode)
|
748
|
+
features = []
|
749
|
+
|
750
|
+
for child in children.get("results", []):
|
751
|
+
# Skip if we're filtering by admin level and this doesn't match
|
752
|
+
if (
|
753
|
+
admin_level_filter is not None
|
754
|
+
and child.get("admin_level") != admin_level_filter
|
755
|
+
):
|
756
|
+
continue
|
757
|
+
|
758
|
+
# Get the child with full geometry
|
759
|
+
child_entity = client.get_entity_by_ucode(child["ucode"])
|
760
|
+
if "features" in child_entity:
|
761
|
+
features.extend(child_entity["features"])
|
762
|
+
|
763
|
+
# Recursively get grandchildren if not filtering by admin level
|
764
|
+
if admin_level_filter is None:
|
765
|
+
features.extend(
|
766
|
+
get_all_children(
|
767
|
+
child["ucode"], view_uuid, level + 1, max_depth
|
768
|
+
)
|
769
|
+
)
|
770
|
+
|
771
|
+
return features
|
772
|
+
except Exception as e:
|
773
|
+
client.logger.warning(f"Error getting children for {parent_ucode}: {e}")
|
774
|
+
return []
|
775
|
+
|
776
|
+
# Start with the country boundaries
|
777
|
+
boundaries = {"type": "FeatureCollection", "features": []}
|
778
|
+
|
779
|
+
# If admin_level is 0, just get the country entity
|
780
|
+
if admin_level == 0:
|
781
|
+
country_entity = client.get_entity_by_ucode(country_ucode)
|
782
|
+
if "features" in country_entity:
|
783
|
+
boundaries["features"].extend(country_entity["features"])
|
784
|
+
# If specific admin level requested, get all entities at that level
|
785
|
+
elif admin_level is not None:
|
786
|
+
children_features = get_all_children(
|
787
|
+
country_ucode,
|
788
|
+
latest_view_uuid,
|
789
|
+
max_depth=admin_level + 1,
|
790
|
+
admin_level_filter=admin_level,
|
791
|
+
)
|
792
|
+
boundaries["features"].extend(children_features)
|
793
|
+
# If no admin_level specified, get all levels
|
794
|
+
else:
|
795
|
+
# Start with the country entity
|
796
|
+
country_entity = client.get_entity_by_ucode(country_ucode)
|
797
|
+
if "features" in country_entity:
|
798
|
+
boundaries["features"].extend(country_entity["features"])
|
799
|
+
|
800
|
+
# Get all descendants
|
801
|
+
children_features = get_all_children(
|
802
|
+
country_ucode, latest_view_uuid, max_depth=5
|
803
|
+
)
|
804
|
+
boundaries["features"].extend(children_features)
|
805
|
+
|
806
|
+
return boundaries
|