giga-spatial 0.6.9__py3-none-any.whl → 0.7.1__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.9.dist-info → giga_spatial-0.7.1.dist-info}/METADATA +30 -4
- {giga_spatial-0.6.9.dist-info → giga_spatial-0.7.1.dist-info}/RECORD +22 -20
- gigaspatial/__init__.py +1 -1
- gigaspatial/config.py +1 -0
- gigaspatial/core/io/adls_data_store.py +104 -11
- gigaspatial/core/io/local_data_store.py +8 -0
- gigaspatial/generators/poi.py +226 -82
- gigaspatial/generators/zonal/base.py +41 -28
- gigaspatial/generators/zonal/geometry.py +91 -41
- gigaspatial/grid/h3.py +417 -0
- gigaspatial/grid/mercator_tiles.py +1 -1
- gigaspatial/handlers/base.py +22 -8
- gigaspatial/handlers/ghsl.py +22 -8
- gigaspatial/handlers/giga.py +9 -4
- gigaspatial/handlers/healthsites.py +350 -0
- gigaspatial/handlers/osm.py +325 -105
- gigaspatial/handlers/worldpop.py +228 -9
- gigaspatial/processing/geo.py +11 -6
- gigaspatial/processing/tif_processor.py +1183 -496
- {giga_spatial-0.6.9.dist-info → giga_spatial-0.7.1.dist-info}/WHEEL +0 -0
- {giga_spatial-0.6.9.dist-info → giga_spatial-0.7.1.dist-info}/licenses/LICENSE +0 -0
- {giga_spatial-0.6.9.dist-info → giga_spatial-0.7.1.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,29 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: giga-spatial
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.1
|
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
|
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
|
+
[](https://opensource.org/license/agpl-v3)
|
75
|
+
[](https://badge.fury.io/py/giga-spatial)
|
76
|
+
[](https://pypi.org/project/giga-spatial/)
|
77
|
+
[](https://github.com/psf/black)
|
78
|
+
[](https://pepy.tech/projects/giga-spatial)
|
79
|
+
[](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,50 +1,52 @@
|
|
1
|
-
giga_spatial-0.
|
2
|
-
gigaspatial/__init__.py,sha256=
|
3
|
-
gigaspatial/config.py,sha256=
|
1
|
+
giga_spatial-0.7.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
2
|
+
gigaspatial/__init__.py,sha256=2KJZDSMOG7KS82AxYOrZ4ZihYxX0wjfUjDsIZh3L024,22
|
3
|
+
gigaspatial/config.py,sha256=xqP800jaDMYE7-cgHXSGGwuF1fmo5Q56-DqpJ61p8a0,9382
|
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=
|
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=
|
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
|
14
14
|
gigaspatial/core/schemas/entity.py,sha256=QAhEW0-JgdWh9pjKGbB5ArvqtVK85ayYZJPgjdb-IKw,8590
|
15
15
|
gigaspatial/generators/__init__.py,sha256=cKbMTW7Eh-oDPtM9OfGP14_ekVwc2_7Je7n_pr_anig,223
|
16
|
-
gigaspatial/generators/poi.py,sha256=
|
16
|
+
gigaspatial/generators/poi.py,sha256=tlLqVsXOjl83OL3Z6yajz9BLtZVbhZF67A-el-F2oU4,42356
|
17
17
|
gigaspatial/generators/zonal/__init__.py,sha256=egnpvGVeIOS2Zg516AT84tJnIqS4owxmMLLmBQJmK7Y,301
|
18
18
|
gigaspatial/generators/zonal/admin.py,sha256=rgOyQX3f_g9qnXqrf-NkR2GEdwOqjNuPNe1H7AUVsfg,3698
|
19
|
-
gigaspatial/generators/zonal/base.py,sha256=
|
20
|
-
gigaspatial/generators/zonal/geometry.py,sha256=
|
19
|
+
gigaspatial/generators/zonal/base.py,sha256=byyfv4dNpOMzszBMMbKQlZuHMlq2e84Hboh0W58f-8k,22959
|
20
|
+
gigaspatial/generators/zonal/geometry.py,sha256=0EF3VXAVOcj5AtcubGWU325biLaZ76zt3Sf1uyKEE88,23358
|
21
21
|
gigaspatial/generators/zonal/mercator.py,sha256=fA02j30PWB5BVjrbNGCMjiOw-ds182yK7R27z8mWFug,5291
|
22
22
|
gigaspatial/grid/__init__.py,sha256=ypSSyZ4fYtMNc4IG7chSD7NkUfS2bv9KWRsKR1D9pDI,80
|
23
|
-
gigaspatial/grid/
|
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=
|
26
|
+
gigaspatial/handlers/base.py,sha256=JSZ64eWRoF17bKvVj2Bq3lFA00uJ4i6qlwp72O4HSv4,27885
|
26
27
|
gigaspatial/handlers/boundaries.py,sha256=jtWyQt3iAzS77mbAOi7mjh3cv_YCV3uB_r1h56gCfeY,20729
|
27
|
-
gigaspatial/handlers/ghsl.py,sha256=
|
28
|
-
gigaspatial/handlers/giga.py,sha256=
|
28
|
+
gigaspatial/handlers/ghsl.py,sha256=QvW7GeHWQSrPdxXxmR_61UcfBIHziNpCHCxRXg1nnug,31285
|
29
|
+
gigaspatial/handlers/giga.py,sha256=tNy2O0YAdy4iYWjMNKUadV_W2HxcTsb-aG4te6wvMOk,28660
|
29
30
|
gigaspatial/handlers/google_open_buildings.py,sha256=Liqk7qJhDtB4Ia4uhBe44LFcf-XVKBjRfj-pWlE5erY,16594
|
30
31
|
gigaspatial/handlers/hdx.py,sha256=1m6oG1DeEC_RLFtb6CrTReWpbQ5uG2e8EIt-IUkZbaI,18122
|
32
|
+
gigaspatial/handlers/healthsites.py,sha256=gJCnKDXE4Gu-3Z6Io0-EknXfSdOxBHdr4-JV0z9bul0,12829
|
31
33
|
gigaspatial/handlers/mapbox_image.py,sha256=M_nkJ_b1PD8FG1ajVgSycCb0NRTAI_SLpHdzszNetKA,7786
|
32
34
|
gigaspatial/handlers/maxar_image.py,sha256=kcc8uGljQB0Yh0MKBA7lT7KwBbNZwFzuyBklR3db1P4,10204
|
33
35
|
gigaspatial/handlers/microsoft_global_buildings.py,sha256=bQ5WHIv3v0wWrZZUbZkKPRjgdlqIxlK7CV_0zSvdrTw,20292
|
34
36
|
gigaspatial/handlers/ookla_speedtest.py,sha256=EcvSAxJZ9GPfzYnT_C85Qgy2ecc9ndf70Pklk53OdC8,6506
|
35
37
|
gigaspatial/handlers/opencellid.py,sha256=KuJqd-5-RO5ZzyDaBSrTgCK2ib5N_m3RUcPlX5heWwI,10683
|
36
|
-
gigaspatial/handlers/osm.py,sha256=
|
38
|
+
gigaspatial/handlers/osm.py,sha256=gyY_a2OoRZjL14iOTEKLCPbKTcmSDHc72Q0hluaENrY,25464
|
37
39
|
gigaspatial/handlers/overture.py,sha256=lKeNw00v5Qia7LdWORuYihnlKEqxE9m38tdeRrvag9k,4218
|
38
40
|
gigaspatial/handlers/rwi.py,sha256=eAaplDysVeBhghJusYUKZYbKL5hW-klWvi8pWhILQkY,4962
|
39
41
|
gigaspatial/handlers/unicef_georepo.py,sha256=ODYNvkU_UKgOHXT--0MqmJ4Uk6U1_mp9xgehbTzKpX8,31924
|
40
|
-
gigaspatial/handlers/worldpop.py,sha256=
|
42
|
+
gigaspatial/handlers/worldpop.py,sha256=5gSAhTPs3l8cVflx_Vp4ZxhvcUKKb_r2hGmc_sXCgio,39893
|
41
43
|
gigaspatial/processing/__init__.py,sha256=QDVL-QbLCrIb19lrajP7LrHNdGdnsLeGcvAs_jQpdRM,183
|
42
44
|
gigaspatial/processing/algorithms.py,sha256=6fBCwbZrI_ISWJ7UpkH6moq1vw-7dBy14yXSLHZprqY,6591
|
43
|
-
gigaspatial/processing/geo.py,sha256=
|
45
|
+
gigaspatial/processing/geo.py,sha256=ekGbU1sEn10zX3esYIEba6eZyhpx9WOjPlVEkspqcNk,41674
|
44
46
|
gigaspatial/processing/sat_images.py,sha256=YUbH5MFNzl6NX49Obk14WaFcr1s3SyGJIOk-kRpbBNg,1429
|
45
|
-
gigaspatial/processing/tif_processor.py,sha256=
|
47
|
+
gigaspatial/processing/tif_processor.py,sha256=WzD3inLUdr50fl11WPF6czvG1d8s7y2qdTBOc5Yo2yw,66400
|
46
48
|
gigaspatial/processing/utils.py,sha256=HC85vGKQakxlkoQAkZmeAXWHsenAwTIRn7jPKUA7x20,1500
|
47
|
-
giga_spatial-0.
|
48
|
-
giga_spatial-0.
|
49
|
-
giga_spatial-0.
|
50
|
-
giga_spatial-0.
|
49
|
+
giga_spatial-0.7.1.dist-info/METADATA,sha256=Fo_X_aPJg9Mv1GfX4esviiljEhcK4n7imbACWKrNo6s,9277
|
50
|
+
giga_spatial-0.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
51
|
+
giga_spatial-0.7.1.dist-info/top_level.txt,sha256=LZsccgw6H4zXT7m6Y4XChm-Y5LjHAwZ2hkGN_B3ExmI,12
|
52
|
+
giga_spatial-0.7.1.dist-info/RECORD,,
|
gigaspatial/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.7.1"
|
gigaspatial/config.py
CHANGED
@@ -38,6 +38,7 @@ class Config(BaseSettings):
|
|
38
38
|
GIGA_SCHOOL_MEASUREMENTS_API_KEY: str = Field(
|
39
39
|
default="", alias="GIGA_SCHOOL_MEASUREMENTS_API_KEY"
|
40
40
|
)
|
41
|
+
HEALTHSITES_API_KEY: str = Field(default="", alias="HEALTHSITES_API_KEY")
|
41
42
|
|
42
43
|
ROOT_DATA_DIR: Path = Field(
|
43
44
|
default=Path("."),
|
@@ -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
|
-
#
|
155
|
-
|
155
|
+
# Use copy_file method to copy each file
|
156
|
+
self.copy_file(blob.name, new_blob_path, overwrite=True)
|
156
157
|
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
163
|
-
|
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
|
-
|
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
|
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
|
-
|
289
|
-
|
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():
|