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.
Files changed (47) hide show
  1. giga_spatial-0.6.0.dist-info/METADATA +141 -0
  2. giga_spatial-0.6.0.dist-info/RECORD +47 -0
  3. giga_spatial-0.6.0.dist-info/WHEEL +5 -0
  4. giga_spatial-0.6.0.dist-info/licenses/LICENSE +661 -0
  5. giga_spatial-0.6.0.dist-info/top_level.txt +1 -0
  6. gigaspatial/__init__.py +1 -0
  7. gigaspatial/config.py +226 -0
  8. gigaspatial/core/__init__.py +0 -0
  9. gigaspatial/core/io/__init__.py +5 -0
  10. gigaspatial/core/io/adls_data_store.py +325 -0
  11. gigaspatial/core/io/data_api.py +113 -0
  12. gigaspatial/core/io/data_store.py +147 -0
  13. gigaspatial/core/io/local_data_store.py +92 -0
  14. gigaspatial/core/io/readers.py +265 -0
  15. gigaspatial/core/io/writers.py +128 -0
  16. gigaspatial/core/schemas/__init__.py +0 -0
  17. gigaspatial/core/schemas/entity.py +244 -0
  18. gigaspatial/generators/__init__.py +2 -0
  19. gigaspatial/generators/poi.py +636 -0
  20. gigaspatial/generators/zonal/__init__.py +3 -0
  21. gigaspatial/generators/zonal/base.py +370 -0
  22. gigaspatial/generators/zonal/geometry.py +439 -0
  23. gigaspatial/generators/zonal/mercator.py +78 -0
  24. gigaspatial/grid/__init__.py +1 -0
  25. gigaspatial/grid/mercator_tiles.py +286 -0
  26. gigaspatial/handlers/__init__.py +40 -0
  27. gigaspatial/handlers/base.py +761 -0
  28. gigaspatial/handlers/boundaries.py +305 -0
  29. gigaspatial/handlers/ghsl.py +772 -0
  30. gigaspatial/handlers/giga.py +145 -0
  31. gigaspatial/handlers/google_open_buildings.py +472 -0
  32. gigaspatial/handlers/hdx.py +241 -0
  33. gigaspatial/handlers/mapbox_image.py +208 -0
  34. gigaspatial/handlers/maxar_image.py +291 -0
  35. gigaspatial/handlers/microsoft_global_buildings.py +548 -0
  36. gigaspatial/handlers/ookla_speedtest.py +199 -0
  37. gigaspatial/handlers/opencellid.py +290 -0
  38. gigaspatial/handlers/osm.py +356 -0
  39. gigaspatial/handlers/overture.py +126 -0
  40. gigaspatial/handlers/rwi.py +157 -0
  41. gigaspatial/handlers/unicef_georepo.py +806 -0
  42. gigaspatial/handlers/worldpop.py +266 -0
  43. gigaspatial/processing/__init__.py +4 -0
  44. gigaspatial/processing/geo.py +1054 -0
  45. gigaspatial/processing/sat_images.py +39 -0
  46. gigaspatial/processing/tif_processor.py +477 -0
  47. 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