From a58cbb2df2ed7291382a1e614524fda869c018c5 Mon Sep 17 00:00:00 2001 From: William Guilherme Date: Tue, 7 Jul 2026 14:23:02 -0700 Subject: [PATCH] fix: Fixed ZCELL Sim Handling model details --- CHANGELOG.md | 10 + docsrc/conf.py | 4 +- docsrc/zs/guides/release_notes.rst | 13 ++ docsrc/zs/zcell/index.rst | 3 +- pyproject.toml | 2 +- zscaler/__init__.py | 2 +- zscaler/zcell/models/sim_handling.py | 320 +++++++++++---------------- zscaler/zcell/sim_handling.py | 41 ++-- zscaler/zcell/zcell_service.py | 3 - 9 files changed, 178 insertions(+), 220 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72509a86..c2d90a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Zscaler Python SDK Changelog +## 1.9.36 (July 7, 2026) + +### Notes + +- Python Versions: **v3.9, v3.10, v3.11, v3.12** + +### Bug Fixes + +* [PR #543](https://github.com/zscaler/zscaler-sdk-python/issues/543) - Fixed ZCELL Sim Handling model details. + ## 1.9.35 (July 6, 2026) ### Notes diff --git a/docsrc/conf.py b/docsrc/conf.py index dfe5cbbe..71bce878 100644 --- a/docsrc/conf.py +++ b/docsrc/conf.py @@ -28,9 +28,9 @@ html_title = "" # The short X.Y version -version = "1.9.32" +version = "1.9.36" # The full version, including alpha/beta/rc tags -release = "1.9.32" +release = "1.9.36" # -- General configuration --------------------------------------------------- diff --git a/docsrc/zs/guides/release_notes.rst b/docsrc/zs/guides/release_notes.rst index 096ce1f7..ce7b6db7 100644 --- a/docsrc/zs/guides/release_notes.rst +++ b/docsrc/zs/guides/release_notes.rst @@ -6,6 +6,19 @@ Release Notes Zscaler Python SDK Changelog ---------------------------- +1.9.36 (July 7, 2026) +--------------------------- + +Notes +------- + +- Python Versions: **v3.9, v3.10, v3.11, v3.12** + +Bug Fixes +--------- + +(`#543 `_) - Fixed ZCELL Sim Handling model details. + 1.9.35 (July 6, 2026) --------------------------- diff --git a/docsrc/zs/zcell/index.rst b/docsrc/zs/zcell/index.rst index 0baadf90..90d9a5a0 100644 --- a/docsrc/zs/zcell/index.rst +++ b/docsrc/zs/zcell/index.rst @@ -4,6 +4,7 @@ ZCELL This package covers the Zscaler Cellular interface. .. toctree:: + :maxdepth: 1 :glob: :hidden: @@ -12,4 +13,4 @@ This package covers the Zscaler Cellular interface. .. automodule:: zscaler.zcell :members: :undoc-members: - :show-inheritance: + :show-inheritance: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 12bfba6c..fac58707 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zscaler-sdk-python" -version = "1.9.35" +version = "1.9.36" description = "Official Python SDK for the Zscaler Products" authors = ["Zscaler, Inc. "] license = "MIT" diff --git a/zscaler/__init__.py b/zscaler/__init__.py index 29b965d3..14fb2820 100644 --- a/zscaler/__init__.py +++ b/zscaler/__init__.py @@ -29,7 +29,7 @@ __contributors__ = [ "William Guilherme", ] -__version__ = "1.9.35" +__version__ = "1.9.36" from zscaler.oneapi_client import Client as ZscalerClient # noqa diff --git a/zscaler/zcell/models/sim_handling.py b/zscaler/zcell/models/sim_handling.py index e511c383..cf511fc4 100644 --- a/zscaler/zcell/models/sim_handling.py +++ b/zscaler/zcell/models/sim_handling.py @@ -16,37 +16,9 @@ from zscaler.oneapi_collection import ZscalerCollection from zscaler.oneapi_object import ZscalerObject -from zscaler.zcell.models import sim_handling as sim_handling -class SimHandling(ZscalerObject): - """ - A class representing a SimHandling object. - """ - - def __init__(self, config=None): - super().__init__(config) - if config: - self.icc_id = config["iccId"] if "iccId" in config else None - self.tag_ids = ZscalerCollection.form_list(config["tagIds"] if "tagIds" in config else [], str) - else: - self.icc_id = None - self.tag_ids = [] - - def request_format(self): - """ - Return the object as a dictionary in the format expected for API requests. - """ - parent_req_format = super().request_format() - current_obj_format = { - "iccId": self.icc_id, - "tagIds": self.tag_ids, - } - parent_req_format.update(current_obj_format) - return parent_req_format - - -class SimData(ZscalerObject): +class SimDetails(ZscalerObject): """ A class representing a SimData object. """ @@ -78,15 +50,9 @@ def __init__(self, config=None): self.data_authorize_imei_value = config["dataAuthorizeImeiValue"] if "dataAuthorizeImeiValue" in config else None self.sim_lat = config["simLat"] if "simLat" in config else None self.sim_lng = config["simLng"] if "simLng" in config else None - if "simLocInfo" in config: - if isinstance(config["simLocInfo"], sim_handling.JsonNode): - self.sim_loc_info = config["simLocInfo"] - elif config["simLocInfo"] is not None: - self.sim_loc_info = sim_handling.JsonNode(config["simLocInfo"]) - else: - self.sim_loc_info = None - else: - self.sim_loc_info = None + # ``simLocInfo`` is returned by the API as a JSON-encoded string + # (e.g. '{"ECI": 7965454, "MCC": "311", "MNC": "480", "TAC": 7936}'). + self.sim_loc_info = config["simLocInfo"] if "simLocInfo" in config else None self.event_session_id = config["eventSessionId"] if "eventSessionId" in config else None self.tac_id = config["tacId"] if "tacId" in config else None self.brand_name = config["brandName"] if "brandName" in config else None @@ -195,115 +161,21 @@ def request_format(self): return parent_req_format -class SimDataSearchRequest(ZscalerObject): +class GetActivationCodeResponse(ZscalerObject): """ - A class representing a SimDataSearchRequest object. + A class representing a GetActivationCodeResponse object. """ def __init__(self, config=None): super().__init__(config) if config: - self.iccid = ZscalerCollection.form_list( - config["iccid"] if "iccid" in config else [], sim_handling.SanitizedString50 - ) - if "status" in config: - if isinstance(config["status"], sim_handling.SanitizedString20): - self.status = config["status"] - elif config["status"] is not None: - self.status = sim_handling.SanitizedString20(config["status"]) - else: - self.status = None - else: - self.status = None - if "networkStatus" in config: - if isinstance(config["networkStatus"], sim_handling.SanitizedString20): - self.network_status = config["networkStatus"] - elif config["networkStatus"] is not None: - self.network_status = sim_handling.SanitizedString20(config["networkStatus"]) - else: - self.network_status = None - else: - self.network_status = None - self.ip_address = ZscalerCollection.form_list( - config["ipAddress"] if "ipAddress" in config else [], sim_handling.SanitizedString20 - ) - if "locationCountry" in config: - if isinstance(config["locationCountry"], sim_handling.SanitizedString50): - self.location_country = config["locationCountry"] - elif config["locationCountry"] is not None: - self.location_country = sim_handling.SanitizedString50(config["locationCountry"]) - else: - self.location_country = None - else: - self.location_country = None - self.tag = ZscalerCollection.form_list(config["tag"] if "tag" in config else [], sim_handling.SanitizedString50) - if "deviceType" in config: - if isinstance(config["deviceType"], sim_handling.SanitizedString255): - self.device_type = config["deviceType"] - elif config["deviceType"] is not None: - self.device_type = sim_handling.SanitizedString255(config["deviceType"]) - else: - self.device_type = None - else: - self.device_type = None - if "brandName" in config: - if isinstance(config["brandName"], sim_handling.SanitizedString255): - self.brand_name = config["brandName"] - elif config["brandName"] is not None: - self.brand_name = sim_handling.SanitizedString255(config["brandName"]) - else: - self.brand_name = None - else: - self.brand_name = None - if "marketingName" in config: - if isinstance(config["marketingName"], sim_handling.SanitizedString255): - self.marketing_name = config["marketingName"] - elif config["marketingName"] is not None: - self.marketing_name = sim_handling.SanitizedString255(config["marketingName"]) - else: - self.marketing_name = None - else: - self.marketing_name = None - if "modelName" in config: - if isinstance(config["modelName"], sim_handling.SanitizedString255): - self.model_name = config["modelName"] - elif config["modelName"] is not None: - self.model_name = sim_handling.SanitizedString255(config["modelName"]) - else: - self.model_name = None - else: - self.model_name = None - if "formFactor" in config: - if isinstance(config["formFactor"], sim_handling.SanitizedString20): - self.form_factor = config["formFactor"] - elif config["formFactor"] is not None: - self.form_factor = sim_handling.SanitizedString20(config["formFactor"]) - else: - self.form_factor = None - else: - self.form_factor = None - if "imeiStatus" in config: - if isinstance(config["imeiStatus"], sim_handling.SimImeiStatusEnum): - self.imei_status = config["imeiStatus"] - elif config["imeiStatus"] is not None: - self.imei_status = sim_handling.SimImeiStatusEnum(config["imeiStatus"]) - else: - self.imei_status = None - else: - self.imei_status = None + self.iccid = config["iccid"] if "iccid" in config else None + self.activation_code = config["activationCode"] if "activationCode" in config else None + self.qr_code = config["qrCode"] if "qrCode" in config else None else: - self.iccid = [] - self.status = None - self.network_status = None - self.ip_address = [] - self.location_country = None - self.tag = [] - self.device_type = None - self.brand_name = None - self.marketing_name = None - self.model_name = None - self.form_factor = None - self.imei_status = None + self.iccid = None + self.activation_code = None + self.qr_code = None def request_format(self): """ @@ -311,18 +183,36 @@ def request_format(self): """ parent_req_format = super().request_format() current_obj_format = { - "iccid": [item.request_format() for item in (self.iccid or [])], - "status": self.status, - "networkStatus": self.network_status, - "ipAddress": [item.request_format() for item in (self.ip_address or [])], - "locationCountry": self.location_country, - "tag": [item.request_format() for item in (self.tag or [])], - "deviceType": self.device_type, - "brandName": self.brand_name, - "marketingName": self.marketing_name, - "modelName": self.model_name, - "formFactor": self.form_factor, - "imeiStatus": self.imei_status, + "iccid": self.iccid, + "activationCode": self.activation_code, + "qrCode": self.qr_code, + } + parent_req_format.update(current_obj_format) + return parent_req_format + + +class SimHandling(ZscalerObject): + """ + A class representing a SimHandling object. + """ + + def __init__(self, config=None): + super().__init__(config) + if config: + self.icc_id = config["iccId"] if "iccId" in config else None + self.tag_ids = ZscalerCollection.form_list(config["tagIds"] if "tagIds" in config else [], str) + else: + self.icc_id = None + self.tag_ids = [] + + def request_format(self): + """ + Return the object as a dictionary in the format expected for API requests. + """ + parent_req_format = super().request_format() + current_obj_format = { + "iccId": self.icc_id, + "tagIds": self.tag_ids, } parent_req_format.update(current_obj_format) return parent_req_format @@ -338,7 +228,7 @@ def __init__(self, config=None): if config: self.data_authorize = config["dataAuthorize"] if "dataAuthorize" in config else False self.sim_lock_details = ZscalerCollection.form_list( - config["simLockDetails"] if "simLockDetails" in config else [], sim_handling.SimLockDetail + config["simLockDetails"] if "simLockDetails" in config else [], SimLockDetails ) else: self.data_authorize = False @@ -351,25 +241,25 @@ def request_format(self): parent_req_format = super().request_format() current_obj_format = { "dataAuthorize": self.data_authorize, - "simLockDetails": [item.request_format() for item in (self.sim_lock_details or [])], + "simLockDetails": [dg.request_format() for dg in (self.sim_lock_details or [])], } parent_req_format.update(current_obj_format) return parent_req_format -class SimDataResponse(ZscalerObject): +class SimLockDetails(ZscalerObject): """ - A class representing a SimDataResponse object. + A class representing a SimLockDetails object. """ def __init__(self, config=None): super().__init__(config) if config: - self.page_details = config["pageDetails"] if "pageDetails" in config else None - self.total_usage = config["totalUsage"] if "totalUsage" in config else None + self.imei = config["imei"] if "imei" in config else False + self.iccid = config["iccid"] if "iccid" in config else False else: - self.page_details = None - self.total_usage = None + self.imei = False + self.iccid = False def request_format(self): """ @@ -377,8 +267,8 @@ def request_format(self): """ parent_req_format = super().request_format() current_obj_format = { - "pageDetails": self.page_details, - "totalUsage": self.total_usage, + "imei": self.imei, + "iccid": self.iccid, } parent_req_format.update(current_obj_format) return parent_req_format @@ -392,15 +282,7 @@ class SimUpdateRequest(ZscalerObject): def __init__(self, config=None): super().__init__(config) if config: - if "status" in config: - if isinstance(config["status"], sim_handling.SimStatusEnum): - self.status = config["status"] - elif config["status"] is not None: - self.status = sim_handling.SimStatusEnum(config["status"]) - else: - self.status = None - else: - self.status = None + self.status = config["status"] if "status" in config else None self.iccid = ZscalerCollection.form_list(config["iccid"] if "iccid" in config else [], str) self.reason = config["reason"] if "reason" in config else None else: @@ -422,21 +304,17 @@ def request_format(self): return parent_req_format -class GetActivationCodeResponse(ZscalerObject): +class SimAssignRequest(ZscalerObject): """ - A class representing a GetActivationCodeResponse object. + A class representing a SimAssignRequest object. """ def __init__(self, config=None): super().__init__(config) if config: - self.iccid = config["iccid"] if "iccid" in config else None - self.activation_code = config["activationCode"] if "activationCode" in config else None - self.qr_code = config["qrCode"] if "qrCode" in config else None + self.assignment = config["assignment"] if "assignment" in config else None else: - self.iccid = None - self.activation_code = None - self.qr_code = None + self.assignment = None def request_format(self): """ @@ -444,25 +322,23 @@ def request_format(self): """ parent_req_format = super().request_format() current_obj_format = { - "iccid": self.iccid, - "activationCode": self.activation_code, - "qrCode": self.qr_code, + "assignment": self.assignment, } parent_req_format.update(current_obj_format) return parent_req_format -class SimAssignRequest(ZscalerObject): +class RefreshEsimState(ZscalerObject): """ - A class representing a SimAssignRequest object. + A class representing a RefreshEsimState object. """ def __init__(self, config=None): super().__init__(config) if config: - self.assignment = config["assignment"] if "assignment" in config else None + self.esim_state = config["esimState"] if "esimState" in config else None else: - self.assignment = None + self.esim_state = None def request_format(self): """ @@ -470,23 +346,45 @@ def request_format(self): """ parent_req_format = super().request_format() current_obj_format = { - "assignment": self.assignment, + "esimState": self.esim_state, } parent_req_format.update(current_obj_format) return parent_req_format -class RefreshEsimState(ZscalerObject): +class SimDataSearchRequest(ZscalerObject): """ - A class representing a RefreshEsimState object. + A class representing a SimUpdateRequest object. """ def __init__(self, config=None): super().__init__(config) if config: - self.esim_state = config["esimState"] if "esimState" in config else None + self.iccid = config["iccid"] if "iccid" in config else None + self.status = ZscalerCollection.form_list(config["status"] if "status" in config else [], str) + self.network_status = config["networkStatus"] if "networkStatus" in config else None + self.ip_address = ZscalerCollection.form_list(config["ipAddress"] if "ipAddress" in config else [], str) + self.location_country = config["locationCountry"] if "locationCountry" in config else None + self.tag = ZscalerCollection.form_list(config["tag"] if "tag" in config else [], str) + self.device_type = config["deviceType"] if "deviceType" in config else None + self.brand_name = config["brandName"] if "brandName" in config else None + self.marketing_name = config["marketingName"] if "marketingName" in config else None + self.model_name = config["modelName"] if "modelName" in config else None + self.form_factor = config["formFactor"] if "formFactor" in config else None + self.imei_status = config["imeiStatus"] if "imeiStatus" in config else None else: - self.esim_state = None + self.iccid = [] + self.status = None + self.network_status = None + self.ip_address = [] + self.location_country = None + self.tag = [] + self.device_type = None + self.brand_name = None + self.marketing_name = None + self.model_name = None + self.form_factor = None + self.imei_status = None def request_format(self): """ @@ -494,7 +392,45 @@ def request_format(self): """ parent_req_format = super().request_format() current_obj_format = { - "esimState": self.esim_state, + "iccid": [item.request_format() for item in (self.iccid or [])], + "status": self.status, + "networkStatus": self.network_status, + "ipAddress": [item.request_format() for item in (self.ip_address or [])], + "locationCountry": self.location_country, + "tag": [item.request_format() for item in (self.tag or [])], + "deviceType": self.device_type, + "brandName": self.brand_name, + "marketingName": self.marketing_name, + "modelName": self.model_name, + "formFactor": self.form_factor, + "imeiStatus": self.imei_status, + } + parent_req_format.update(current_obj_format) + return parent_req_format + + +class SimDataResponse(ZscalerObject): + """ + A class representing a SimDataResponse object. + """ + + def __init__(self, config=None): + super().__init__(config) + if config: + self.page_details = config["pageDetails"] if "pageDetails" in config else None + self.total_usage = config["totalUsage"] if "totalUsage" in config else None + else: + self.page_details = None + self.total_usage = None + + def request_format(self): + """ + Return the object as a dictionary in the format expected for API requests. + """ + parent_req_format = super().request_format() + current_obj_format = { + "pageDetails": self.page_details, + "totalUsage": self.total_usage, } parent_req_format.update(current_obj_format) return parent_req_format diff --git a/zscaler/zcell/sim_handling.py b/zscaler/zcell/sim_handling.py index 57027e8e..04803968 100644 --- a/zscaler/zcell/sim_handling.py +++ b/zscaler/zcell/sim_handling.py @@ -23,8 +23,8 @@ from zscaler.zcell.models.sim_handling import ( GetActivationCodeResponse, RefreshEsimState, - SimData, SimDataResponse, + SimDetails, SimHandling, SimLockRequest, SimUpdateRequest, @@ -40,7 +40,7 @@ def __init__(self, request_executor: "RequestExecutor", config: dict = None) -> self._request_executor: RequestExecutor = request_executor self._zcell_customer_id = (config or {}).get("client", {}).get("zcellCustomerId") - def list_sims_details(self, id: str = None, icc_id: str = None, query_params=None) -> APIResult[SimData]: + def list_sims_details(self, id: str = None, icc_id: str = None, query_params=None) -> APIResult[SimDetails]: """ Get sim details by icc_id. @@ -84,11 +84,11 @@ def list_sims_details(self, id: str = None, icc_id: str = None, query_params=Non if error: return (None, None, error) - response, error = self._request_executor.execute(request, SimData) + response, error = self._request_executor.execute(request, SimDetails) if error: return (None, response, error) try: - result = SimData(self.form_response_body(response.get_body())) + result = SimDetails(self.form_response_body(response.get_body())) except Exception as error: return (None, response, error) return (result, response, None) @@ -338,7 +338,9 @@ def update_sims_status(self, id: str = None, **kwargs) -> APIResult[SimUpdateReq tuple: (result, Response, error) Examples: - >>> result, response, error = client.zcell.sim_handling.update_sims_status(id='...', name='example') + >>> result, response, error = client.zcell.sim_handling.update_sims_status( + ... id='...', + ... name='example') >>> if error: ... print(f"Error: {error}") ... return @@ -372,35 +374,39 @@ def update_sims_status(self, id: str = None, **kwargs) -> APIResult[SimUpdateReq return (None, response, error) return (result, response, None) - def update_sims_assign(self, id: str = None, iccid: str = None, **kwargs) -> APIResult[GetActivationCodeResponse]: + def update_sims_assign( + self, id: str = None, iccid: str = None, assignment: str = None, **kwargs + ) -> APIResult[GetActivationCodeResponse]: """ - Assigns an eSIM to the user email and gives back the activation code. + Assigns an eSIM to the user and returns the activation code. Args: id (str): Optional. The ZCell customer ID. Defaults to the ``zcellCustomerId`` config value or the ``ZCELL_CUSTOMER_ID`` environment variable when omitted. - iccid (str): Path parameter. - **kwargs: Request body fields. + iccid (str): The ICCID of the eSIM to assign (path parameter). + assignment (str): The user to assign the eSIM to. Sent in the request body as ``{"assignment": ...}``. Returns: - tuple: (result, Response, error) + tuple: A tuple containing the :class:`GetActivationCodeResponse` (``iccid``, ``activation_code``, + ``qr_code``), the raw Response, and error (if any). Examples: >>> result, response, error = client.zcell.sim_handling.update_sims_assign( - ... id='...', - ... iccid='...', - ... name='example', + ... iccid='89852350525020075842', + ... assignment='testuser', ... ) >>> if error: ... print(f"Error: {error}") ... return - >>> print(result.as_dict()) + >>> print(result.activation_code) + >>> print(result.qr_code) """ http_method = "patch".upper() id = id or self._zcell_customer_id api_url = format_url(f"{self._zcell_base_endpoint_customer}/{id}/sims/{iccid}/assign") - body = kwargs + body = {"assignment": assignment} + body.update(kwargs) request, error = self._request_executor.create_request( method=http_method, @@ -414,10 +420,6 @@ def update_sims_assign(self, id: str = None, iccid: str = None, **kwargs) -> API if error: return (None, response, error) - # The API returns 204 No Content on success — there is no body to parse. - if not response or not response.get_body(): - return (None, response, None) - try: result = GetActivationCodeResponse(self.form_response_body(response.get_body())) except Exception as error: @@ -441,7 +443,6 @@ def update_sims_state(self, id: str = None, iccid: str = None, **kwargs) -> APIR >>> result, response, error = client.zcell.sim_handling.update_sims_state( ... id='...', ... iccid='...', - ... name='example', ... ) >>> if error: ... print(f"Error: {error}") diff --git a/zscaler/zcell/zcell_service.py b/zscaler/zcell/zcell_service.py index f5f93616..6c72298a 100644 --- a/zscaler/zcell/zcell_service.py +++ b/zscaler/zcell/zcell_service.py @@ -30,10 +30,7 @@ class ZCellService: """ZCell Service client, exposing various Zscaler Cellular APIs.""" def __init__(self, request_executor: RequestExecutor, config: dict = None) -> None: - # Ensure the service gets the request executor from the Client object self._request_executor = request_executor - # Shared config carries the resolved zcellCustomerId (config or env), which - # each ZCell API reads to auto-inject the customer id into request paths. self._config = config or {} @property