geobox 1.3.0__tar.gz → 1.3.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 (69) hide show
  1. {geobox-1.3.0 → geobox-1.3.2}/PKG-INFO +1 -1
  2. {geobox-1.3.0 → geobox-1.3.2}/geobox/mosaic.py +11 -2
  3. {geobox-1.3.0 → geobox-1.3.2}/geobox/query.py +1 -1
  4. {geobox-1.3.0 → geobox-1.3.2}/geobox/raster.py +107 -45
  5. {geobox-1.3.0 → geobox-1.3.2}/geobox/user.py +4 -7
  6. {geobox-1.3.0 → geobox-1.3.2}/geobox/utils.py +7 -0
  7. {geobox-1.3.0 → geobox-1.3.2}/geobox/vectorlayer.py +30 -0
  8. {geobox-1.3.0 → geobox-1.3.2}/geobox.egg-info/PKG-INFO +1 -1
  9. {geobox-1.3.0 → geobox-1.3.2}/pyproject.toml +1 -1
  10. {geobox-1.3.0 → geobox-1.3.2}/tests/test_mosaic.py +2 -2
  11. {geobox-1.3.0 → geobox-1.3.2}/tests/test_raster.py +25 -9
  12. {geobox-1.3.0 → geobox-1.3.2}/tests/test_user.py +4 -3
  13. {geobox-1.3.0 → geobox-1.3.2}/tests/test_vectorlayer.py +11 -0
  14. {geobox-1.3.0 → geobox-1.3.2}/LICENSE +0 -0
  15. {geobox-1.3.0 → geobox-1.3.2}/README.md +0 -0
  16. {geobox-1.3.0 → geobox-1.3.2}/geobox/__init__.py +0 -0
  17. {geobox-1.3.0 → geobox-1.3.2}/geobox/api.py +0 -0
  18. {geobox-1.3.0 → geobox-1.3.2}/geobox/apikey.py +0 -0
  19. {geobox-1.3.0 → geobox-1.3.2}/geobox/attachment.py +0 -0
  20. {geobox-1.3.0 → geobox-1.3.2}/geobox/base.py +0 -0
  21. {geobox-1.3.0 → geobox-1.3.2}/geobox/basemap.py +0 -0
  22. {geobox-1.3.0 → geobox-1.3.2}/geobox/dashboard.py +0 -0
  23. {geobox-1.3.0 → geobox-1.3.2}/geobox/enums.py +0 -0
  24. {geobox-1.3.0 → geobox-1.3.2}/geobox/exception.py +0 -0
  25. {geobox-1.3.0 → geobox-1.3.2}/geobox/feature.py +0 -0
  26. {geobox-1.3.0 → geobox-1.3.2}/geobox/field.py +0 -0
  27. {geobox-1.3.0 → geobox-1.3.2}/geobox/file.py +0 -0
  28. {geobox-1.3.0 → geobox-1.3.2}/geobox/log.py +0 -0
  29. {geobox-1.3.0 → geobox-1.3.2}/geobox/map.py +0 -0
  30. {geobox-1.3.0 → geobox-1.3.2}/geobox/model3d.py +0 -0
  31. {geobox-1.3.0 → geobox-1.3.2}/geobox/plan.py +0 -0
  32. {geobox-1.3.0 → geobox-1.3.2}/geobox/route.py +0 -0
  33. {geobox-1.3.0 → geobox-1.3.2}/geobox/scene.py +0 -0
  34. {geobox-1.3.0 → geobox-1.3.2}/geobox/settings.py +0 -0
  35. {geobox-1.3.0 → geobox-1.3.2}/geobox/task.py +0 -0
  36. {geobox-1.3.0 → geobox-1.3.2}/geobox/tile3d.py +0 -0
  37. {geobox-1.3.0 → geobox-1.3.2}/geobox/tileset.py +0 -0
  38. {geobox-1.3.0 → geobox-1.3.2}/geobox/usage.py +0 -0
  39. {geobox-1.3.0 → geobox-1.3.2}/geobox/version.py +0 -0
  40. {geobox-1.3.0 → geobox-1.3.2}/geobox/view.py +0 -0
  41. {geobox-1.3.0 → geobox-1.3.2}/geobox/workflow.py +0 -0
  42. {geobox-1.3.0 → geobox-1.3.2}/geobox.egg-info/SOURCES.txt +0 -0
  43. {geobox-1.3.0 → geobox-1.3.2}/geobox.egg-info/dependency_links.txt +0 -0
  44. {geobox-1.3.0 → geobox-1.3.2}/geobox.egg-info/requires.txt +0 -0
  45. {geobox-1.3.0 → geobox-1.3.2}/geobox.egg-info/top_level.txt +0 -0
  46. {geobox-1.3.0 → geobox-1.3.2}/setup.cfg +0 -0
  47. {geobox-1.3.0 → geobox-1.3.2}/tests/test_api.py +0 -0
  48. {geobox-1.3.0 → geobox-1.3.2}/tests/test_apikey.py +0 -0
  49. {geobox-1.3.0 → geobox-1.3.2}/tests/test_attachment.py +0 -0
  50. {geobox-1.3.0 → geobox-1.3.2}/tests/test_basemap.py +0 -0
  51. {geobox-1.3.0 → geobox-1.3.2}/tests/test_dashboard.py +0 -0
  52. {geobox-1.3.0 → geobox-1.3.2}/tests/test_feature.py +0 -0
  53. {geobox-1.3.0 → geobox-1.3.2}/tests/test_field.py +0 -0
  54. {geobox-1.3.0 → geobox-1.3.2}/tests/test_file.py +0 -0
  55. {geobox-1.3.0 → geobox-1.3.2}/tests/test_log.py +0 -0
  56. {geobox-1.3.0 → geobox-1.3.2}/tests/test_map.py +0 -0
  57. {geobox-1.3.0 → geobox-1.3.2}/tests/test_model3d.py +0 -0
  58. {geobox-1.3.0 → geobox-1.3.2}/tests/test_plan.py +0 -0
  59. {geobox-1.3.0 → geobox-1.3.2}/tests/test_query.py +0 -0
  60. {geobox-1.3.0 → geobox-1.3.2}/tests/test_route.py +0 -0
  61. {geobox-1.3.0 → geobox-1.3.2}/tests/test_scene.py +0 -0
  62. {geobox-1.3.0 → geobox-1.3.2}/tests/test_settings.py +0 -0
  63. {geobox-1.3.0 → geobox-1.3.2}/tests/test_task.py +0 -0
  64. {geobox-1.3.0 → geobox-1.3.2}/tests/test_tile3d.py +0 -0
  65. {geobox-1.3.0 → geobox-1.3.2}/tests/test_tileset.py +0 -0
  66. {geobox-1.3.0 → geobox-1.3.2}/tests/test_usage.py +0 -0
  67. {geobox-1.3.0 → geobox-1.3.2}/tests/test_version.py +0 -0
  68. {geobox-1.3.0 → geobox-1.3.2}/tests/test_view.py +0 -0
  69. {geobox-1.3.0 → geobox-1.3.2}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geobox
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: SDK for Geobox's APIs
5
5
  Author-email: Hamid Heydari <heydari.h62@gmail.com>
6
6
  License: MIT
@@ -295,7 +295,7 @@ class Mosaic(Raster):
295
295
  return super().get_point(lat, lng)
296
296
 
297
297
 
298
- def get_tile_render_url(self, x: int, y: int, z: int) -> str:
298
+ def get_render_png_url(self, x: int, y: int, z: int, **kwargs) -> str:
299
299
  """
300
300
  Get the tile render URL of the mosaic.
301
301
 
@@ -304,6 +304,15 @@ class Mosaic(Raster):
304
304
  y (int): The y coordinate of the tile.
305
305
  z (int): The zoom level of the tile.
306
306
 
307
+ Keyword Args:
308
+ indexes (str, optional): list of comma separated band indexes to be rendered. e.g. 1, 2, 3
309
+ nodata (int, optional)
310
+ expression (str, optional): band math expression. e.g. b1*b2+b3
311
+ rescale (List, optional): comma (',') separated Min,Max range. Can set multiple time for multiple bands.
312
+ color_formula (str, optional): Color formula. e.g. gamma R 0.5
313
+ colormap_name (str, optional)
314
+ colormap (str, optional): JSON encoded custom Colormap. e.g. {"0": "#ff0000", "1": "#00ff00"} or [[[0, 100], "#ff0000"], [[100, 200], "#00ff00"]]
315
+
307
316
  Returns:
308
317
  str: The tile render URL of the mosaic.
309
318
 
@@ -314,7 +323,7 @@ class Mosaic(Raster):
314
323
  >>> mosaic = Mosaic.get_mosaic(client, uuid="12345678-1234-5678-1234-567812345678")
315
324
  >>> mosaic.get_tile_render_url(x=1, y=1, z=1)
316
325
  """
317
- return super().get_tile_render_url(x, y, z)
326
+ return super().get_render_png_url(x, y, z, **kwargs)
318
327
 
319
328
 
320
329
  def get_tile_png_url(self, x: int, y: int, z: int) -> str:
@@ -582,7 +582,7 @@ class Query(Base):
582
582
  super()._unshare(self.endpoint, users)
583
583
 
584
584
 
585
- def get_shared_users(self, search: str, skip: int = 0, limit: int = 10) -> List['User']:
585
+ def get_shared_users(self, search: str = None, skip: int = 0, limit: int = 10) -> List['User']:
586
586
  """
587
587
  Retrieves the list of users the query is shared with.
588
588
 
@@ -3,6 +3,7 @@ from urllib.parse import urljoin, urlencode
3
3
  from typing import Optional, Dict, List, Optional, Union, TYPE_CHECKING
4
4
  import mimetypes
5
5
  import requests
6
+ import sys
6
7
 
7
8
  from .base import Base
8
9
  from .utils import clean_data
@@ -179,7 +180,6 @@ class Raster(Base):
179
180
  name (str): The name of the raster.
180
181
  display_name (str): The display name of the raster.
181
182
  description (str): The description of the raster.
182
- max_zoom (int): The max zoom of the raster.
183
183
 
184
184
  Returns:
185
185
  None
@@ -194,10 +194,9 @@ class Raster(Base):
194
194
  params = {
195
195
  'name': kwargs.get('name'),
196
196
  'display_name': kwargs.get('display_name'),
197
- 'description': kwargs.get('description'),
198
- 'max_zoom': kwargs.get('max_zoom'),
197
+ 'description': kwargs.get('description')
199
198
  }
200
- super()._update(self.endpoint, params)
199
+ return super()._update(self.endpoint, params)
201
200
 
202
201
 
203
202
  def delete(self) -> None:
@@ -236,7 +235,8 @@ class Raster(Base):
236
235
  return endpoint
237
236
 
238
237
 
239
- def get_raster_info(self) -> Dict:
238
+ @property
239
+ def info(self) -> Dict:
240
240
  """
241
241
  Get the info of the raster.
242
242
 
@@ -248,7 +248,7 @@ class Raster(Base):
248
248
  >>> from geobox.raster import Raster
249
249
  >>> client = GeoboxClient()
250
250
  >>> raster = Raster.get_raster(client, uuid="12345678-1234-5678-1234-567812345678")
251
- >>> raster.get_raster_info()
251
+ >>> raster.info
252
252
  """
253
253
  endpoint = urljoin(self.endpoint, 'info/')
254
254
  return self.api.get(endpoint)
@@ -323,53 +323,74 @@ class Raster(Base):
323
323
 
324
324
  Raises:
325
325
  ValueError: If save_path does not end with a '/'.
326
- """
327
- # Get the original filename from data or use uuid
328
- if self.name:
329
- filename = f"{self.name.split('.')[0]}" if len(self.name.split('.')) > 1 else f'{self.name}'
330
- else:
331
- filename = f'{self.uuid}'
332
326
 
327
+ Example:
328
+ >>> from geobox import GeoboxClient
329
+ >>> from geobox.file import File
330
+ >>> from geobox import GeoboxClient
331
+ >>> client = GeoboxClient()
332
+ >>> file_path = File.get_file(client, uuid="12345678-1234-5678-1234-567812345678")
333
+ """
333
334
  # If save_path is provided, check if it ends with a '/'
334
335
  if save_path and save_path.endswith('/'):
335
- return f'{save_path}{filename}'
336
+ return f'{save_path}'
336
337
 
337
338
  if save_path and not save_path.endswith('/'):
338
339
  raise ValueError("save_path must end with a '/'")
339
340
 
340
- return os.path.join(os.getcwd(), filename)
341
+ return os.getcwd()
341
342
 
342
343
 
343
- def _get_file_ext(self, response: requests.Response) -> str:
344
+ def _get_file_name(self, response: requests.Response) -> str:
344
345
  """
345
- Get the file extension of the response.
346
+ Get the file name from the response.
346
347
 
347
348
  Args:
348
349
  response (requests.Response): The response of the request.
349
350
 
350
351
  Returns:
351
- str: The file extension of the response.
352
+ str: The file name
352
353
  """
353
- ext = ""
354
354
  if 'Content-Disposition' in response.headers and 'filename=' in response.headers['Content-Disposition']:
355
- content_disposition = response.headers['Content-Disposition']
356
- filename = content_disposition.split('filename=')[-1].strip().strip('"')
357
- ext = f".{filename.split('.')[-1]}"
355
+ file_name = response.headers['Content-Disposition'].split('filename=')[-1].strip().strip('"')
358
356
 
359
357
  else:
360
358
  content_type = response.headers.get("Content-Type", "")
361
- ext = mimetypes.guess_extension(content_type.split(";")[0])
359
+ file_name = f'{self.name}.{mimetypes.guess_extension(content_type.split(";")[0])}'
360
+
361
+ return file_name
362
+
363
+
364
+ def _create_progress_bar(self) -> 'tqdm':
365
+ """Creates a progress bar for the task."""
366
+ try:
367
+ from tqdm.auto import tqdm
368
+ except ImportError:
369
+ from .api import logger
370
+ logger.warning("[tqdm] extra is required to show the progress bar. install with: pip insatll geobox[tqdm]")
371
+ return None
362
372
 
363
- return ext
373
+ return tqdm(unit="B",
374
+ total=int(self.size),
375
+ file=sys.stdout,
376
+ dynamic_ncols=True,
377
+ desc="Downloading",
378
+ unit_scale=True,
379
+ unit_divisor=1024,
380
+ ascii=True
381
+ )
364
382
 
365
383
 
366
- def download(self, save_path: str = None) -> str:
384
+ def download(self, save_path: str = None, progress_bar: bool = True) -> str:
367
385
  """
368
386
  Download the raster.
369
387
 
370
388
  Args:
371
- save_path (str): The path to save the raster.
372
-
389
+ save_path (str, optional): Path where the file should be saved.
390
+ If not provided, it saves to the current working directory
391
+ using the original filename and appropriate extension.
392
+ progress_bar (bool, optional): Whether to show a progress bar. default: True
393
+
373
394
  Returns:
374
395
  str: The path to save the raster.
375
396
 
@@ -390,23 +411,32 @@ class Raster(Base):
390
411
  save_path = self._get_save_path(save_path)
391
412
  os.makedirs(os.path.dirname(save_path), exist_ok=True)
392
413
 
393
- with self.api.get(f"{self.endpoint}download/", stream=True) as response, \
394
- open(save_path, 'wb') as f:
395
- for chunk in response.iter_content(chunk_size=8192):
396
- f.write(chunk)
414
+ with self.api.get(f"{self.endpoint}download/", stream=True) as response:
415
+ file_name = self._get_file_name(response)
416
+ full_path = f"{save_path}/{file_name}"
417
+ with open(full_path, 'wb') as f:
418
+ pbar = self._create_progress_bar() if progress_bar else None
419
+ for chunk in response.iter_content(chunk_size=8192):
420
+ f.write(chunk)
421
+ if pbar:
422
+ pbar.update(len(chunk))
423
+ pbar.refresh()
424
+ if pbar:
425
+ pbar.close()
397
426
 
398
- final_path = os.path.abspath(save_path) + self._get_file_ext(response)
399
- os.rename(os.path.abspath(save_path), os.path.abspath(final_path))
400
- return os.path.abspath(final_path)
427
+ return os.path.abspath(full_path)
401
428
 
402
429
 
403
- def get_content_file(self, save_path: str = None) -> str:
430
+ def get_content_file(self, save_path: str = None, progress_bar: bool = True) -> str:
404
431
  """
405
432
  Get Raster Content URL
406
433
 
407
434
  Args:
408
- save_path (str): The path to save the raster.
409
-
435
+ save_path (str, optional): Path where the file should be saved.
436
+ If not provided, it saves to the current working directory
437
+ using the original filename and appropriate extension.
438
+ progress_bar (bool, optional): Whether to show a progress bar. default: True
439
+
410
440
  Returns:
411
441
  str: The path to save the raster.
412
442
 
@@ -427,17 +457,23 @@ class Raster(Base):
427
457
  save_path = self._get_save_path(save_path)
428
458
  os.makedirs(os.path.dirname(save_path), exist_ok=True)
429
459
 
430
- with self.api.get(f"{self.endpoint}content/", stream=True) as response, \
431
- open(save_path, 'wb') as f:
432
- for chunk in response.iter_content(chunk_size=8192):
433
- f.write(chunk)
460
+ with self.api.get(f"{self.endpoint}content/", stream=True) as response:
461
+ file_name = self._get_file_name(response)
462
+ full_path = f"{save_path}/{file_name}"
463
+ with open(full_path, 'wb') as f:
464
+ pbar = self._create_progress_bar() if progress_bar else None
465
+ for chunk in response.iter_content(chunk_size=8192):
466
+ f.write(chunk)
467
+ if pbar:
468
+ pbar.update(len(chunk))
469
+ pbar.refresh()
470
+ if pbar:
471
+ pbar.close()
434
472
 
435
- final_path = os.path.abspath(save_path) + self._get_file_ext(response)
436
- os.rename(os.path.abspath(save_path), os.path.abspath(final_path))
437
- return os.path.abspath(final_path)
473
+ return os.path.abspath(full_path)
438
474
 
439
475
 
440
- def get_tile_render_url(self, x: int, y: int, z: int) -> str:
476
+ def get_render_png_url(self, x: int, y: int, z: int, **kwargs) -> str:
441
477
  """
442
478
  Get the PNG URL of the raster.
443
479
 
@@ -446,6 +482,15 @@ class Raster(Base):
446
482
  y (int): The y coordinate of the tile.
447
483
  z (int): The zoom level of the tile.
448
484
 
485
+ Keyword Args:
486
+ indexes (str, optional): list of comma separated band indexes to be rendered. e.g. 1, 2, 3
487
+ nodata (int, optional)
488
+ expression (str, optional): band math expression. e.g. b1*b2+b3
489
+ rescale (List, optional): comma (',') separated Min,Max range. Can set multiple time for multiple bands.
490
+ color_formula (str, optional): Color formula. e.g. gamma R 0.5
491
+ colormap_name (str, optional)
492
+ colormap (str, optional): JSON encoded custom Colormap. e.g. {"0": "#ff0000", "1": "#00ff00"} or [[[0, 100], "#ff0000"], [[100, 200], "#00ff00"]]
493
+
449
494
  Returns:
450
495
  str: The PNG Render URL of the raster.
451
496
 
@@ -456,11 +501,22 @@ class Raster(Base):
456
501
  >>> raster = Raster.get_raster(client, uuid="12345678-1234-5678-1234-567812345678")
457
502
  >>> raster.get_tile_render_url(x=10, y=20, z=1)
458
503
  """
504
+ params = clean_data({
505
+ 'indexes': kwargs.get('indexes'),
506
+ 'nodata': kwargs.get('nodata'),
507
+ 'expression': kwargs.get('expression'),
508
+ 'rescale': kwargs.get('rescale'),
509
+ 'color_formula': kwargs.get('color_formula'),
510
+ 'colormap_name': kwargs.get('colormap_name'),
511
+ 'colormap': kwargs.get('colormap')
512
+ })
513
+ query_string = urlencode(params)
459
514
  endpoint = urljoin(self.api.base_url, f'{self.endpoint}render/{z}/{x}/{y}.png')
515
+ endpoint = urljoin(endpoint, f'?{query_string}')
460
516
  return endpoint
461
517
 
462
518
 
463
- def get_tile_pbf_url(self, x: int, y: int, z: int) -> str:
519
+ def get_tile_pbf_url(self, x: int, y: int, z: int, indexes: str = None) -> str:
464
520
  """
465
521
  Get the URL of the tile.
466
522
 
@@ -468,6 +524,7 @@ class Raster(Base):
468
524
  x (int): The x coordinate of the tile.
469
525
  y (int): The y coordinate of the tile.
470
526
  z (int): The zoom level of the tile.
527
+ indexes (str, optional): list of comma separated band indexes to be rendered. e.g. 1, 2, 3
471
528
 
472
529
  Returns:
473
530
  str: The URL of the tile.
@@ -479,7 +536,12 @@ class Raster(Base):
479
536
  >>> raster = Raster.get_raster(client, uuid="12345678-1234-5678-1234-567812345678")
480
537
  >>> raster.get_tile_pbf_url(x=10, y=20, z=1)
481
538
  """
539
+ params = clean_data({
540
+ 'indexes': indexes
541
+ })
542
+ query_string = urlencode(params)
482
543
  endpoint = urljoin(self.api.base_url, f'{self.endpoint}tiles/{z}/{x}/{y}.pbf')
544
+ endpoint = urljoin(endpoint, f'?{query_string}')
483
545
  return endpoint
484
546
 
485
547
 
@@ -2,7 +2,7 @@ from typing import List, Any, TYPE_CHECKING, Union, Dict
2
2
  from urllib.parse import urlencode, urljoin
3
3
 
4
4
  from .base import Base
5
- from .utils import clean_data
5
+ from .utils import clean_data, xor_encode
6
6
  from .enums import UserRole, UserStatus
7
7
  from .plan import Plan
8
8
 
@@ -113,11 +113,9 @@ class User(Base):
113
113
  'return_count': kwargs.get('return_count', False),
114
114
  'skip': kwargs.get('skip', 0),
115
115
  'limit': kwargs.get('limit', 10),
116
- 'user_id': kwargs.get('user_id'),
117
- 'shared': kwargs.get('shared', False)
116
+ 'user_id': kwargs.get('user_id')
118
117
  }
119
118
  return super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: User(api, item['id'], item))
120
-
121
119
 
122
120
 
123
121
  @classmethod
@@ -174,7 +172,7 @@ class User(Base):
174
172
  data = {
175
173
  "username": username,
176
174
  "email": email,
177
- "password": password,
175
+ "password": xor_encode(password),
178
176
  "role": role.value,
179
177
  "first_name": first_name,
180
178
  "last_name": last_name,
@@ -184,7 +182,6 @@ class User(Base):
184
182
  return super()._create(api, cls.BASE_ENDPOINT, data, factory_func=lambda api, item: User(api, item['id'], item))
185
183
 
186
184
 
187
-
188
185
  @classmethod
189
186
  def search_users(cls, api: 'GeoboxClient', search: str = None, skip: int = 0, limit: int = 10) -> List['User']:
190
187
  """
@@ -377,7 +374,7 @@ class User(Base):
377
374
  >>> user.change_password(new_password='user_new_password')
378
375
  """
379
376
  data = clean_data({
380
- "new_password": new_password
377
+ "new_password": xor_encode(new_password)
381
378
  })
382
379
  endpoint = urljoin(self.endpoint, 'change-password')
383
380
  self.api.post(endpoint, data, is_json=False)
@@ -1,4 +1,11 @@
1
1
  from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
2
+ import base64
3
+
4
+
5
+ def xor_encode(s, key=42):
6
+ xor_str = ''.join(chr(ord(c) ^ key) for c in s)
7
+ encoded_bytes = base64.b64encode(xor_str.encode('utf-8'))
8
+ return encoded_bytes.decode('utf-8')
2
9
 
3
10
 
4
11
  def clean_data(data: dict) -> dict:
@@ -439,6 +439,36 @@ class VectorLayer(Base):
439
439
  return super()._create(self.api, endpoint, data, factory_func=lambda api, item: VectorLayerVersion(api, item['uuid'], item))
440
440
 
441
441
 
442
+ def get_versions(self, **kwargs) -> List['VectorLayerVersion']:
443
+ """
444
+ Get list of versions of the current vector layer object with optional filtering and pagination.
445
+
446
+ Keyword Args:
447
+ q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
448
+ search (str): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value. NOTE: if q param is defined this param will be ignored.
449
+ search_fields (str): comma separated list of fields for searching.
450
+ order_by (str): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
451
+ return_count (bool): Whether to return total count. default is False.
452
+ skip (int): Number of items to skip. default is 0.
453
+ limit (int): Number of items to return. default is 10.
454
+ user_id (int): Specific user. privileges required.
455
+ shared (bool): Whether to return shared versions. default is False.
456
+
457
+ Returns:
458
+ List[VectorLayerVersion] | int: A list of vector layer version instances or the total number of versions.
459
+
460
+ Example:
461
+ >>> from geobox import GeoboxClient
462
+ >>> from geobox.version import VectorLayerVersion
463
+ >>> client = GeoboxClient()
464
+ >>> layer = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
465
+ >>> versions = layer.get_versions()
466
+ or
467
+ >>> versions = layer.get_versions()
468
+ """
469
+ return VectorLayerVersion.get_versions(self.api, layer_id=self.id, **kwargs)
470
+
471
+
442
472
  @property
443
473
  def wfs(self) -> str:
444
474
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geobox
3
- Version: 1.3.0
3
+ Version: 1.3.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.3.0"
7
+ version = "1.3.2"
8
8
  description = "SDK for Geobox's APIs"
9
9
  authors = [
10
10
  {name = "Hamid Heydari", email = "heydari.h62@gmail.com"}
@@ -139,11 +139,11 @@ def test_get_point(api, mock_mosaic_data):
139
139
  api.get.assert_called_once_with(f'{mosaic.endpoint}point?lat=1&lng=2')
140
140
 
141
141
 
142
- def test_get_tile_render_url(api, mock_mosaic_data):
142
+ def test_get_render_png_url(api, mock_mosaic_data):
143
143
  """Test the get_tile_render_url method."""
144
144
  mosaic = Mosaic(api, mock_mosaic_data['uuid'], mock_mosaic_data)
145
145
 
146
- tile_render_url = mosaic.get_tile_render_url(x=1, y=2, z=3)
146
+ tile_render_url = mosaic.get_render_png_url(x=1, y=2, z=3)
147
147
 
148
148
  assert tile_render_url == f'{api.base_url}{mosaic.endpoint}render/3/1/2.png'
149
149
 
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
  from unittest.mock import MagicMock, patch
3
+ from unittest import mock
3
4
  import os
4
5
  import tempfile
5
6
  from urllib.parse import urljoin
@@ -93,14 +94,14 @@ def test_update(api, mock_raster_data):
93
94
  raster = Raster(api, uuid=mock_raster_data['uuid'], data=mock_raster_data)
94
95
  api.put.return_value = {**mock_raster_data, **{'name': 'new_name', 'display_name': 'New Display Name', 'description': 'New Description'}}
95
96
 
96
- raster.update(name='new_name', display_name='New Display Name', description='New Description')
97
+ result = raster.update(name='new_name', display_name='New Display Name', description='New Description')
97
98
  api.put.assert_called_once_with(
98
99
  raster.endpoint,
99
100
  {'name': 'new_name', 'display_name': 'New Display Name', 'description': 'New Description'}
100
101
  )
101
- assert raster.name == 'new_name'
102
- assert raster.display_name == 'New Display Name'
103
- assert raster.description == 'New Description'
102
+ assert raster.name == result['name']
103
+ assert raster.display_name == result['display_name']
104
+ assert raster.description == result['description']
104
105
 
105
106
 
106
107
  def test_delete(api, mock_raster_data):
@@ -121,12 +122,12 @@ def test_thumbnail(api, mock_raster_data):
121
122
  assert thumbnail_url == f"{api.base_url}{raster.endpoint}thumbnail/"
122
123
 
123
124
 
124
- def test_get_raster_info(api, mock_raster_data):
125
+ def test_raster_info(api, mock_raster_data):
125
126
  """Test getting raster info."""
126
127
  raster = Raster(api, uuid=mock_raster_data['uuid'], data=mock_raster_data)
127
128
  api.get.return_value = mock_raster_data
128
129
 
129
- info = raster.get_raster_info()
130
+ info = raster.info
130
131
  assert info == mock_raster_data
131
132
  api.get.assert_called_once_with(f'{raster.endpoint}info/')
132
133
 
@@ -204,6 +205,21 @@ def test_download(api, mock_raster_data):
204
205
  with pytest.raises(ValueError):
205
206
  raster.download()
206
207
 
208
+ # import warning log
209
+ import builtins
210
+ original_import = builtins.__import__
211
+
212
+ def mocked_import(name, globals=None, locals=None, fromlist=(), level=0):
213
+ if name == "tqdm.auto":
214
+ raise ImportError("No module named 'tqdm'")
215
+ return original_import(name, globals, locals, fromlist, level)
216
+
217
+ with mock.patch("builtins.__import__", side_effect=mocked_import):
218
+ with mock.patch("geobox.api.logger") as mock_logger:
219
+ result = raster._create_progress_bar()
220
+ assert result is None
221
+ mock_logger.warning.assert_called_once()
222
+
207
223
 
208
224
  def test_get_content(api, mock_raster_data):
209
225
  """Test getting raster content."""
@@ -228,11 +244,11 @@ def test_get_content(api, mock_raster_data):
228
244
  raster.get_content_file()
229
245
 
230
246
 
231
- def test_get_tile_png_url(api, mock_raster_data):
247
+ def test_get_render_png_url(api, mock_raster_data):
232
248
  """Test getting tile PNG URL."""
233
249
  raster = Raster(api, uuid=mock_raster_data['uuid'], data=mock_raster_data)
234
- url = raster.get_tile_png_url(x=10, y=20, z=1)
235
- assert url == f"{raster.api.base_url}{raster.endpoint}tiles/1/10/20.png"
250
+ url = raster.get_render_png_url(x=10, y=20, z=1)
251
+ assert url == f"{raster.api.base_url}{raster.endpoint}render/1/10/20.png"
236
252
 
237
253
 
238
254
  def test_get_tile_pbf_url(api, mock_raster_data):
@@ -5,6 +5,7 @@ from urllib.parse import urljoin, urlencode
5
5
  from geobox.user import User
6
6
  from geobox.enums import UserRole, UserStatus
7
7
  from geobox.plan import Plan
8
+ from geobox.utils import xor_encode
8
9
 
9
10
 
10
11
  def test_init(api, mock_user_data):
@@ -38,7 +39,7 @@ def test_get_users(api, mock_admin_user_data):
38
39
  result = User.get_users(api, search='test')
39
40
  assert isinstance(result, list)
40
41
  assert isinstance(result[0], User)
41
- api.get.assert_called_once_with('users/?f=json&search=test&return_count=False&skip=0&limit=10&shared=False')
42
+ api.get.assert_called_once_with('users/?f=json&search=test&return_count=False&skip=0&limit=10')
42
43
 
43
44
  def test_create_user(api, mock_admin_user_data):
44
45
  api.post.return_value = mock_admin_user_data
@@ -57,7 +58,7 @@ def test_create_user(api, mock_admin_user_data):
57
58
  expected_data = {
58
59
  "username": mock_admin_user_data['username'],
59
60
  "email": mock_admin_user_data['email'],
60
- "password": 'password',
61
+ "password": xor_encode('password'),
61
62
  "role": UserRole.ACCOUNT_ADMIN.value,
62
63
  "first_name": mock_admin_user_data['first_name'],
63
64
  "last_name": mock_admin_user_data['last_name'],
@@ -126,7 +127,7 @@ def test_change_password(api, mock_admin_user_data):
126
127
  with patch.object(api, 'post', return_value={"message": "success"}):
127
128
  user.change_password('new_password')
128
129
  expected_endpoint = urljoin(user.endpoint, 'change-password')
129
- api.post.assert_called_once_with(expected_endpoint, {'new_password': 'new_password'}, is_json=False)
130
+ api.post.assert_called_once_with(expected_endpoint, {'new_password': xor_encode('new_password')}, is_json=False)
130
131
 
131
132
 
132
133
  def test_renew_plan(api, mock_admin_user_data):
@@ -253,6 +253,17 @@ def test_create_version(api, mock_vector_data, layer_type, mock_version_data):
253
253
  api.post.assert_called_once_with(f'{layer.endpoint}versions', {'name': 'design_version', 'display_name': 'Design Version', 'description': 'This layer represents design version.'})
254
254
 
255
255
 
256
+ @pytest.mark.parametrize("layer_type", [type for type in LayerType])
257
+ def test_get_versions(api, mock_vector_data, layer_type, mock_version_data):
258
+ layer = VectorLayer(api, uuid=mock_vector_data['uuid'], data=mock_vector_data, layer_type=layer_type)
259
+ api.get.return_value = [mock_version_data]
260
+ versions = layer.get_versions()
261
+ assert len(versions) == 1
262
+ assert versions[0].uuid == mock_version_data['uuid']
263
+ assert versions[0].data == mock_version_data
264
+ api.get.assert_called_once_with(f'vectorLayerVersions/?layer_id={layer.id}&f=json&return_count=False&skip=0&limit=10&shared=False')
265
+
266
+
256
267
  @pytest.mark.parametrize("layer_type", [type for type in LayerType])
257
268
  def test_wfs_access_token(api, mock_vector_data, layer_type):
258
269
  layer = VectorLayer(api, uuid=mock_vector_data['uuid'], data=mock_vector_data, layer_type=layer_type)
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
File without changes