geobox 1.4.1__tar.gz → 1.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {geobox-1.4.1 → geobox-1.4.2}/PKG-INFO +1 -1
  2. {geobox-1.4.1 → geobox-1.4.2}/geobox/map.py +22 -3
  3. {geobox-1.4.1 → geobox-1.4.2}/geobox/model3d.py +101 -16
  4. {geobox-1.4.1 → geobox-1.4.2}/geobox/mosaic.py +19 -0
  5. {geobox-1.4.1 → geobox-1.4.2}/geobox/query.py +3 -1
  6. {geobox-1.4.1 → geobox-1.4.2}/geobox/raster.py +19 -0
  7. {geobox-1.4.1 → geobox-1.4.2}/geobox/vectorlayer.py +19 -0
  8. {geobox-1.4.1 → geobox-1.4.2}/geobox/view.py +19 -0
  9. {geobox-1.4.1 → geobox-1.4.2}/geobox.egg-info/PKG-INFO +1 -1
  10. {geobox-1.4.1 → geobox-1.4.2}/pyproject.toml +1 -1
  11. {geobox-1.4.1 → geobox-1.4.2}/tests/test_map.py +9 -0
  12. {geobox-1.4.1 → geobox-1.4.2}/tests/test_model3d.py +152 -16
  13. {geobox-1.4.1 → geobox-1.4.2}/tests/test_mosaic.py +9 -0
  14. {geobox-1.4.1 → geobox-1.4.2}/tests/test_raster.py +9 -0
  15. {geobox-1.4.1 → geobox-1.4.2}/tests/test_vectorlayer.py +10 -0
  16. {geobox-1.4.1 → geobox-1.4.2}/tests/test_view.py +10 -0
  17. {geobox-1.4.1 → geobox-1.4.2}/LICENSE +0 -0
  18. {geobox-1.4.1 → geobox-1.4.2}/README.md +0 -0
  19. {geobox-1.4.1 → geobox-1.4.2}/geobox/__init__.py +0 -0
  20. {geobox-1.4.1 → geobox-1.4.2}/geobox/api.py +0 -0
  21. {geobox-1.4.1 → geobox-1.4.2}/geobox/apikey.py +0 -0
  22. {geobox-1.4.1 → geobox-1.4.2}/geobox/attachment.py +0 -0
  23. {geobox-1.4.1 → geobox-1.4.2}/geobox/base.py +0 -0
  24. {geobox-1.4.1 → geobox-1.4.2}/geobox/basemap.py +0 -0
  25. {geobox-1.4.1 → geobox-1.4.2}/geobox/dashboard.py +0 -0
  26. {geobox-1.4.1 → geobox-1.4.2}/geobox/enums.py +0 -0
  27. {geobox-1.4.1 → geobox-1.4.2}/geobox/exception.py +0 -0
  28. {geobox-1.4.1 → geobox-1.4.2}/geobox/feature.py +0 -0
  29. {geobox-1.4.1 → geobox-1.4.2}/geobox/field.py +0 -0
  30. {geobox-1.4.1 → geobox-1.4.2}/geobox/file.py +0 -0
  31. {geobox-1.4.1 → geobox-1.4.2}/geobox/layout.py +0 -0
  32. {geobox-1.4.1 → geobox-1.4.2}/geobox/log.py +0 -0
  33. {geobox-1.4.1 → geobox-1.4.2}/geobox/plan.py +0 -0
  34. {geobox-1.4.1 → geobox-1.4.2}/geobox/route.py +0 -0
  35. {geobox-1.4.1 → geobox-1.4.2}/geobox/scene.py +0 -0
  36. {geobox-1.4.1 → geobox-1.4.2}/geobox/settings.py +0 -0
  37. {geobox-1.4.1 → geobox-1.4.2}/geobox/task.py +0 -0
  38. {geobox-1.4.1 → geobox-1.4.2}/geobox/tile3d.py +0 -0
  39. {geobox-1.4.1 → geobox-1.4.2}/geobox/tileset.py +0 -0
  40. {geobox-1.4.1 → geobox-1.4.2}/geobox/usage.py +0 -0
  41. {geobox-1.4.1 → geobox-1.4.2}/geobox/user.py +0 -0
  42. {geobox-1.4.1 → geobox-1.4.2}/geobox/utils.py +0 -0
  43. {geobox-1.4.1 → geobox-1.4.2}/geobox/version.py +0 -0
  44. {geobox-1.4.1 → geobox-1.4.2}/geobox/workflow.py +0 -0
  45. {geobox-1.4.1 → geobox-1.4.2}/geobox.egg-info/SOURCES.txt +0 -0
  46. {geobox-1.4.1 → geobox-1.4.2}/geobox.egg-info/dependency_links.txt +0 -0
  47. {geobox-1.4.1 → geobox-1.4.2}/geobox.egg-info/requires.txt +0 -0
  48. {geobox-1.4.1 → geobox-1.4.2}/geobox.egg-info/top_level.txt +0 -0
  49. {geobox-1.4.1 → geobox-1.4.2}/setup.cfg +0 -0
  50. {geobox-1.4.1 → geobox-1.4.2}/tests/test_api.py +0 -0
  51. {geobox-1.4.1 → geobox-1.4.2}/tests/test_apikey.py +0 -0
  52. {geobox-1.4.1 → geobox-1.4.2}/tests/test_attachment.py +0 -0
  53. {geobox-1.4.1 → geobox-1.4.2}/tests/test_basemap.py +0 -0
  54. {geobox-1.4.1 → geobox-1.4.2}/tests/test_dashboard.py +0 -0
  55. {geobox-1.4.1 → geobox-1.4.2}/tests/test_feature.py +0 -0
  56. {geobox-1.4.1 → geobox-1.4.2}/tests/test_field.py +0 -0
  57. {geobox-1.4.1 → geobox-1.4.2}/tests/test_file.py +0 -0
  58. {geobox-1.4.1 → geobox-1.4.2}/tests/test_layout.py +0 -0
  59. {geobox-1.4.1 → geobox-1.4.2}/tests/test_log.py +0 -0
  60. {geobox-1.4.1 → geobox-1.4.2}/tests/test_plan.py +0 -0
  61. {geobox-1.4.1 → geobox-1.4.2}/tests/test_query.py +0 -0
  62. {geobox-1.4.1 → geobox-1.4.2}/tests/test_route.py +0 -0
  63. {geobox-1.4.1 → geobox-1.4.2}/tests/test_scene.py +0 -0
  64. {geobox-1.4.1 → geobox-1.4.2}/tests/test_settings.py +0 -0
  65. {geobox-1.4.1 → geobox-1.4.2}/tests/test_task.py +0 -0
  66. {geobox-1.4.1 → geobox-1.4.2}/tests/test_tile3d.py +0 -0
  67. {geobox-1.4.1 → geobox-1.4.2}/tests/test_tileset.py +0 -0
  68. {geobox-1.4.1 → geobox-1.4.2}/tests/test_usage.py +0 -0
  69. {geobox-1.4.1 → geobox-1.4.2}/tests/test_user.py +0 -0
  70. {geobox-1.4.1 → geobox-1.4.2}/tests/test_version.py +0 -0
  71. {geobox-1.4.1 → geobox-1.4.2}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geobox
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
4
  Summary: SDK for Geobox's APIs
5
5
  Author-email: Hamid Heydari <heydari.h62@gmail.com>
6
6
  License: MIT
@@ -574,11 +574,30 @@ class Map(Base):
574
574
  'view_settings': {},
575
575
  'toc_settings': []
576
576
  })
577
+
578
+
579
+ def update_settings(self, settings: Dict) -> Dict:
580
+ """
581
+ Update the settings
582
+
583
+ settings (Dict): settings dictionary
584
+
585
+ Returns:
586
+ Dict: updated settings
587
+
588
+ Example:
589
+ >>> from geobox import GeoboxClient
590
+ >>> client = GeoboxClient()
591
+ >>> map1 = client.get_map(uuid="12345678-1234-5678-1234-567812345678")
592
+ >>> map2 = client.get_map(uuid="12345678-1234-5678-1234-567812345678")
593
+ >>> map1.update_settings(map2.settings)
594
+ """
595
+ return super()._set_settings(self.endpoint, settings)
577
596
 
578
597
 
579
598
  def set_settings(self, **kwargs) -> Dict:
580
599
  """
581
- Set the settings of the map.
600
+ Set the settings of the map using keywords
582
601
 
583
602
  Keyword Args:
584
603
  map_unit (str): 'latlng' | 'utm'.
@@ -736,7 +755,7 @@ class Map(Base):
736
755
  if not response or json:
737
756
  return response
738
757
  else:
739
- return [Model(self.api, model['obj'], model) for model in response['objects']]
758
+ return [Model(self.api, model['obj'], model) for model in response['objects'] if response.get('objects')]
740
759
 
741
760
 
742
761
  def set_models(self, data: Dict) -> List['Model']:
@@ -771,7 +790,7 @@ class Map(Base):
771
790
  """
772
791
  endpoint = urljoin(self.endpoint, 'models/')
773
792
  response = self.api.put(endpoint, data)
774
- return [Model(self.api, model['obj'], model) for model in response['objects']]
793
+ return [Model(self.api, model['obj'], model) for model in response['objects'] if response.get('objects')]
775
794
 
776
795
 
777
796
  def add_model(self,
@@ -1,5 +1,10 @@
1
1
  from typing import Dict, List, Optional, Optional, Union, TYPE_CHECKING
2
2
  from urllib.parse import urljoin, urlencode
3
+ import os
4
+ import zipfile
5
+ import sys
6
+ import requests
7
+ import mimetypes
3
8
 
4
9
  from .base import Base
5
10
  from .exception import ApiRequestError
@@ -216,33 +221,113 @@ class Model(Base):
216
221
  super().delete(self.endpoint)
217
222
 
218
223
 
219
- def get_content(self) -> bytes:
224
+ def _get_save_path(self, save_path: str = None) -> str:
220
225
  """
221
- Get the raw content of the 3D model.
226
+ Get the path where the file should be saved.
227
+
228
+ Args:
229
+ save_path (str, optional): The path to save the file.
222
230
 
223
231
  Returns:
224
- bytes: The raw content of the 3D model in glTF format.
232
+ str: The path where the file is saved.
233
+
234
+ Raises:
235
+ ValueError: If save_path does not end with a '/'.
236
+
237
+ Example:
238
+ >>> from geobox import GeoboxClient
239
+ >>> from geobox.file import File
240
+ >>> from geobox import GeoboxClient
241
+ >>> client = GeoboxClient()
242
+ >>> file_path = File.get_file(client, uuid="12345678-1234-5678-1234-567812345678")
243
+ """
244
+ # If save_path is provided, check if it ends with a '/'
245
+ if save_path and save_path.endswith('/'):
246
+ return f'{save_path}'
247
+
248
+ if save_path and not save_path.endswith('/'):
249
+ raise ValueError("save_path must end with a '/'")
250
+
251
+ return os.getcwd()
252
+
253
+
254
+ def _create_progress_bar(self) -> 'tqdm':
255
+ """Creates a progress bar for the task."""
256
+ try:
257
+ from tqdm.auto import tqdm
258
+ except ImportError:
259
+ from .api import logger
260
+ logger.warning("[tqdm] extra is required to show the progress bar. install with: pip insatll geobox[tqdm]")
261
+ return None
262
+
263
+ return tqdm(unit="B",
264
+ total=int(self.size),
265
+ file=sys.stdout,
266
+ dynamic_ncols=True,
267
+ desc="Downloading",
268
+ unit_scale=True,
269
+ unit_divisor=1024,
270
+ ascii=True
271
+ )
272
+
273
+
274
+ def download(self, save_path: str = None, progress_bar: bool = True) -> str:
275
+ """
276
+ Download the 3D model, save it as a .glb file, zip it, and return the zip file path.
277
+
278
+ Args:
279
+ save_path (str, optional): Directory where the file should be saved.
280
+ progress_bar (bool, optional): Whether to show a progress bar. Default: True
281
+
282
+ Returns:
283
+ str: Path to the .zip file containing the .glb model.
225
284
 
226
285
  Raises:
227
286
  ApiRequestError: If the API request fails.
228
287
 
229
288
  Example:
230
289
  >>> from geobox import GeoboxClient
231
- >>> from geobox.model3d import Model
232
290
  >>> client = GeoboxClient()
233
- >>> model = Model.get_model(api=client, uuid="12345678-1234-5678-1234-567812345678")
234
- >>> content = model.get_content()
235
- >>> # Save the content to a file
236
- >>> with open('model.gltf', 'wb') as f:
237
- ... f.write(content)
291
+ >>> model = client.get_models()[0]
292
+ >>> model.get_content()
238
293
  """
239
- endpoint = urljoin(self.api.base_url, f'{self.endpoint}/content/')
240
- response = self.api.session.get(endpoint)
241
-
242
- if response.status_code != 200:
243
- raise ApiRequestError(f"Failed to get model content: {response.status_code}")
244
-
245
- return response.content
294
+ if not self.uuid:
295
+ raise ValueError("Model UUID is required to download content")
296
+
297
+ if self.data.get('obj'):
298
+ model = self.api.get_model(self.obj)
299
+ else:
300
+ model = self
301
+
302
+ save_path = model._get_save_path(save_path)
303
+ os.makedirs(save_path, exist_ok=True)
304
+
305
+ endpoint = urljoin(model.api.base_url, f"{model.endpoint}/content/")
306
+ with model.api.session.get(endpoint, stream=True) as response:
307
+ if response.status_code != 200:
308
+ raise ApiRequestError(f"Failed to get model content: {response.status_code}")
309
+
310
+ glb_filename = f"{model.name}.glb"
311
+ glb_path = os.path.join(save_path, glb_filename)
312
+
313
+ with open(glb_path, "wb") as f:
314
+ pbar = model._create_progress_bar() if progress_bar else None
315
+ for chunk in response.iter_content(chunk_size=8192):
316
+ f.write(chunk)
317
+ if pbar:
318
+ pbar.update(len(chunk))
319
+ pbar.refresh()
320
+ if pbar:
321
+ pbar.close()
322
+
323
+ zip_filename = f"{model.name}.zip"
324
+ zip_path = os.path.join(save_path, zip_filename)
325
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
326
+ zipf.write(glb_path, arcname=os.path.basename(glb_path))
327
+
328
+ os.remove(glb_path)
329
+
330
+ return os.path.abspath(zip_path)
246
331
 
247
332
 
248
333
  @property
@@ -401,6 +401,25 @@ class Mosaic(Raster):
401
401
  """
402
402
  return super().settings
403
403
 
404
+
405
+ def update_settings(self, settings: Dict) -> Dict:
406
+ """
407
+ Update the settings
408
+
409
+ settings (Dict): settings dictionary
410
+
411
+ Returns:
412
+ Dict: updated settings
413
+
414
+ Example:
415
+ >>> from geobox import GeoboxClient
416
+ >>> client = GeoboxClient()
417
+ >>> mosaic1 = client.get_mosaic(uuid="12345678-1234-5678-1234-567812345678")
418
+ >>> mosaic2 = client.get_mosaic(uuid="12345678-1234-5678-1234-567812345678")
419
+ >>> mosaic1.update_settings(mosaic2.settings)
420
+ """
421
+ return super().update_settings(settings)
422
+
404
423
 
405
424
  def set_settings(self, **kwargs) -> None:
406
425
  """
@@ -179,7 +179,7 @@ class Query(Base):
179
179
 
180
180
 
181
181
  @classmethod
182
- def create_query(cls, api: 'GeoboxClient', name: str, display_name: str = None, sql: str = None, params: List = None) -> 'Query':
182
+ def create_query(cls, api: 'GeoboxClient', name: str, display_name: str = None, description:str = None, sql: str = None, params: List = None) -> 'Query':
183
183
  """
184
184
  Creates a new query.
185
185
 
@@ -187,6 +187,7 @@ class Query(Base):
187
187
  api (Api): The GeoboxClient instance for making requests.
188
188
  name (str): The name of the query.
189
189
  display_name (str, optional): The display name of the query.
190
+ description (str, optional): The description of the query.
190
191
  sql (str, optional): The SQL statement for the query.
191
192
  params (list, optional): The parameters for the SQL statement.
192
193
 
@@ -204,6 +205,7 @@ class Query(Base):
204
205
  data = {
205
206
  "name": name,
206
207
  "display_name": display_name,
208
+ "description": description,
207
209
  "sql": sql,
208
210
  "params": params
209
211
  }
@@ -641,6 +641,25 @@ class Raster(Base):
641
641
  >>> raster.settings
642
642
  """
643
643
  return super()._get_settings(self.endpoint)
644
+
645
+
646
+ def update_settings(self, settings: Dict) -> Dict:
647
+ """
648
+ Update the settings
649
+
650
+ settings (Dict): settings dictionary
651
+
652
+ Returns:
653
+ Dict: updated settings
654
+
655
+ Example:
656
+ >>> from geobox import GeoboxClient
657
+ >>> client = GeoboxClient()
658
+ >>> raster1 = client.get_raster(uuid="12345678-1234-5678-1234-567812345678")
659
+ >>> raster2 = client.get_raster(uuid="12345678-1234-5678-1234-567812345678")
660
+ >>> raster1.update_settings(raster2.settings)
661
+ """
662
+ return super()._set_settings(self.endpoint, settings)
644
663
 
645
664
 
646
665
  def set_settings(self, **kwargs) -> None:
@@ -1062,6 +1062,25 @@ class VectorLayer(Base):
1062
1062
  >>> setting = layer.setting
1063
1063
  """
1064
1064
  return super()._get_settings(endpoint=self.endpoint)
1065
+
1066
+
1067
+ def update_settings(self, settings: Dict) -> Dict:
1068
+ """
1069
+ Update the settings
1070
+
1071
+ settings (Dict): settings dictionary
1072
+
1073
+ Returns:
1074
+ Dict: updated settings
1075
+
1076
+ Example:
1077
+ >>> from geobox import GeoboxClient
1078
+ >>> client = GeoboxClient()
1079
+ >>> layer1 = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
1080
+ >>> layer2 = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
1081
+ >>> layer1.update_settings(layer2.settings)
1082
+ """
1083
+ return super()._set_settings(self.endpoint, settings)
1065
1084
 
1066
1085
 
1067
1086
  def set_settings(self, **kwargs) -> Dict:
@@ -722,6 +722,25 @@ class VectorLayerView(VectorLayer):
722
722
  return super().settings
723
723
 
724
724
 
725
+ def update_settings(self, settings: Dict) -> Dict:
726
+ """
727
+ Update the settings
728
+
729
+ settings (Dict): settings dictionary
730
+
731
+ Returns:
732
+ Dict: updated settings
733
+
734
+ Example:
735
+ >>> from geobox import GeoboxClient
736
+ >>> client = GeoboxClient()
737
+ >>> view1 = client.get_view(uuid="12345678-1234-5678-1234-567812345678")
738
+ >>> view2 = client.get_view(uuid="12345678-1234-5678-1234-567812345678")
739
+ >>> view1.update_settings(view2.settings)
740
+ """
741
+ return super().update_settings(settings)
742
+
743
+
725
744
  def set_settings(self, **kwargs) -> Dict:
726
745
  """
727
746
  Set the settings of the Vector Layer.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geobox
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
4
  Summary: SDK for Geobox's APIs
5
5
  Author-email: Hamid Heydari <heydari.h62@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "geobox"
7
- version = "1.4.1"
7
+ version = "1.4.2"
8
8
  description = "SDK for Geobox's APIs"
9
9
  authors = [
10
10
  {name = "Hamid Heydari", email = "heydari.h62@gmail.com"}
@@ -302,6 +302,15 @@ def test_settings(api, mock_map_data):
302
302
  assert settings == mock_map_data['settings']
303
303
 
304
304
 
305
+ def test_update_settings(api, mock_map_data):
306
+ map1 = Map(api, uuid=mock_map_data['uuid'], data=mock_map_data)
307
+ map2 = Map(api, uuid=mock_map_data['uuid'], data=mock_map_data)
308
+ settings = map2.settings
309
+ api.put.return_value = settings
310
+ map1.update_settings(map2.settings)
311
+ api.put.assert_called_once_with(f'{map1.endpoint}settings/', settings)
312
+
313
+
305
314
  def test_set_settings(api, mock_map_data):
306
315
  map = Map(api, uuid=mock_map_data['uuid'], data=mock_map_data)
307
316
  settings = map.settings
@@ -131,25 +131,161 @@ def test_delete(api, mock_model_data):
131
131
  api.delete.assert_called_once_with(endpoint)
132
132
 
133
133
 
134
- def test_get_content(api, mock_model_data):
134
+ import os
135
+ import pytest
136
+ from unittest.mock import MagicMock, patch
137
+ from geobox.file import File
138
+
139
+
140
+ def test_get_save_path_variants(api, mock_model_data, tmp_path):
141
+ """Test _get_save_path edge cases."""
135
142
  model = Model(api, uuid=mock_model_data['uuid'], data=mock_model_data)
136
-
143
+
144
+ # Case: no save_path -> returns cwd
145
+ result = model._get_save_path()
146
+ assert result == os.getcwd()
147
+
148
+ # Case: save_path ends with '/' -> returns unchanged
149
+ path = str(tmp_path) + "/"
150
+ assert model._get_save_path(save_path=path) == path
151
+
152
+ # Case: save_path does NOT end with '/' -> raises ValueError
153
+ path_invalid = str(tmp_path)
154
+ with pytest.raises(ValueError, match="save_path must end with a '/'"):
155
+ model._get_save_path(save_path=path_invalid)
156
+
157
+
158
+ def test_create_progress_bar_import_error(api, mock_model_data):
159
+ """Test _create_progress_bar when tqdm is missing."""
160
+ model = Model(api, uuid=mock_model_data['uuid'], data=mock_model_data)
161
+ model.size = 1234
162
+
163
+ import builtins
164
+ original_import = builtins.__import__
165
+
166
+ def mocked_import(name, *args, **kwargs):
167
+ if name == "tqdm.auto":
168
+ raise ImportError("No module named 'tqdm'")
169
+ return original_import(name, *args, **kwargs)
170
+
171
+ with patch("builtins.__import__", side_effect=mocked_import):
172
+ with patch("geobox.api.logger") as mock_logger:
173
+ pbar = model._create_progress_bar()
174
+ assert pbar is None
175
+ mock_logger.warning.assert_called_once()
176
+
177
+
178
+ def test_download_with_obj(api, mock_model_data, tmp_path):
179
+ """Test download() when self.data['obj'] exists."""
180
+ # Make mock data have 'obj'
181
+ mock_model_data['obj'] = "object-id"
182
+ model = Model(api, uuid=mock_model_data['uuid'], data=mock_model_data)
183
+ model.name = "test_model"
184
+ model.endpoint = "models"
185
+
186
+ # Mock api.get_model() to return a model with a normal download
187
+ downloaded_model = MagicMock()
188
+ downloaded_model.api = api
189
+ downloaded_model.name = model.name
190
+ downloaded_model.endpoint = model.endpoint
191
+ downloaded_model.data = {}
192
+ downloaded_model._get_save_path = lambda sp: str(tmp_path) + "/"
193
+ downloaded_model._create_progress_bar = lambda: MagicMock()
194
+
195
+ api.get_model.return_value = downloaded_model
196
+
197
+ # Mock API response
137
198
  mock_response = MagicMock()
138
- mock_response.status_code = 200
139
- mock_response.content = b'test content'
140
-
141
- api.session = MagicMock()
142
- api.session.get.return_value = mock_response
143
-
144
- content = model.get_content()
145
-
146
- api.session.get.assert_called_once_with(f"{model.api.base_url}{model.endpoint}content/")
147
-
148
- assert content == b'test content'
199
+ mock_response.__enter__.return_value.iter_content.return_value = [b"chunk"]
200
+ mock_response.__enter__.return_value.status_code = 200
201
+ mock_response.__exit__.return_value = None
202
+
203
+ with patch.object(api.session, "get", return_value=mock_response), \
204
+ patch("builtins.open", MagicMock()), \
205
+ patch("os.makedirs", return_value=None), \
206
+ patch("os.remove", return_value=None), \
207
+ patch("zipfile.ZipFile", MagicMock()):
208
+ zip_path = model.download()
209
+ assert zip_path.endswith(".zip")
210
+
211
+
212
+ def test_download_non_200(api, mock_model_data, tmp_path):
213
+ """Test download() raises ApiRequestError on bad status code."""
214
+ model = Model(api, uuid=mock_model_data['uuid'], data=mock_model_data)
215
+ model.name = "test_model"
216
+ model.endpoint = "models"
217
+
218
+ mock_response = MagicMock()
219
+ mock_response.__enter__.return_value.status_code = 404
220
+ mock_response.__enter__.return_value.iter_content.return_value = []
221
+ mock_response.__exit__.return_value = None
222
+
223
+ with patch.object(api.session, "get", return_value=mock_response), \
224
+ patch("os.makedirs", return_value=None):
225
+ from geobox.api import ApiRequestError
226
+ with pytest.raises(ApiRequestError, match="Failed to get model content: 404"):
227
+ model.download(save_path=str(tmp_path)+"/")
228
+
229
+
230
+ def test_download_missing_uuid(api, mock_model_data):
231
+ """Raise ValueError if UUID is missing."""
232
+ model = Model(api, uuid=None, data=mock_model_data) # uuid is None
233
+ model.name = "test_model"
234
+ model.endpoint = "models"
235
+
236
+ from geobox.api import ApiRequestError
149
237
 
150
- mock_response.status_code = 404
151
- with pytest.raises(ApiRequestError):
152
- content = model.get_content()
238
+ with pytest.raises(ValueError, match="Model UUID is required to download content"):
239
+ model.download()
240
+
241
+
242
+ def test_create_progress_bar_normal(api, mock_model_data):
243
+ """Test _create_progress_bar returns tqdm instance when tqdm is available."""
244
+ from tqdm.auto import tqdm
245
+
246
+ model = Model(api, uuid=mock_model_data['uuid'], data=mock_model_data)
247
+ model.size = 1024 # required for total=int(self.size)
248
+
249
+ pbar = model._create_progress_bar()
250
+ assert isinstance(pbar, tqdm)
251
+ assert pbar.total == 1024
252
+
253
+
254
+ # def test_download_method_simple(api, mock_model_data, tmp_path):
255
+ # """Test File.download() method without manually setting headers."""
256
+ # model = Model(api, uuid=mock_model_data['uuid'], data=mock_model_data)
257
+ # model.name = "test_model" # ensure a name for filenames
258
+ # model.endpoint = "models"
259
+
260
+ # # Mock API response
261
+ # mock_response = MagicMock()
262
+ # mock_response.__enter__.return_value.iter_content.return_value = [b"chunk1", b"chunk2"]
263
+ # mock_response.__enter__.return_value.status_code = 200
264
+ # mock_response.__exit__.return_value = None
265
+
266
+ # # Patch all filesystem and network operations
267
+ # with patch.object(api.session, "get", return_value=mock_response), \
268
+ # patch("builtins.open", new_callable=MagicMock), \
269
+ # patch("os.makedirs", return_value=None), \
270
+ # patch("os.remove", return_value=None), \
271
+ # patch("zipfile.ZipFile") as mock_zip, \
272
+ # patch.object(model, "_create_progress_bar", return_value=MagicMock()) as mock_pbar:
273
+
274
+ # # Save to custom tmp_path
275
+ # save_dir = str(tmp_path) + "/"
276
+ # zip_path = model.download(save_path=save_dir)
277
+
278
+ # # Assertions
279
+ # assert zip_path.endswith(".zip")
280
+ # assert os.path.commonpath([zip_path, save_dir]) == os.path.normpath(save_dir)
281
+ # assert os.path.basename(zip_path) == "test_model.zip"
282
+ # mock_zip.assert_called_once() # ensure zipfile was called
283
+ # mock_pbar.return_value.update.assert_called() # ensure progress bar updated
284
+
285
+ # # Test that ValueError is raised if uuid is missing
286
+ # model.uuid = None
287
+ # with pytest.raises(ValueError, match="Model UUID is required to download content"):
288
+ # model.download(save_path=save_dir)
153
289
 
154
290
 
155
291
  def test_thumbnail(api, mock_model_data):
@@ -184,6 +184,15 @@ def test_settings(api, mock_mosaic_data):
184
184
  assert settings == {'value': 'test'}
185
185
 
186
186
 
187
+ def test_update_settings(api, mock_mosaic_data):
188
+ mosaic1 = Mosaic(api, uuid=mock_mosaic_data['uuid'], data=mock_mosaic_data)
189
+ mosaic2 = Mosaic(api, uuid=mock_mosaic_data['uuid'], data=mock_mosaic_data)
190
+ settings = mosaic2.settings
191
+ api.put.return_value = settings
192
+ mosaic1.update_settings(mosaic2.settings)
193
+ api.put.assert_called_once_with(f'{mosaic1.endpoint}settings/', settings)
194
+
195
+
187
196
  def test_set_settings(api, mock_mosaic_data):
188
197
  """Test the set_settings method."""
189
198
  mosaic = Mosaic(api, mock_mosaic_data['uuid'], mock_mosaic_data)
@@ -310,6 +310,15 @@ def test_settings(api, mock_raster_data):
310
310
  api.get.assert_called_once_with(f'{raster.endpoint}settings/?f=json')
311
311
 
312
312
 
313
+ def test_update_settings(api, mock_raster_data):
314
+ raster1 = Raster(api, uuid=mock_raster_data['uuid'], data=mock_raster_data)
315
+ raster2 = Raster(api, uuid=mock_raster_data['uuid'], data=mock_raster_data)
316
+ settings = raster2.settings
317
+ api.put.return_value = settings
318
+ raster1.update_settings(raster2.settings)
319
+ api.put.assert_called_once_with(f'{raster1.endpoint}settings/', settings)
320
+
321
+
313
322
  def test_set_settings(api, mock_raster_data):
314
323
  """Test setting raster settings."""
315
324
  raster = Raster(api, uuid=mock_raster_data['uuid'], data=mock_raster_data)
@@ -625,6 +625,16 @@ def test_settings(api, mock_vector_data, layer_type):
625
625
  assert settings == {'settings': 'value'}
626
626
 
627
627
 
628
+ @pytest.mark.parametrize("layer_type", [type for type in LayerType])
629
+ def test_update_settings(api, mock_vector_data, layer_type):
630
+ layer1 = VectorLayer(api, uuid=mock_vector_data['uuid'], data=mock_vector_data, layer_type=layer_type)
631
+ layer2 = VectorLayer(api, uuid=mock_vector_data['uuid'], data=mock_vector_data, layer_type=layer_type)
632
+ settings = layer2.settings
633
+ api.put.return_value = settings
634
+ layer1.update_settings(layer2.settings)
635
+ api.put.assert_called_once_with(f'{layer1.endpoint}settings/', settings)
636
+
637
+
628
638
  @pytest.mark.parametrize("layer_type", [type for type in LayerType])
629
639
  def test_set_settings(api, mock_vector_data, layer_type):
630
640
  """Test setting the settings of a layer."""
@@ -563,6 +563,16 @@ def test_settings(api, mock_view_data, layer_type):
563
563
  assert settings == {'settings': 'value'}
564
564
 
565
565
 
566
+ @pytest.mark.parametrize("layer_type", [type for type in LayerType])
567
+ def test_update_settings(api, mock_view_data, layer_type):
568
+ view1 = VectorLayerView(api, uuid=mock_view_data['uuid'], data=mock_view_data, layer_type=layer_type)
569
+ view2 = VectorLayerView(api, uuid=mock_view_data['uuid'], data=mock_view_data, layer_type=layer_type)
570
+ settings = view2.settings
571
+ api.put.return_value = settings
572
+ view1.update_settings(view2.settings)
573
+ api.put.assert_called_once_with(f'{view1.endpoint}settings/', settings)
574
+
575
+
566
576
  @pytest.mark.parametrize("layer_type", [type for type in LayerType])
567
577
  def test_set_settings(api, mock_view_data, layer_type):
568
578
  """Test setting the settings of a layer."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes