zscaler-sdk-python 0.9.5__tar.gz → 0.9.7__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 (99) hide show
  1. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/PKG-INFO +3 -3
  2. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/pyproject.toml +4 -5
  3. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/__init__.py +1 -1
  4. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/__init__.py +97 -49
  5. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/admin_and_role_management.py +2 -1
  6. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/labels.py +2 -1
  7. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/locations.py +2 -1
  8. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/security.py +17 -5
  9. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/traffic.py +2 -1
  10. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/users.py +7 -4
  11. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/__init__.py +34 -34
  12. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/policies.py +32 -44
  13. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/LICENSE.md +0 -0
  14. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/README.md +0 -0
  15. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/cache/__init__.py +0 -0
  16. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/cache/cache.py +0 -0
  17. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/cache/no_op_cache.py +0 -0
  18. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/cache/zscaler_cache.py +0 -0
  19. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/constants.py +0 -0
  20. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/errors/__init__.py +0 -0
  21. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/errors/error.py +0 -0
  22. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/errors/http_error.py +0 -0
  23. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/errors/zscaler_api_error.py +0 -0
  24. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/exceptions/__init__.py +0 -0
  25. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/exceptions/exceptions.py +0 -0
  26. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/logger.py +0 -0
  27. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/ratelimiter/__init__.py +0 -0
  28. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/ratelimiter/ratelimiter.py +0 -0
  29. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/user_agent.py +0 -0
  30. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/utils.py +0 -0
  31. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcc/__init__.py +0 -0
  32. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcc/client.py +0 -0
  33. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcc/devices.py +0 -0
  34. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcc/secrets.py +0 -0
  35. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/__init__.py +0 -0
  36. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/activation.py +0 -0
  37. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/admin_and_role_management.py +0 -0
  38. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/client.py +0 -0
  39. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/ecgroups.py +0 -0
  40. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/locations.py +0 -0
  41. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zcon/provisioning.py +0 -0
  42. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/__init__.py +0 -0
  43. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/admin.py +0 -0
  44. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/alerts.py +0 -0
  45. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/apps.py +0 -0
  46. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/devices.py +0 -0
  47. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/filters.py +0 -0
  48. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/inventory.py +0 -0
  49. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/troubleshooting.py +0 -0
  50. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/users.py +0 -0
  51. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zdx/zdx_client.py +0 -0
  52. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/activate.py +0 -0
  53. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/apptotal.py +0 -0
  54. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/audit_logs.py +0 -0
  55. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/authentication_settings.py +0 -0
  56. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/client.py +0 -0
  57. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/cloud_apps.py +0 -0
  58. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/cloudappcontrol.py +0 -0
  59. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/device_management.py +0 -0
  60. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/dlp.py +0 -0
  61. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/errors.py +0 -0
  62. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/firewall.py +0 -0
  63. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/forwarding_control.py +0 -0
  64. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/isolation_profile.py +0 -0
  65. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/sandbox.py +0 -0
  66. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/ssl_inspection.py +0 -0
  67. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/url_categories.py +0 -0
  68. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/url_filtering.py +0 -0
  69. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/web_dlp.py +0 -0
  70. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/workload_groups.py +0 -0
  71. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zia/zpa_gateway.py +0 -0
  72. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/README.md +0 -0
  73. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/app_segments.py +0 -0
  74. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/app_segments_inspection.py +0 -0
  75. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/app_segments_pra.py +0 -0
  76. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/authdomains.py +0 -0
  77. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/certificates.py +0 -0
  78. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/client.py +0 -0
  79. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/cloud_connector_groups.py +0 -0
  80. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/connectors.py +0 -0
  81. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/emergency_access.py +0 -0
  82. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/errors.py +0 -0
  83. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/idp.py +0 -0
  84. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/inspection.py +0 -0
  85. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/isolation.py +0 -0
  86. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/lss.py +0 -0
  87. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/machine_groups.py +0 -0
  88. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/microtenants.py +0 -0
  89. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/posture_profiles.py +0 -0
  90. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/privileged_remote_access.py +0 -0
  91. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/provisioning.py +0 -0
  92. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/saml_attributes.py +0 -0
  93. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/scim_attributes.py +0 -0
  94. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/scim_groups.py +0 -0
  95. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/segment_groups.py +0 -0
  96. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/server_groups.py +0 -0
  97. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/servers.py +0 -0
  98. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/service_edges.py +0 -0
  99. {zscaler_sdk_python-0.9.5 → zscaler_sdk_python-0.9.7}/zscaler/zpa/trusted_networks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zscaler-sdk-python
3
- Version: 0.9.5
3
+ Version: 0.9.7
4
4
  Summary: Official Python SDK for the Zscaler Products (Beta)
5
5
  Home-page: https://github.com/zscaler/zscaler-sdk-python
6
6
  License: MIT
@@ -42,8 +42,8 @@ Requires-Dist: requests (>=2.32.3)
42
42
  Requires-Dist: responses (>=0.25.3)
43
43
  Requires-Dist: restfly (>=1.5.0)
44
44
  Requires-Dist: six
45
- Requires-Dist: xmltodict
46
- Requires-Dist: yarl
45
+ Requires-Dist: xmltodict (>=0.14.2)
46
+ Requires-Dist: yarl (>=1.14.0)
47
47
  Project-URL: Bug Tracker, https://github.com/zscaler/zscaler-sdk-python/issues
48
48
  Project-URL: Documentation, https://zscaler-sdk-python.readthedocs.io
49
49
  Project-URL: Repository, https://github.com/zscaler/zscaler-sdk-python
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "zscaler-sdk-python"
3
- version = "0.9.5"
3
+ version = "0.9.7"
4
4
  description = "Official Python SDK for the Zscaler Products (Beta)"
5
5
  authors = ["Zscaler, Inc. <devrel@zscaler.com>"]
6
6
  license = "MIT"
@@ -41,15 +41,14 @@ restfly = ">=1.5.0"
41
41
  six = "*"
42
42
  flatdict = "*"
43
43
  pyyaml = "*"
44
- xmltodict = "*"
45
- yarl = "*"
44
+ xmltodict = ">=0.14.2"
45
+ yarl = ">=1.14.0"
46
46
  pycryptodomex = ">=3.20.0"
47
47
  aenum = "*"
48
48
  pydash = ">=8.0.3"
49
49
  flake8 = "*"
50
50
  pytz = ">=2024.2"
51
51
  black = ">=24.3.0"
52
- # cryptography = ">=3.4,<43.0"
53
52
  cryptography = ">=43.0.0"
54
53
  okta = ">=2.9.7"
55
54
  aiohttp = ">=3.10.2"
@@ -61,7 +60,7 @@ pytest-asyncio = "^0.23.8"
61
60
  pytest-mock = "*"
62
61
  pytest-recording = "^0.13.2"
63
62
  pytest-cov = "*"
64
- pyfakefs = "^5.6.0"
63
+ pyfakefs = ">=5.6.0"
65
64
  isort = "*"
66
65
  wheel = "*"
67
66
  sphinx = "^7.4.7"
@@ -29,7 +29,7 @@ __license__ = "MIT"
29
29
  __contributors__ = [
30
30
  "William Guilherme",
31
31
  ]
32
- __version__ = "0.9.5"
32
+ __version__ = "0.9.7"
33
33
 
34
34
  from zscaler.zdx import ZDXClientHelper # noqa
35
35
  from zscaler.zia import ZIAClientHelper # noqa
@@ -399,67 +399,115 @@ class ZIAClientHelper(ZIAClient):
399
399
  time.sleep(delay)
400
400
  return self.send("DELETE", path, json, params)
401
401
 
402
- ERROR_MESSAGES = {
403
- "UNEXPECTED_STATUS": "Unexpected status code {status_code} received for page {page}.",
404
- "MISSING_DATA_KEY": "The key '{data_key_name}' was not found in the response for page {page}.",
405
- "EMPTY_RESULTS": "No results found for page {page}.",
406
- }
407
-
408
- def get_paginated_data(self, path=None, data_key_name=None, data_per_page=5, expected_status_code=200):
409
- """
410
- Fetch paginated data from the ZIA API.
411
- ...
402
+ def get_paginated_data(
403
+ self,
404
+ path=None,
405
+ expected_status_code=200,
406
+ page=None,
407
+ pagesize=None,
408
+ search=None,
409
+ max_page_size=1000, # Default to 1000, can be adjusted based on endpoint constraints
410
+ max_items=None, # Maximum number of items to retrieve across pages
411
+ max_pages=None, # Maximum number of pages to retrieve
412
+ ):
413
+ """
414
+ Fetches paginated data from the API based on specified parameters and handles pagination.
415
+
416
+ Args:
417
+ path (str): The API endpoint path to send requests to.
418
+ expected_status_code (int): The expected HTTP status code for a successful request. Defaults to 200.
419
+ page (int): Specific page number to fetch. Defaults to 1 if not provided.
420
+ pagesize (int): Number of items per page, default is 100, with a maximum of 1000.
421
+ search (str): Search query to filter the results.
422
+ max_items (int): Maximum number of items to retrieve.
423
+ max_pages (int): Maximum number of pages to fetch.
412
424
 
413
425
  Returns:
414
- - list: List of fetched items.
415
- - str: Error message, if any occurred.
426
+ tuple: A tuple containing:
427
+ - BoxList: A list of fetched items wrapped in a BoxList for easy access.
428
+ - str: An error message if any occurred during the data fetching process.
416
429
  """
430
+ logger = logging.getLogger(__name__)
431
+
432
+ ERROR_MESSAGES = {
433
+ "UNEXPECTED_STATUS": "Unexpected status code {status_code} received for page {page}.",
434
+ "EMPTY_RESULTS": "No results found for page {page}.",
435
+ }
436
+
437
+ # Initialize pagination parameters
438
+ params = {
439
+ "page": page if page is not None else 1, # Start at page 1 if not specified
440
+ "pagesize": min(pagesize if pagesize is not None else 100, max_page_size) # Apply max_page_size limit
441
+ }
442
+
443
+ if search:
444
+ params["search"] = search
417
445
 
418
- page = 1
419
446
  ret_data = []
420
- error_message = None
421
-
422
- while True:
423
- required_url = f"{path}"
424
- should_wait, delay = self.rate_limiter.wait("GET")
425
- if should_wait:
426
- time.sleep(delay)
427
-
428
- # Now proceed with sending the request
429
- response = self.send(
430
- method="GET",
431
- path=required_url,
432
- params={"page": page, "pageSize": data_per_page},
433
- )
447
+ total_collected = 0 # Track total items collected
434
448
 
435
- if response.status_code != expected_status_code:
436
- error_message = self.ERROR_MESSAGES["UNEXPECTED_STATUS"].format(status_code=response.status_code, page=page)
437
- logger.error(error_message)
438
- break
439
- data_json = response.json()
440
- if isinstance(data_json, list):
441
- data = data_json
442
- else:
443
- data = data_json.get(data_key_name)
449
+ try:
450
+ while True:
451
+ # Apply rate-limiting if necessary
452
+ should_wait, delay = self.rate_limiter.wait("GET")
453
+ if should_wait:
454
+ time.sleep(delay)
455
+
456
+ # Send the request to the API
457
+ response = self.send("GET", path=path, params=params)
458
+
459
+ # Check for unexpected status code
460
+ if response.status_code != expected_status_code:
461
+ error_msg = ERROR_MESSAGES["UNEXPECTED_STATUS"].format(
462
+ status_code=response.status_code, page=params["page"]
463
+ )
464
+ logger.error(error_msg)
465
+ return BoxList([]), error_msg
466
+
467
+ # Parse the response as a flat list of items
468
+ response_data = response.json()
469
+ if not isinstance(response_data, list):
470
+ error_msg = ERROR_MESSAGES["EMPTY_RESULTS"].format(page=params["page"])
471
+ logger.warn(error_msg)
472
+ return BoxList([]), error_msg
473
+
474
+ data = convert_keys_to_snake(response_data)
475
+
476
+ # If searching for a specific item, stop if we find a match
477
+ if search:
478
+ for item in data:
479
+ if item.get("name") == search:
480
+ ret_data.append(item)
481
+ return BoxList(ret_data), None
482
+
483
+ # Limit data collection based on max_items
484
+ if max_items is not None:
485
+ data = data[:max_items - total_collected] # Limit items on the current page
486
+ ret_data.extend(data)
487
+ total_collected += len(data)
488
+
489
+ # Check if we've reached max_items or max_pages limits
490
+ if (max_items is not None and total_collected >= max_items) or \
491
+ (max_pages is not None and params["page"] >= max_pages):
492
+ break
444
493
 
445
- if data is None:
446
- error_message = self.ERROR_MESSAGES["MISSING_DATA_KEY"].format(data_key_name=data_key_name, page=page)
447
- logger.error(error_message)
448
- break
494
+ # Stop if we've processed all available pages (i.e., less than requested page size)
495
+ if len(data) < params["pagesize"]:
496
+ break
449
497
 
450
- if not data: # Checks for empty data
451
- logger.info(self.ERROR_MESSAGES["EMPTY_RESULTS"].format(page=page))
452
- break
498
+ # Move to the next page
499
+ params["page"] += 1
453
500
 
454
- ret_data.extend(convert_keys_to_snake(data))
501
+ finally:
502
+ time.sleep(2) # Ensure a delay between requests regardless of outcome
455
503
 
456
- # Check for more pages
457
- if len(data) == 0 or isinstance(data_json, dict) and int(response.json().get("totalPages")) <= page + 1:
458
- break
504
+ if not ret_data:
505
+ error_msg = ERROR_MESSAGES["EMPTY_RESULTS"].format(page=params["page"])
506
+ logger.warn(error_msg)
507
+ return BoxList([]), error_msg
459
508
 
460
- page += 1
509
+ return BoxList(ret_data), None
461
510
 
462
- return BoxList(ret_data), error_message
463
511
 
464
512
  @property
465
513
  def admin_and_role_management(self):
@@ -48,7 +48,8 @@ class AdminAndRoleManagementAPI:
48
48
  >>> users = zia.admin_and_role_management.list_users('admin@example.com')
49
49
 
50
50
  """
51
- return BoxList(Iterator(self.rest, "adminUsers", **kwargs))
51
+ list, _ = self.rest.get_paginated_data(path="/adminUsers", **kwargs)
52
+ return list
52
53
 
53
54
  def get_user(self, user_id: str) -> Box:
54
55
  """
@@ -58,7 +58,8 @@ class RuleLabelsAPI:
58
58
  ... print(label)
59
59
 
60
60
  """
61
- return BoxList(Iterator(self.rest, "ruleLabels", **kwargs))
61
+ list, _ = self.rest.get_paginated_data(path="/ruleLabels", **kwargs)
62
+ return list
62
63
 
63
64
  def get_label(self, label_id: str) -> Box:
64
65
  """
@@ -64,7 +64,8 @@ class LocationsAPI:
64
64
  ... print(location)
65
65
 
66
66
  """
67
- return BoxList(Iterator(self.rest, "locations", **kwargs))
67
+ list, _ = self.rest.get_paginated_data(path="/locations", **kwargs)
68
+ return list
68
69
 
69
70
  def get_location(self, location_id: str = None, location_name: str = None) -> Box:
70
71
  """
@@ -163,11 +163,23 @@ class SecurityPolicyAPI:
163
163
 
164
164
  payload = {"blacklistUrls": url_list}
165
165
 
166
- resp = self.rest.post("security/advanced/blacklistUrls?action=ADD_TO_LIST", json=payload).status_code
167
-
168
- # Return the object if it was updated successfully
169
- if resp == 204:
170
- return self.get_blacklist()
166
+ try:
167
+ # Send the POST request to add URLs to the blacklist
168
+ response = self.rest.post("security/advanced/blacklistUrls?action=ADD_TO_LIST", json=payload)
169
+
170
+ # Check if the response includes an empty 'blacklistUrls', signaling no update
171
+ if "blacklistUrls" in response and not response["blacklistUrls"]:
172
+ raise Exception("Failed to add URLs to blacklist: The API response returned an empty 'blacklistUrls' list.")
173
+
174
+ # Verify the URLs were added by checking the current blacklist
175
+ updated_blacklist = self.get_blacklist()
176
+ if all(url in updated_blacklist for url in url_list):
177
+ return updated_blacklist
178
+ else:
179
+ raise Exception("Failed to add URLs to blacklist: URLs were not present in the updated blacklist.")
180
+
181
+ except Exception as exc:
182
+ raise Exception(f"Failed to add URLs to blacklist: {exc}")
171
183
 
172
184
  def replace_blacklist(self, url_list: list) -> BoxList:
173
185
  """
@@ -642,7 +642,8 @@ class TrafficForwardingAPI:
642
642
  >>> for credential in zia.traffic.list_vpn_credentials(page_size=200, max_pages=2):
643
643
  ... print(credential)
644
644
  """
645
- return BoxList(Iterator(self.rest, "vpnCredentials", **kwargs))
645
+ list, _ = self.rest.get_paginated_data(path="/vpnCredentials", **kwargs)
646
+ return list
646
647
 
647
648
  def add_vpn_credential(self, authentication_type: str, pre_shared_key: str = None, **kwargs) -> Box:
648
649
  """
@@ -69,8 +69,9 @@ class UserManagementAPI:
69
69
  >>> for department in zia.users.list_departments(page_size=200, max_pages=2):
70
70
  ... print(department)
71
71
  """
72
- return BoxList(Iterator(self.rest, "departments", **kwargs))
73
-
72
+ list, _ = self.rest.get_paginated_data(path="/departments", **kwargs)
73
+ return list
74
+
74
75
  def get_department(self, department_id: str) -> Box:
75
76
  """
76
77
  Returns the department details for a given department.
@@ -127,7 +128,8 @@ class UserManagementAPI:
127
128
  ... print(group)
128
129
 
129
130
  """
130
- return BoxList(Iterator(self.rest, "groups", **kwargs))
131
+ list, _ = self.rest.get_paginated_data(path="/groups", **kwargs)
132
+ return list
131
133
 
132
134
  def get_group(self, group_id: str) -> Box:
133
135
  """
@@ -194,7 +196,8 @@ class UserManagementAPI:
194
196
  ... print(user)
195
197
 
196
198
  """
197
- return BoxList(Iterator(self.rest, "users", **kwargs))
199
+ list, _ = self.rest.get_paginated_data(path="/users", **kwargs)
200
+ return list
198
201
 
199
202
  def add_user(self, name: str, email: str, groups: list, department: dict, **kwargs) -> Box:
200
203
  """
@@ -369,27 +369,27 @@ class ZPAClientHelper(ZPAClient):
369
369
  return self.send("DELETE", path, json, params, api_version=api_version)
370
370
 
371
371
  def get_paginated_data(
372
- self,
373
- path=None,
374
- params=None,
375
- expected_status_code=200,
376
- api_version: str = None,
377
- search=None,
378
- search_field="name",
379
- max_pages=None,
380
- max_items=None,
381
- all_entries=False, # Return all SCIM groups including the deleted ones if set to true
382
- sort_order=None,
383
- sort_by=None,
384
- sort_dir=None,
385
- start_time=None,
386
- end_time=None,
387
- idp_group_id=None,
388
- scim_user_id=None,
389
- scim_username=None,
390
- page=None,
391
- pagesize=None,
392
- microtenant_id=None,
372
+ self,
373
+ path=None,
374
+ params=None,
375
+ expected_status_code=200,
376
+ api_version: str = None,
377
+ search=None,
378
+ search_field="name",
379
+ max_pages=None,
380
+ max_items=None,
381
+ all_entries=False,
382
+ sort_order=None,
383
+ sort_by=None,
384
+ sort_dir=None,
385
+ start_time=None,
386
+ end_time=None,
387
+ idp_group_id=None,
388
+ scim_user_id=None,
389
+ scim_username=None,
390
+ page=None,
391
+ pagesize=None,
392
+ microtenant_id=None,
393
393
  ):
394
394
  """
395
395
  Fetches paginated data from the ZPA API based on specified parameters and handles various types of API pagination.
@@ -432,15 +432,10 @@ class ZPAClientHelper(ZPAClient):
432
432
  if params is None:
433
433
  params = {}
434
434
 
435
- if (page is not None or pagesize is not None) and (max_pages is not None or max_items is not None):
436
- raise ValueError(
437
- "Do not mix 'page' or 'pagesize' with 'max_pages' or 'max_items'. Choose either set of parameters."
438
- )
439
-
440
- params["page"] = page if page is not None else 1 # Default to page 1 if not specified
441
- params["pagesize"] = min(pagesize if pagesize is not None else 20, 500) # Apply maximum constraint and handle default
435
+ # Set initial pagination params
436
+ params["page"] = page or 1
437
+ params["pagesize"] = min(pagesize, 500) if pagesize else 500
442
438
 
443
- # Check for microtenantId in function arguments first, then environment variable
444
439
  if microtenant_id:
445
440
  params["microtenantId"] = microtenant_id
446
441
  elif self.microtenant_id and "microtenantId" not in params:
@@ -472,7 +467,8 @@ class ZPAClientHelper(ZPAClient):
472
467
 
473
468
  try:
474
469
  while True:
475
- if max_pages is not None and (page is not None and page > max_pages):
470
+ # Stop if max_pages reached
471
+ if max_pages is not None and params["page"] > max_pages:
476
472
  break
477
473
 
478
474
  should_wait, delay = self.rate_limiter.wait("GET")
@@ -490,23 +486,27 @@ class ZPAClientHelper(ZPAClient):
490
486
 
491
487
  response_data = response.json()
492
488
  data = response_data.get("list", [])
493
- if not data and (params["page"] == 1):
489
+ if not data and params["page"] == 1:
494
490
  error_msg = ERROR_MESSAGES["EMPTY_RESULTS"]
495
491
  logger.warn(error_msg)
496
492
  return BoxList([]), error_msg
497
493
 
494
+ # Convert and extend the collected data
498
495
  data = convert_keys_to_snake(data)
499
496
  ret_data.extend(data[: max_items - total_collected] if max_items is not None else data)
500
497
  total_collected += len(data)
501
498
 
499
+ # Check if we’ve collected the max_items
502
500
  if max_items is not None and total_collected >= max_items:
503
501
  break
504
502
 
505
- next_page = response_data.get("nextPage")
506
- if not next_page or (max_pages is not None and params["page"] >= max_pages):
503
+ # Determine if there is a next page based on totalPages, converting totalPages to an integer if present
504
+ total_pages = int(response_data.get("totalPages", 0)) # Default to 0 if not provided
505
+ if not total_pages or params["page"] >= total_pages:
507
506
  break
508
507
 
509
- params["page"] = next_page if params["page"] is None else params["page"] + 1
508
+ # Move to the next page
509
+ params["page"] += 1
510
510
 
511
511
  finally:
512
512
  time.sleep(2) # Ensure a delay between requests regardless of outcome
@@ -56,7 +56,7 @@ class PolicySetsAPI:
56
56
  conditions (list): List of condition dicts or tuples.
57
57
 
58
58
  Returns:
59
- :obj:`dict`: The conditions template.
59
+ :obj:`list`: The conditions template.
60
60
 
61
61
  """
62
62
  template = []
@@ -78,63 +78,47 @@ class PolicySetsAPI:
78
78
  "COUNTRY_CODE": [],
79
79
  }
80
80
 
81
+ operators_for_types = {} # Dictionary to store specific operators for each object type
82
+
81
83
  for condition in conditions:
84
+ # Check if the first item in a tuple is an operator, like "AND" or "OR"
85
+ if isinstance(condition, tuple) and isinstance(condition[0], str) and condition[0].upper() in ["AND", "OR"]:
86
+ operator = condition[0].upper()
87
+ condition = condition[1] # The second element is the actual condition
88
+ else:
89
+ operator = "OR" # Default operator if none specified
90
+
91
+ # Process each condition and categorize by object type and operator
82
92
  if isinstance(condition, tuple) and len(condition) == 3:
83
- # Handle each object type according to its pattern
84
93
  object_type = condition[0].upper()
85
94
  lhs = condition[1]
86
95
  rhs = condition[2]
96
+ operand = {"objectType": object_type, "lhs": lhs, "rhs": rhs}
97
+
98
+ # Track the operator for the current object type
99
+ operators_for_types[object_type] = operator
87
100
 
88
101
  if object_type in ["APP", "APP_GROUP"]:
89
- app_and_app_group_operands.append({"objectType": object_type, "lhs": "id", "rhs": rhs})
102
+ app_and_app_group_operands.append(operand)
90
103
  elif object_type in object_types_to_operands:
91
- if object_type == "CLIENT_TYPE":
92
- if rhs in {
93
- "zpn_client_type_exporter",
94
- "zpn_client_type_machine_tunnel",
95
- "zpn_client_type_ip_anchoring",
96
- "zpn_client_type_edge_connector",
97
- "zpn_client_type_zapp",
98
- "zpn_client_type_slogger",
99
- }:
100
- object_types_to_operands[object_type].append({"objectType": object_type, "lhs": "id", "rhs": rhs})
101
- elif object_type in [
102
- "PLATFORM",
103
- "POSTURE",
104
- "TRUSTED_NETWORK",
105
- "SAML",
106
- "SCIM",
107
- "SCIM_GROUP",
108
- "COUNTRY_CODE",
109
- ]:
110
- object_types_to_operands[object_type].append({"objectType": object_type, "lhs": lhs, "rhs": rhs})
111
- else:
112
- object_types_to_operands[object_type].append({"objectType": object_type, "lhs": "id", "rhs": rhs})
104
+ object_types_to_operands[object_type].append(operand)
105
+
113
106
  elif isinstance(condition, dict):
114
- # Handle the dictionary logic based on the Go code schema
115
- condition_template = {}
107
+ if "operator" in condition:
108
+ operators_for_types["default"] = condition["operator"]
109
+ continue # Skip to the next condition after setting the operator
116
110
 
117
- # Extracting keys from the condition dictionary
111
+ condition_template = {}
118
112
  for key in ["id", "negated", "operator"]:
119
113
  if key in condition:
120
114
  condition_template[key] = condition[key]
121
115
 
122
- # Handling the operands
123
116
  operands = condition.get("operands", [])
124
117
  condition_template["operands"] = []
125
118
 
126
119
  for operand in operands:
127
120
  operand_template = {}
128
-
129
- # Extracting keys from the operand dictionary
130
- for operand_key in [
131
- "id",
132
- "idp_id",
133
- "name",
134
- "lhs",
135
- "rhs",
136
- "objectType",
137
- ]:
121
+ for operand_key in ["id", "idp_id", "name", "lhs", "rhs", "objectType"]:
138
122
  if operand_key in operand:
139
123
  operand_template[operand_key] = operand[operand_key]
140
124
 
@@ -142,14 +126,16 @@ class PolicySetsAPI:
142
126
 
143
127
  template.append(condition_template)
144
128
 
145
- # Combine APP and APP_GROUP operands into one block
129
+ # Combine APP and APP_GROUP operands with their specific operator
146
130
  if app_and_app_group_operands:
147
- template.append({"operator": "OR", "operands": app_and_app_group_operands})
131
+ app_group_operator = operators_for_types.get("APP", "OR")
132
+ template.append({"operator": app_group_operator, "operands": app_and_app_group_operands})
148
133
 
149
- # Combine other object types into their own blocks
134
+ # Combine other object types into their blocks with their respective operator
150
135
  for object_type, operands in object_types_to_operands.items():
151
136
  if operands:
152
- template.append({"operator": "OR", "operands": operands})
137
+ operator = operators_for_types.get(object_type, "OR")
138
+ template.append({"operator": operator, "operands": operands})
153
139
 
154
140
  return template
155
141
 
@@ -390,7 +376,9 @@ class PolicySetsAPI:
390
376
  ('app', 'id', '88888'),
391
377
  ('app_group', 'id', '77777),
392
378
  ('client_type', 'zpn_client_type_exporter', 'zpn_client_type_zapp'),
393
- ('trusted_network', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx', True)]
379
+ ("OR", 'trusted_network', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx', True))
380
+ ("OR", ("posture", "d019df8b-ec97-4087-a892-749b5abca54c", "false")),
381
+ ]
394
382
  custom_msg (str):
395
383
  A custom message.
396
384
  description (str):