giga-spatial 0.6.8__py3-none-any.whl → 0.7.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.
@@ -1,13 +1,29 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: giga-spatial
3
- Version: 0.6.8
3
+ Version: 0.7.0
4
4
  Summary: A package for spatial data download & processing
5
- Home-page: https://github.com/unicef/giga-spatial
6
5
  Author: Utku Can Ozturk
7
6
  Author-email: utkucanozturk@gmail.com
8
7
  License: AGPL-3.0-or-later
8
+ Project-URL: Homepage, https://github.com/unicef/giga-spatial
9
+ Project-URL: Documentation, https://unicef.github.io/giga-spatial/
10
+ Project-URL: Source, https://github.com/unicef/giga-spatial
11
+ Project-URL: Issue Tracker, https://github.com/unicef/giga-spatial/issues
12
+ Project-URL: Discussions, https://github.com/unicef/giga-spatial/discussions
13
+ Project-URL: Changelog, https://unicef.github.io/giga-spatial/changelog
9
14
  Keywords: gigaspatial,spatial,geospatial,gis,remote sensing,data processing,download,openstreetmap,osm,ghsl,grid,point of interest,POI,raster,vector,school connectivity,unicef,giga,mapping,analysis,python
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Education
18
+ Classifier: Intended Audience :: Healthcare Industry
19
+ Classifier: Intended Audience :: Science/Research
20
+ Classifier: Intended Audience :: Telecommunications Industry
21
+ Classifier: Programming Language :: Python
10
22
  Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Topic :: Scientific/Engineering :: GIS
11
27
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3
12
28
  Classifier: Operating System :: OS Independent
13
29
  Requires-Python: >=3.10
@@ -24,8 +40,9 @@ Requires-Dist: pycountry==24.6.1
24
40
  Requires-Dist: pydantic>=2.10.6
25
41
  Requires-Dist: rasterio==1.3.10
26
42
  Requires-Dist: Requests==2.32.3
27
- Requires-Dist: scipy==1.15.1
43
+ Requires-Dist: scipy>=1.15.1
28
44
  Requires-Dist: Shapely>=2.0.7
45
+ Requires-Dist: networkx>=3.2.1
29
46
  Requires-Dist: tqdm==4.65.0
30
47
  Requires-Dist: OWSLib==0.32.1
31
48
  Requires-Dist: pydantic-settings>=2.7.1
@@ -33,15 +50,16 @@ Requires-Dist: hdx-python-api>=6.3.8
33
50
  Requires-Dist: bs4==0.0.2
34
51
  Requires-Dist: sqlalchemy-trino==0.5.0
35
52
  Requires-Dist: dask>=2024.12.1
53
+ Requires-Dist: h3>=4.2.0
36
54
  Dynamic: author
37
55
  Dynamic: author-email
38
56
  Dynamic: classifier
39
57
  Dynamic: description
40
58
  Dynamic: description-content-type
41
- Dynamic: home-page
42
59
  Dynamic: keywords
43
60
  Dynamic: license
44
61
  Dynamic: license-file
62
+ Dynamic: project-url
45
63
  Dynamic: requires-dist
46
64
  Dynamic: requires-python
47
65
  Dynamic: summary
@@ -53,6 +71,14 @@ Dynamic: summary
53
71
 
54
72
  # GigaSpatial
55
73
 
74
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-brightgreen.svg)](https://opensource.org/license/agpl-v3)
75
+ [![PyPI version](https://badge.fury.io/py/giga-spatial.svg)](https://badge.fury.io/py/giga-spatial)
76
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/giga-spatial.svg?color=dark-green)](https://pypi.org/project/giga-spatial/)
77
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
78
+ [![PyPI Downloads](https://static.pepy.tech/badge/giga-spatial)](https://pepy.tech/projects/giga-spatial)
79
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/y/unicef/giga-spatial.svg?color=dark-green)](https://github.com/unicef/giga-spatial/graphs/contributors)
80
+
81
+
56
82
  ## About Giga
57
83
 
58
84
  [Giga](https://giga.global/) is a UNICEF-ITU initiative to connect every school to the Internet and every young person to information, opportunity and choice.
@@ -1,13 +1,13 @@
1
- giga_spatial-0.6.8.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
2
- gigaspatial/__init__.py,sha256=wwbrOIx2rQA0YHGob_KGFY89qGDsh20rh2M3y3Ua458,22
1
+ giga_spatial-0.7.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
2
+ gigaspatial/__init__.py,sha256=RaANGbRu5e-vehwXI1-Qe2ggPPfs1TQaZj072JdbLk4,22
3
3
  gigaspatial/config.py,sha256=pLbxGc08OHT2IfTBzZVuIJTPR2vvg3KTFfvciOtRswk,9304
4
4
  gigaspatial/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  gigaspatial/core/io/__init__.py,sha256=stlpgEeHf5KIb2ZW8yEbdJK5iq6n_wX4DPmKyR9PK-w,317
6
- gigaspatial/core/io/adls_data_store.py,sha256=Zv-D_8d_2h57HnCUTJb0JWWjXqR_0XH4F8Nu_UFZK9E,11975
6
+ gigaspatial/core/io/adls_data_store.py,sha256=Pr1FKpzFb2TSeidMSGhhNSDk1tFxAi4G1aPZ_No_xUs,15783
7
7
  gigaspatial/core/io/data_api.py,sha256=0TtXEfqIz9m1uC8ktcUhZZtx6ZIPY_SIXNnlIodcry8,3970
8
8
  gigaspatial/core/io/data_store.py,sha256=mi8fy78Dtwj4dpKkyDM6kTlna1lfCQ5ro2hUAOFr83A,3223
9
9
  gigaspatial/core/io/database.py,sha256=kR9ZHuIpZEjXmyj_PnMn6ManQ504kl_f1kJRjHuNWwk,11378
10
- gigaspatial/core/io/local_data_store.py,sha256=hcu7DNYa3AL6sEPMqguzxWal_bnP7CIpbwpoiyf5TCw,2933
10
+ gigaspatial/core/io/local_data_store.py,sha256=ZMkj5IteQmGLy8G1L2qt4_WKxosA-F_0evF-oW-JRKo,3227
11
11
  gigaspatial/core/io/readers.py,sha256=gqFKGRCsAP_EBXipqGtT8MEV-x0u6SrCqaSiOC5YPTA,9284
12
12
  gigaspatial/core/io/writers.py,sha256=asb56ZHQEWO2rdilIq7QywDRk8yfebecWv1KwzUpaXI,4367
13
13
  gigaspatial/core/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,12 +17,13 @@ gigaspatial/generators/poi.py,sha256=FMYH2yPFuCeYiwj-pHYe83bAxXPkQP28VDQx5L2eEnQ
17
17
  gigaspatial/generators/zonal/__init__.py,sha256=egnpvGVeIOS2Zg516AT84tJnIqS4owxmMLLmBQJmK7Y,301
18
18
  gigaspatial/generators/zonal/admin.py,sha256=rgOyQX3f_g9qnXqrf-NkR2GEdwOqjNuPNe1H7AUVsfg,3698
19
19
  gigaspatial/generators/zonal/base.py,sha256=aoq9AmawPnxu04YI7hPmJzvBTyuUqBQuMZAEUdiVStg,22818
20
- gigaspatial/generators/zonal/geometry.py,sha256=JbaQ4WS6g45g3uBaATfJd7DS3wW-GPW0GW9FtsvGH-c,21284
20
+ gigaspatial/generators/zonal/geometry.py,sha256=iDpITqTTU_Tzd0eEO9suuoOA9Dpc2kbtwRgOX8C5zAE,21485
21
21
  gigaspatial/generators/zonal/mercator.py,sha256=fA02j30PWB5BVjrbNGCMjiOw-ds182yK7R27z8mWFug,5291
22
22
  gigaspatial/grid/__init__.py,sha256=ypSSyZ4fYtMNc4IG7chSD7NkUfS2bv9KWRsKR1D9pDI,80
23
- gigaspatial/grid/mercator_tiles.py,sha256=mAYZDBJ1U0l3z9i4rh5OqiPhOGWcBYzUOI1cvQG_Ff4,11240
23
+ gigaspatial/grid/h3.py,sha256=0d_rNNvwxc0LDD2qKCd2gTy_16_5sgPwJwk29z5pE9U,15310
24
+ gigaspatial/grid/mercator_tiles.py,sha256=dzUDbXe0QHEC4s334IVT76d41D35dbd-QiN2LnkDdvA,11240
24
25
  gigaspatial/handlers/__init__.py,sha256=T0_6OxXQ59yTu9g2P6P9vnepudOWp_85R4WQKxRF94c,1618
25
- gigaspatial/handlers/base.py,sha256=ZcahOEMmS_uECBqOhEEdCoDcPCbVItA5mRS5zEUqR-s,27194
26
+ gigaspatial/handlers/base.py,sha256=wxkB8fniOIzgsg0WF9WkaZbAmFhNkqtZ_LGD8Ch53Mw,27238
26
27
  gigaspatial/handlers/boundaries.py,sha256=jtWyQt3iAzS77mbAOi7mjh3cv_YCV3uB_r1h56gCfeY,20729
27
28
  gigaspatial/handlers/ghsl.py,sha256=aSEVQVANzJf8O8TiQYmfwyeM43ZaO65VJHmiuLSQfLs,30524
28
29
  gigaspatial/handlers/giga.py,sha256=F5ZfcE37a24X-c6Xhyt72C9eZZbyN_gV7w_InxKFMQQ,28348
@@ -40,11 +41,11 @@ gigaspatial/handlers/unicef_georepo.py,sha256=ODYNvkU_UKgOHXT--0MqmJ4Uk6U1_mp9xg
40
41
  gigaspatial/handlers/worldpop.py,sha256=jV166EP02Xdj8jiT8aQi4sexds8Qd3KRGHXqq70_Sdk,30161
41
42
  gigaspatial/processing/__init__.py,sha256=QDVL-QbLCrIb19lrajP7LrHNdGdnsLeGcvAs_jQpdRM,183
42
43
  gigaspatial/processing/algorithms.py,sha256=6fBCwbZrI_ISWJ7UpkH6moq1vw-7dBy14yXSLHZprqY,6591
43
- gigaspatial/processing/geo.py,sha256=8kD7-LQdGzKVfuZDWr3zK5uQhPzgxbZ3JBPosLRBJ5M,41390
44
+ gigaspatial/processing/geo.py,sha256=i8MMXrf05dik-LkmeSZ45siWLT9Dgk0M-tjWKx5jVdk,41686
44
45
  gigaspatial/processing/sat_images.py,sha256=YUbH5MFNzl6NX49Obk14WaFcr1s3SyGJIOk-kRpbBNg,1429
45
- gigaspatial/processing/tif_processor.py,sha256=dZRhMGj5r7DIu8Bop31NPbN1IdOK1syIlCOFTjTiiyo,40024
46
+ gigaspatial/processing/tif_processor.py,sha256=NSTxxeRtLS70Ki_AjaOfSGYpAFevkQnM-ZfYaE1RF7M,46793
46
47
  gigaspatial/processing/utils.py,sha256=HC85vGKQakxlkoQAkZmeAXWHsenAwTIRn7jPKUA7x20,1500
47
- giga_spatial-0.6.8.dist-info/METADATA,sha256=f9MSxVRX6yhfkeoGhrsO5CdbAmVVHfhq9T4Ip7CRac4,7537
48
- giga_spatial-0.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
49
- giga_spatial-0.6.8.dist-info/top_level.txt,sha256=LZsccgw6H4zXT7m6Y4XChm-Y5LjHAwZ2hkGN_B3ExmI,12
50
- giga_spatial-0.6.8.dist-info/RECORD,,
48
+ giga_spatial-0.7.0.dist-info/METADATA,sha256=PZ1IWjRm10ol2wfFifI1Sp5tm2v-dhdr-ip0NiRKB_s,9277
49
+ giga_spatial-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ giga_spatial-0.7.0.dist-info/top_level.txt,sha256=LZsccgw6H4zXT7m6Y4XChm-Y5LjHAwZ2hkGN_B3ExmI,12
51
+ giga_spatial-0.7.0.dist-info/RECORD,,
gigaspatial/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.6.8"
1
+ __version__ = "0.7.0"
@@ -1,4 +1,5 @@
1
1
  from azure.storage.blob import BlobServiceClient
2
+ import time
2
3
  import io
3
4
  import contextlib
4
5
  import logging
@@ -151,20 +152,45 @@ class ADLSDataStore(DataStore):
151
152
  "\\", "/"
152
153
  )
153
154
 
154
- # Create a source blob client
155
- source_blob_client = self.container_client.get_blob_client(blob.name)
155
+ # Use copy_file method to copy each file
156
+ self.copy_file(blob.name, new_blob_path, overwrite=True)
156
157
 
157
- # Create a destination blob client
158
- destination_blob_client = self.container_client.get_blob_client(
159
- new_blob_path
158
+ print(f"Copied directory from {source_dir} to {destination_dir}")
159
+ except Exception as e:
160
+ print(f"Failed to copy directory {source_dir}: {e}")
161
+
162
+ def copy_file(
163
+ self, source_path: str, destination_path: str, overwrite: bool = False
164
+ ):
165
+ """
166
+ Copies a single file from source to destination within the same container.
167
+
168
+ :param source_path: The source file path in the blob storage
169
+ :param destination_path: The destination file path in the blob storage
170
+ :param overwrite: If True, overwrite the destination file if it already exists
171
+ """
172
+ try:
173
+ if not self.file_exists(source_path):
174
+ raise FileNotFoundError(f"Source file not found: {source_path}")
175
+
176
+ if self.file_exists(destination_path) and not overwrite:
177
+ raise FileExistsError(
178
+ f"Destination file already exists and overwrite is False: {destination_path}"
160
179
  )
161
180
 
162
- # Start the copy operation
163
- destination_blob_client.start_copy_from_url(source_blob_client.url)
181
+ # Create source and destination blob clients
182
+ source_blob_client = self.container_client.get_blob_client(source_path)
183
+ destination_blob_client = self.container_client.get_blob_client(
184
+ destination_path
185
+ )
164
186
 
165
- print(f"Copied directory from {source_dir} to {destination_dir}")
187
+ # Start the server-side copy operation
188
+ destination_blob_client.start_copy_from_url(source_blob_client.url)
189
+
190
+ print(f"Copied file from {source_path} to {destination_path}")
166
191
  except Exception as e:
167
- print(f"Failed to copy directory {source_dir}: {e}")
192
+ print(f"Failed to copy file {source_path}: {e}")
193
+ raise
168
194
 
169
195
  def exists(self, path: str) -> bool:
170
196
  blob_client = self.blob_service_client.get_blob_client(
@@ -285,8 +311,20 @@ class ADLSDataStore(DataStore):
285
311
  return False
286
312
 
287
313
  def rmdir(self, dir: str) -> None:
288
- blobs = self.list_files(dir)
289
- self.container_client.delete_blobs(*blobs)
314
+ # Normalize directory path to ensure it targets all children
315
+ dir_path = dir.rstrip("/") + "/"
316
+
317
+ # Azure Blob batch delete has a hard limit on number of sub-requests
318
+ # per batch (currently 256). Delete in chunks to avoid
319
+ # ExceedsMaxBatchRequestCount errors.
320
+ blobs = list(self.list_files(dir_path))
321
+ if not blobs:
322
+ return
323
+
324
+ BATCH_LIMIT = 256
325
+ for start_idx in range(0, len(blobs), BATCH_LIMIT):
326
+ batch = blobs[start_idx : start_idx + BATCH_LIMIT]
327
+ self.container_client.delete_blobs(*batch)
290
328
 
291
329
  def mkdir(self, path: str, exist_ok: bool = False) -> None:
292
330
  """
@@ -323,3 +361,58 @@ class ADLSDataStore(DataStore):
323
361
  )
324
362
  if blob_client.exists():
325
363
  blob_client.delete_blob()
364
+
365
+ def rename(
366
+ self,
367
+ source_path: str,
368
+ destination_path: str,
369
+ overwrite: bool = False,
370
+ delete_source: bool = True,
371
+ wait: bool = True,
372
+ timeout_seconds: int = 300,
373
+ poll_interval_seconds: int = 1,
374
+ ) -> None:
375
+ """
376
+ Rename (move) a single file by copying to the new path and deleting the source.
377
+
378
+ :param source_path: Existing blob path
379
+ :param destination_path: Target blob path
380
+ :param overwrite: Overwrite destination if it already exists
381
+ :param delete_source: Delete original after successful copy
382
+ :param wait: Wait for the copy operation to complete
383
+ :param timeout_seconds: Max time to wait for copy to succeed
384
+ :param poll_interval_seconds: Polling interval while waiting
385
+ """
386
+
387
+ if not self.file_exists(source_path):
388
+ raise FileNotFoundError(f"Source file not found: {source_path}")
389
+
390
+ if self.file_exists(destination_path) and not overwrite:
391
+ raise FileExistsError(
392
+ f"Destination already exists and overwrite is False: {destination_path}"
393
+ )
394
+
395
+ # Use copy_file method to copy the file
396
+ self.copy_file(source_path, destination_path, overwrite=overwrite)
397
+
398
+ if wait:
399
+ # Wait for copy to complete if requested
400
+ dest_client = self.container_client.get_blob_client(destination_path)
401
+ deadline = time.time() + timeout_seconds
402
+ while True:
403
+ props = dest_client.get_blob_properties()
404
+ status = getattr(props.copy, "status", None)
405
+ if status == "success":
406
+ break
407
+ if status in {"aborted", "failed"}:
408
+ raise IOError(
409
+ f"Copy failed with status {status} from {source_path} to {destination_path}"
410
+ )
411
+ if time.time() > deadline:
412
+ raise TimeoutError(
413
+ f"Timed out waiting for copy to complete for {destination_path}"
414
+ )
415
+ time.sleep(poll_interval_seconds)
416
+
417
+ if delete_source:
418
+ self.remove(source_path)
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
  import os
3
+ import shutil
3
4
  from typing import Any, List, Generator, Tuple, Union, IO
4
5
 
5
6
  from .data_store import DataStore
@@ -79,6 +80,13 @@ class LocalDataStore(DataStore):
79
80
  if full_path.is_file():
80
81
  os.remove(full_path)
81
82
 
83
+ def copy_file(self, src: str, dst: str) -> None:
84
+ """Copy a file from src to dst."""
85
+ src_path = self._resolve_path(src)
86
+ dst_path = self._resolve_path(dst)
87
+ self.mkdir(str(dst_path.parent), exist_ok=True)
88
+ shutil.copy2(src_path, dst_path)
89
+
82
90
  def rmdir(self, directory: str) -> None:
83
91
  full_path = self._resolve_path(directory)
84
92
  if full_path.is_dir():
@@ -145,11 +145,18 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
145
145
  gpd.GeoDataFrame: A GeoDataFrame with 'zone_id' and 'geometry' columns.
146
146
  The zone_id column is renamed from the original zone_id_column if different.
147
147
  """
148
- # If we already have a GeoDataFrame, just rename the ID column if needed
149
- result = self._zone_gdf.copy()
150
- if self.zone_id_column != "zone_id":
151
- result = result.rename(columns={self.zone_id_column: "zone_id"})
152
- return result
148
+ # Since _zone_gdf is already created with 'zone_id' column in the constructor,
149
+ # we just need to return a copy of it
150
+ return self._zone_gdf.copy()
151
+
152
+ @property
153
+ def zone_gdf(self) -> gpd.GeoDataFrame:
154
+ """Override the base class zone_gdf property to ensure correct column names.
155
+
156
+ Returns:
157
+ gpd.GeoDataFrame: A GeoDataFrame with 'zone_id' and 'geometry' columns.
158
+ """
159
+ return self._zone_gdf.copy()
153
160
 
154
161
  def map_built_s(
155
162
  self,