Metadata-Version: 2.4
Name: zscaler-sdk-python
Version: 2.0.0b2
Summary: Zscaler SDK for Python
Author-email: "Zscaler Inc." <devrel@zscaler.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/zscaler/zscaler-sdk-python
Project-URL: Repository, https://github.com/zscaler/zscaler-sdk-python
Project-URL: Documentation, http://automation.zsapidocs.net/docs/
Project-URL: Bug Tracker, https://github.com/zscaler/zscaler-sdk-python/issues
Keywords: zscaler,sdk,zpa,zia,zdx,zcc,ztw,zid,zidentity
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: cryptography>=46.0.0
Requires-Dist: httpx<2.0.0,>=0.28.0
Requires-Dist: jmespath>=1.0.0
Requires-Dist: pydantic<3.0.0,>=2.0.0
Requires-Dist: PyJWT<3.0.0,>=2.8.0
Requires-Dist: truststore>=0.9.0
Provides-Extra: examples
Requires-Dist: prettytable>=3.0.0; extra == "examples"
Provides-Extra: dev
Requires-Dist: black==25.12.0; extra == "dev"
Requires-Dist: faker>=30.0.0; extra == "dev"
Requires-Dist: flake8>=7.0.0; extra == "dev"
Requires-Dist: isort==7.0.0; extra == "dev"
Requires-Dist: pylint==4.0.4; extra == "dev"
Requires-Dist: pytest==9.0.1; extra == "dev"
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
Requires-Dist: pytest-httpserver>=1.0.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Requires-Dist: anyio==4.12.0; extra == "dev"
Requires-Dist: autopep8==2.3.2; extra == "dev"
Requires-Dist: tomlkit==0.13.3; extra == "dev"
Requires-Dist: uv==0.9.21; extra == "dev"
Dynamic: license-file

# Official Zscaler OneAPI SDK for Python

[![PyPI Version](https://img.shields.io/pypi/v/zscaler-sdk-python.svg)](https://pypi.org/project/zscaler-sdk-python)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zscaler-sdk-python.svg)](https://pypi.org/project/zscaler-sdk-python)
[![License](https://img.shields.io/badge/license-ISC-blue.svg)](LICENSE.md)
[![PyPI - Downloads](https://img.shields.io/pypi/dw/zscaler-sdk-python)](https://pypistats.org/packages/zscaler-sdk-python)
[![Documentation Status](https://img.shields.io/badge/docs-online-success)](http://automation.zsapidocs.net/docs/)
[![Zscaler Community](https://img.shields.io/badge/zscaler-community-blue)](https://community.zscaler.com/)

---

## Table of Contents

- [Overview](#overview)
- [Why This SDK](#why-this-sdk)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Building the SDK](#building-the-sdk)
- [Getting Started](#getting-started)
  - [Authentication](#authentication)
  - [Basic Usage](#basic-usage)
- [Configuration](#configuration)
  - [Configuration File](#configuration-file)
  - [Environment Variables](#environment-variables)
  - [Constructor Parameters](#constructor-parameters)
  - [Configuration Precedence](#configuration-precedence)
- [API Layers](#api-layers)
  - [Resource Layer (Recommended)](#resource-layer-recommended)
  - [Service Client Layer](#service-client-layer)
- [Supported Products](#supported-products)
- [ZPA: customer_id Requirement](#zpa-customer_id-requirement)
- [ZIA and ZTW Context Manager](#zia-and-ztw-context-manager)
- [Collections and Pagination](#collections-and-pagination)
- [Error Handling](#error-handling)
- [Rate Limiting and Retry Mechanism](#rate-limiting-and-retry-mechanism)
- [Logging](#logging)
- [Auto-completion and IntelliSense](#auto-completion-and-intellisense)
- [Examples](#examples)
- [Migrating from zscaler-sdk-python](#migrating-from-zscaler-sdk-python)
- [Contributing](#contributing)
- [Need Help?](#need-help)

---

## Overview

The Zscaler OneAPI SDK for Python (`zscaler-sdk-python`) provides a convenient and consistent interface for interacting with Zscaler product APIs through the [OneAPI](https://help.zscaler.com/oneapi/understanding-oneapi) framework.

This SDK is designed exclusively for OneAPI. It uses OAuth2 authentication via [Zscaler Identity (Zidentity)](https://help.zscaler.com/zidentity/what-zidentity) and provides a high-level `Session` object that manages credentials, configuration, and API calls.

This SDK can be used in your server-side code to interact with the Zscaler API platform across multiple products such as:

- [ZPA API](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/zpa)
- [ZIA API](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/zia)
- [ZDX API](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/zdx)
- [ZCC API](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/zcc)
- [ZIdentity](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/zid)
- [ZTW API](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/zcloudconnector)

> **Note:** This SDK (`zscaler-sdk-python`) is the next-generation replacement for the previous [`zscaler-sdk-python`](https://github.com/zscaler/zscaler-sdk-python) package. See [Why This SDK](#why-this-sdk) and [Migrating from zscaler-sdk-python](#migrating-from-zscaler-sdk-python) below.

### Key Features

- **True resource objects with dot notation** — Work with resources as Python objects (`user.name`, `cred.fqdn`), not raw dictionaries (`user["name"]`). Attribute access, assignment, and IDE auto-completion work out of the box.
- **Active Record pattern** — Create, read, update, and delete resources with natural method calls: `cred.create()`, `cred.load()`, `cred.update()`, `cred.delete()`. No need to call separate service methods and pass IDs around.
- **Non-standard operations as first-class methods** — Operations beyond CRUDL (e.g., `activate()`, `bulk_delete_assistant()`, `export_application()`, `validate_wild_card_domain_name()`) are dynamically generated from API metadata and exposed as snake_case methods on the same resource instances — no drop-down to a low-level client required.
- **Iterator-based collections with chaining** — Paginate effortlessly with `.all()`, apply server-side filters with `.filter()`, cap results with `.limit()`, tune performance with `.page_size()`, and apply client-side JMESPath queries with `.search()` — all chainable in a single expression.
- **Python exceptions, not error tuples** — Errors raise typed exceptions (`BadRequestException`, `NotFoundException`) with full context. Use standard `try`/`except` instead of unpacking `(result, response, error)` tuples on every call.
- **Automatic `snake_case` / `camelCase` conversion** — Write idiomatic Python everywhere. The SDK transparently converts to API `camelCase` on the wire and back.
- **One session, all products** — A single `Session` with unified OAuth2 authentication gives you `session.zia`, `session.zpa`, `session.zcc`, `session.zid`, `session.zdx`, and `session.ztw`. No per-product client setup or separate credentials.
- **IDE auto-completion and type checking** — Ships with PEP 484 `.pyi` type stubs. VS Code, PyCharm, and other editors provide auto-complete, parameter hints, and docstrings for every resource and method.
- **Transparent retry and rate-limit handling** — Built-in exponential backoff with `Retry-After` header support. Rate limits and transient errors are handled automatically — no retry logic needed in your code.
- **Metadata-driven architecture** — API operations, shapes, pagination, and error mappings are defined in JSON metadata generated from TypeSpec. New endpoints are added via JSON, not Python code.
- **Context managers for ZIA/ZTW activation** — `with session.zia as zia:` automatically activates staged configuration changes on exit. No manual activation calls required.

### SDK Design and Data Model

This SDK uses a resource-oriented design with the following characteristics:

- **Resource objects** — Resources (e.g., `VpnCredential`, `UserProfile`) are Python objects with attribute access (dot notation), e.g., `cred.fqdn`, `user.login_name`.
- **Snake_case** — Property names use `snake_case` in Python while the SDK maps to/from API `camelCase` automatically.
- **Native types** — Data is represented as native Python types; responses are JSON-serializable.

---

## Why This SDK

If you are currently using [`zscaler-sdk-python`](https://github.com/zscaler/zscaler-sdk-python), here is why you should switch to `zscaler-sdk-python`:

| Capability | `zscaler-sdk-python` v1 | `zscaler-sdk-python` v2 |
|---|---|---|
| **Data access** | Raw `dict` — `user["name"]` | Resource objects — `user.name` |
| **CRUD pattern** | Service methods returning `(result, resp, error)` tuples | Active Record — `resource.create()`, `.load()`, `.update()`, `.delete()` |
| **Error handling** | Go-style tuples: `result, resp, err = ...` | Python exceptions: `try` / `except NotFoundException` |
| **Pagination** | Manual loop with `resp.has_next()` / `resp.next()` | Iterator-based: `for item in resources.all().limit(100)` |
| **Filtering** | Manual query params dict | Chainable `.filter()` + `.search()` (JMESPath) |
| **Case conversion** | Caller manages `camelCase` ↔ `snake_case` | Automatic — write `snake_case`, API receives `camelCase` |
| **IDE support** | No type stubs | PEP 484 `.pyi` stubs — auto-completion, param hints, docstrings |

### Side-by-side comparison

**Previous SDK — dict access, error tuples, manual pagination:**

```python
from zscaler import ZscalerClient

config = {"clientId": "...", "clientSecret": "...", "vanityDomain": "..."}

with ZscalerClient(config) as client:
    # List users — must unpack tuple on every call
    users, resp, err = client.zia.user_management.list_users()
    if err:
        print(f"Error: {err}")
    else:
        for user in users:
            print(user["name"])  # dict access

    # Manual pagination
    while resp.has_next():
        more_users, resp, err = resp.next()
        if err:
            break
        for user in more_users:
            print(user["name"])
```

**This SDK — resource objects, exceptions, iterator collections:**

```python
from zssdk import Session

session = Session(vanity_domain="...", client_id="...", client_secret="...")

# Iterate all users — pagination is automatic
for user in session.zia.admin_users.all():
    print(user.name)  # attribute access

# Chain filters, limits, and search in one expression
for user in session.zia.admin_users.filter(search="SDWAN").limit(10):
    print(user.name)

# Errors are exceptions, not return values
from zssdk.zsresource.exceptions import NotFoundException
try:
    role = session.zia.AdminRole(id=99999)
    role.load()
except NotFoundException:
    print("Role not found")
```

---

## Prerequisites

- **Python 3.10+**
- An administrator account for the Zscaler products you wish to manage
- [API credentials](https://help.zscaler.com/zidentity/about-api-clients) created in the Zscaler Identity Admin UI (Client ID and Client Secret)

---

## Installation

Install the SDK from PyPI:

```sh
pip install zscaler-sdk-python
```

Or install from source (e.g., for development):

```sh
pip install -e /path/to/oneapi-sdk/python
```

---

## Building the SDK

In most cases, you won't need to build the SDK from source. If you want to build it yourself:

1. Clone the repository
2. Navigate to the `python` directory
3. Run `python -m build` (requires the `build` package: `pip install build`)
4. Install the built package: `pip install dist/zssdk_python-*.whl` or `pip install dist/zssdk_python-*.tar.gz`

---

## Getting Started

### Authentication

The SDK uses OAuth2 via [Zscaler Identity (Zidentity)](https://help.zscaler.com/zidentity/what-zidentity). You must have an API Client created in the [ZIdentity platform](https://help.zscaler.com/zidentity/about-api-clients).

#### OneAPI (API Client Scope)

OneAPI resources are automatically created within the ZIdentity Admin UI based on the RBAC roles applicable to APIs within the various products. For example, in ZIA, navigate to **Administration → Role Management** and select **Add API Role**. Once this role has been saved, return to the ZIdentity Admin UI and from the **Integration** menu select **API Resources**. Click the **View** icon to the right of Zscaler APIs and under the ZIA dropdown you will see the newly created role. In the event a newly created role is not seen in the ZIdentity Admin UI, a **Sync Now** button is provided in the API Resources menu which initiates an on-demand sync of newly created roles.

**Required parameters:**

- **vanity_domain** — Your organization's vanity domain (e.g., `acme`). Use only the vanity part; full domains like `acme.zslogin.net` are not allowed. Refers to the domain used by your organization: `https://<vanity_domain>.zslogin.net/oauth2/v1/token`.
- **client_id** — Your Zidentity API Client ID
- **client_secret** — Your API Client secret
- **cloud** — (Optional) Omit for production. Use only when authenticating to a non-production environment such as `alpha` or `beta` (test-based tenants).

> **Caution:** Do not hard-code credentials. Use environment variables or the configuration file instead.

### Basic Usage

Create a `Session` and access services as attributes. The SDK dynamically provides service resources when you access them.

```python
import os
from zssdk import Session

# Create a session using environment variables (omit cloud for production)
session = Session(
    cloud=os.getenv("ZSCALER_CLOUD"),  # Optional: only for alpha/beta
    vanity_domain=os.getenv("ZSCALER_VANITY_DOMAIN"),
    client_id=os.getenv("ZSCALER_CLIENT_ID"),
    client_secret=os.getenv("ZSCALER_CLIENT_SECRET"),
)

# Access ZIA and list VPN credentials
zia = session.zia
for cred in zia.vpn_credentials.all().limit(10):
    print(cred.fqdn, cred.id)

# Access ZID and list user profiles
zid = session.zid
for user in zid.user_profiles.all().limit(5):
    print(user.login_name, user.display_name)
```

**Using the default session:** You can call `setup_default_session(**kwargs)` once to configure a shared session, then use `get_default_session()` anywhere. The `zssdk.client()` helper returns a low-level client from the default session.

---

## Configuration

### Configuration File

Create a configuration file at `~/.zscaler/config`. The file uses INI format with named profiles (you can also use `config.ini` and pass `config_file="/path/to/config.ini"` to `Session`):

```ini
[default]
vanity_domain = your_vanity_domain
client_id = your_client_id
client_secret = your_client_secret

[beta]
cloud = beta
vanity_domain = your_vanity_domain
client_id = your_beta_client_id
client_secret = your_beta_client_secret
```

> **Note:** Profile names (e.g., `[default]`, `[beta]`) are arbitrary labels you choose. The `cloud` key within a profile is what selects the non-production Zscaler environment. Omit `cloud` entirely for production.

Use a profile by name:

```python
session = Session(profile_name="beta")
```

For multi-product setups with different clouds, define separate profiles and pass `profile_name` when creating the session for each product.

### Environment Variables

**Credential and cloud settings** (prefix `ZSCALER_`):

| Variable                 | Description                                              |
|--------------------------|----------------------------------------------------------|
| `ZSCALER_CLOUD`          | Non-production environment only (e.g., `alpha`, `beta`); omit for production |
| `ZSCALER_VANITY_DOMAIN`  | Your organization's vanity domain                       |
| `ZSCALER_CLIENT_ID`      | OAuth2 Client ID                                         |
| `ZSCALER_CLIENT_SECRET`  | OAuth2 Client Secret                                      |
| `ZSCALER_PRIVATE_KEY`    | Private key string (for JWT Bearer auth)                 |
| `ZSCALER_CERT_FILE_PATH` | Path to private key file (for certificate-based auth)   |
| `ZSCALER_PROFILE`        | Default profile name (default: `default`)                |

**SDK-level settings** (prefix `ZSSDK_`), defined in `zssdk.zscore.configprovider`:

| Variable                 | Description                                              |
|--------------------------|----------------------------------------------------------|
| `ZSSDK_API_VERSION`      | API version (default: `v1`)                              |
| `ZSSDK_USE_FIPS_ENDPOINT`| Use FIPS endpoints (`true` / `false`)                    |
| `ZSSDK_LOG_ENABLED`      | Enable SDK logging (`true` / `false`)                    |
| `ZSSDK_LOG_VERBOSE`      | Verbose logging (`true` / `false`)                       |
| `ZSSDK_LOG_TO_FILE`      | Log to file (`true` / `false`)                           |
| `ZSSDK_LOG_FILE_PATH`    | Log file path (default: `zssdk.log`)                     |
| `ZSSDK_LOG_FORMAT`       | Log format: `basic`, `json`, or `custom` (see [Logging](#logging)) |

### Constructor Parameters

You can pass credentials and settings directly to `Session`:

```python
session = Session(
    vanity_domain="acme",
    client_id="your_client_id",
    client_secret="your_client_secret",
    profile_name="beta",        # Use config file profile
    config_file="/path/to/config",  # Custom config path
)
```

For JWT Bearer (client assertion) authentication:

```python
session = Session(
    vanity_domain="acme",
    client_id="your_client_id",
    cert_file_path="/path/to/private_key.pem",  # Or private_key="<key string>"
)
```

### Configuration Precedence

Configuration is resolved in the following order (highest to lowest):

1. Constructor arguments
2. Instance variables set via `session.set_config_variable()`
3. Environment variables
4. Configuration file (profile)
5. Default values

### User-Agent and Custom Headers

The SDK sends a `User-Agent` header with each request in the format: `oneapi-sdk/<version> python/<py_version> <os>/<os_version>`. You can append custom text via the `Config` object:

```python
from zssdk.zscore.config import Config

config = Config(user_agent_extra="my-app/1.0")
session = Session(config=config, profile_name="beta")
# User-Agent will be: oneapi-sdk/<version> python/<version> <os>/<release> my-app/1.0
```

---

## API Layers

The SDK exposes two ways to interact with Zscaler APIs.

### Resource Layer (Recommended)

The resource layer provides a Pythonic, object-oriented interface. Access services as session attributes and work with resources as objects.

**Creating and managing a resource:**

```python
import zssdk

session = zssdk.Session(profile_name="default")
zia = session.zia

# Create a new VPN credential
cred = zia.VpnCredential()
cred.type = "UFQDN"
cred.fqdn = "office@example.zslogin.net"
cred.pre_shared_key = "somekey"
cred.create()
print(f"Created credential with ID: {cred.id}")

# Load an existing resource by ID
loaded = zia.VpnCredential(id=cred.id)
loaded.load()

# Update
loaded.pre_shared_key = "newkey"
loaded.update()

# Delete
cred.delete()
```

**Working with collections:**

```python
# List all (iterates over paginated results)
for item in zia.vpn_credentials.all():
    print(item.fqdn)

# Apply server-side filters
for item in zia.vpn_credentials.all().filter(type="UFQDN"):
    print(item.fqdn)

# Limit results
for item in zia.vpn_credentials.all().limit(20):
    print(item.fqdn)

# Control page size for performance tuning
for item in zia.vpn_credentials.all().page_size(50).limit(100):
    print(item.fqdn)

# Client-side filtering with JMESPath (see Collections and Pagination for more examples)
for item in zia.vpn_credentials.all().search("[?type=='UFQDN']"):
    print(item.fqdn)

# Iterate by page
for page in zia.vpn_credentials.all().pages():
    for item in page:
        print(item.fqdn)
```

**ZID (Zscaler Identity) example:**

```python
zid = session.zid

# Create user profile
user = zid.UserProfile()
user.login_name = "john.doe@example.com"
user.display_name = "John Doe"
user.primary_email = "john.doe@example.com"
user.status = True
user.create()

# List groups
for group in zid.groups.all().limit(10):
    print(group.name)
```

### Service Client Layer

For lower-level access, you can obtain a raw service client via `session.client("zia")`. This returns a low-level client whose methods map directly to API operations. For typical usage, prefer the resource layer above. Use `session.get_available_services()` to list available service names (`zia`, `zid`, `zcc`, `zpa`, `zdx`, `ztw`) and `session.get_available_resources()` for resource-style service names.

---

## Supported Products

| Product                    | Service Name | Description                                      |
|----------------------------|--------------|--------------------------------------------------|
| Zscaler Internet Access   | `zia`        | ZIA administration and policy management        |
| Zscaler Identity           | `zid`        | User, group, and API client management          |
| Zscaler Client Connector   | `zcc`        | Client Connector administration                 |
| Zscaler Private Access     | `zpa`        | ZPA configuration and policies                 |
| Zscaler Digital Experience | `zdx`        | ZDX monitoring and analytics                   |
| Zscaler Cloud & Branch Connector | `ztw`  | Cloud & Branch Connector management             |

---

## ZPA: customer_id Requirement

When interacting with **Zscaler Private Access (ZPA)** endpoints, you must provide `customer_id` in addition to the standard authentication parameters. The `customer_id` is the ZPA tenant ID, found under **Configuration & Control → Public API → API Keys** in the ZPA Admin Portal.

For ZPA resources, pass `customer_id` as a parameter to each operation. Every ZPA API call includes `customer_id` in the request path (e.g., `/customers/{customerId}/...`), so you must supply it when creating, updating, or querying ZPA resources.

`microtenant_id` is optional and only required when using ZPA with Microtenant; pass `0` for the default microtenant.

---

## ZIA and ZTW Context Manager

The SDK provides a context manager pattern for **ZIA** and **ZTW** that automatically activates configuration changes when exiting the block.

### How It Works

When you use the `with` statement with `session.zia` or `session.ztw`:

1. **Entry** — The service resource is returned; you make configuration changes within the block.
2. **Exit** — When exiting the context (successfully or via exception), the SDK runs a finalize workflow that activates staged changes.

### Activation Process

- **ZIA** — The finalize workflow calls the activation status endpoint. If the status is `PENDING`, it deauthenticates the session, which triggers activation of all staged configuration changes.
- **ZTW** — The finalize workflow fetches the status; if `PENDING`, it explicitly calls the Activate endpoint.

### Example

```python
import zssdk

session = zssdk.Session(profile_name="default")

# ZIA: context manager activates changes on exit
with session.zia as zia:
    role = zia.AdminRole()
    role.name = "New API Role"
    role.description = "Role created via API"
    role.create()
# All staged ZIA changes are activated here

# ZTW: context manager activates changes on exit
with session.ztw as ztw:
    group = ztw.IpDestinationGroup()
    group.name = "New IP Group"
    group.description = "IP group via API"
    group.create()
# All staged ZTW changes are activated here
```

### Benefits

- **Automatic activation** — No need to call activation endpoints manually.
- **Deterministic behavior** — Exiting the context always triggers the activation workflow.
- **Error handling** — Even if an exception occurs, the context manager runs the finalize logic on normal exit (exceptions are not suppressed).

---

## Collections and Pagination

Collections support the following methods:

| Method          | Description                                                       |
|-----------------|-------------------------------------------------------------------|
| `.all()`        | Returns an iterable over all resources (handles pagination)       |
| `.filter(**kwargs)` | Applies server-side filters (snake_case or camelCase)        |
| `.limit(n)`     | Stops after yielding `n` resources                                |
| `.page_size(n)` | Sets the number of resources fetched per API request             |
| `.search(expr)` | Client-side JMESPath filtering on each page                       |
| `.pages()`      | Yields pages (lists) of resources instead of individual items    |

Example with combined options:

```python
# Server-side filter, custom page size, limit total results
for cred in (
    zia.vpn_credentials.all()
    .filter(type="UFQDN")
    .page_size(25)
    .limit(100)
):
    print(cred.fqdn)
```

### Client-side filtering with JMESPath

The `.search(expr)` method applies a [JMESPath](https://jmespath.org/) expression on each page of results. Use it when you need filtering that the API does not support server-side. The expression is applied to the items array, so you write it relative to each item.

```python
# Exact match on a field
for user in zia.admin_users.search("[?name == 'SDWAN-SilverPeak']"):
    print(user.name)

# Multiple values with OR
for user in zia.admin_users.search("[?name == 'SDWAN-SilverPeak' || name == 'SDWAN-VeloCloud']"):
    print(user.name)

# Substring match using contains()
for user in zia.admin_users.search("[?contains(name, 'SilverPeak')]"):
    print(user.name)

# Chain with server-side filter (filter first to reduce data, then search)
for user in zia.admin_users.filter(search="SDWAN").search("[?contains(name, 'SilverPeak')]"):
    print(user.name)

# Chain with limit and page_size
for user in zia.admin_users.search("[?name == 'SDWAN-SilverPeak']").limit(10).page_size(50):
    print(user.name)
```

---

## Error Handling

The SDK raises structured exceptions:

| Exception            | Base          | When it occurs                                      |
|----------------------|---------------|----------------------------------------------------|
| `OneApiError`        | —             | Base SDK error                                     |
| `ZsResourceError`    | `OneApiError` | Resource-level errors; includes `resource_name`, `action_name` |
| `BadRequestException`| `ZsResourceError` | API returns 400 Bad Request                     |
| `NotFoundException` | `ZsResourceError` | API returns 404 Not Found                      |

Additional resource-specific exceptions (e.g., `ConflictException`) may be defined in `zssdk.zsresource.exceptions`.

**Example:**

```python
from zssdk.zscore import OneApiError
from zssdk.zsresource import ZsResourceError
from zssdk.zsresource.exceptions import BadRequestException

try:
    cred = zia.VpnCredential()
    cred.fqdn = "invalid"
    cred.create()
except BadRequestException as e:
    print(f"Bad request: {e}")
except ZsResourceError as e:
    print(f"Resource error: {e.resource_name} / {e.action_name}: {e}")
except OneApiError as e:
    print(f"SDK error: {e}")
```

---

## Rate Limiting and Retry Mechanism

Each Zscaler product has its own rate limiting criteria. Product-specific limits and quotas are documented in the [Zscaler Automation Hub Rate Limiting Guide](https://automate.zscaler.com/docs/docs/api-reference-and-guides/api-reference/guides/rate-limiting).

### Built-in Retry Mechanism

The SDK includes a built-in retry mechanism (configured in `zssdk.zscore.data._retry`) that handles rate limiting and transient failures automatically. When a retryable response or connection error occurs, the SDK:

1. **Checks for rate limit headers** — If present, waits the specified time before retrying.
2. **Falls back to exponential backoff** — If no header is present, uses exponential backoff (base 0.3s, growth factor 2).
3. **Caps wait time** — Maximum wait is 300 seconds per retry.
4. **Limits attempts** — Retries up to 3 times (`max_attempts`) before raising the final exception.

The retry logic is applied per request across all supported products (ZIA, ZID, ZCC, ZPA, ZDX, ZTW).

### HTTP Status Codes That Trigger Retries

The SDK retries requests when it receives one of the following responses:

| Status Code | Meaning                 | Policy Name        |
|-------------|-------------------------|--------------------|
| `429`       | Too Many Requests       | TooManyRequests, Throttling |
| `500`       | Internal Server Error   | ServerError (with TransientFailure) |
| `502`       | Bad Gateway             | BadGateway         |
| `503`       | Service Unavailable     | ServiceUnavailable |
| `504`       | Gateway Timeout         | GatewayTimeout     |

Connection errors (e.g., `ConnectionError`, `ReadTimeoutError`) also trigger retries.

### Retry-After and Rate Limit Headers

When rate limiting is enabled (default), the SDK uses response headers to determine how long to wait before retrying. Headers are checked in this order:

1. **`Retry-After`** or **`retry-after`** — Standard HTTP headers. Supports values like `9` (seconds) or `9s`.
2. **`x-ratelimit-reset`**, **`X-RateLimit-Reset`**, **`RateLimit-Reset`**, **`X-Rate-Limit-Retry-After-Seconds`** — Product-specific headers indicating seconds until the rate limit resets.

If no rate limit header is present, the SDK falls back to **exponential backoff** (base 0.3s, growth factor 2).

---

## Logging

SDK logging is controlled by environment variables (see [Environment Variables](#environment-variables)) or a `LoggerConfig` object.

### Log format (`ZSSDK_LOG_FORMAT`)

The `logging_format` setting supports three values:

| Format    | Description                                                                 |
|-----------|-----------------------------------------------------------------------------|
| `basic`   | Human-readable lines: `%(asctime)s - %(name)s - %(module)s - %(levelname)s - %(message)s`. Default format. |
| `json`    | Structured JSON output, one record per line, suitable for log aggregators (e.g., Grafana Loki). |
| `custom`  | Use your own `logging.Formatter` instance. **Only available programmatically** via `LoggerConfig`; requires passing `custom_formatter` and cannot be set via environment variables. |

**Environment variables example:**

```sh
export ZSSDK_LOG_ENABLED=true
export ZSSDK_LOG_VERBOSE=false
export ZSSDK_LOG_TO_FILE=false
export ZSSDK_LOG_FILE_PATH=zssdk.log
export ZSSDK_LOG_FORMAT=basic
```

**Programmatic configuration:**

```python
from zssdk.zscore.config import Config, LoggerConfig

config = Config(
    logger_config=LoggerConfig(
        enabled=True,
        verbose=False,
        logging_format="json",
        log_to_file=True,
        log_file_path="/var/log/zssdk.log",
    )
)
session = Session(config=config, profile_name="default")
```

**Custom format (programmatic only):**

```python
import logging
from zssdk.zscore.config import Config, LoggerConfig

custom_formatter = logging.Formatter("%(levelname)s | %(message)s")
config = Config(
    logger_config=LoggerConfig(
        enabled=True,
        logging_format="custom",
        custom_formatter=custom_formatter,
    )
)
session = Session(config=config, profile_name="default")
```

Sensitive fields (tokens, secrets, passwords) are redacted in log output.

---

## Auto-completion and IntelliSense

The SDK ships with PEP 484 [type stub](https://peps.python.org/pep-0484/) files (`.pyi`) that enable rich IDE support. Compatible editors (e.g., VS Code with Pylance, PyCharm) automatically use these stubs for:

- **Auto-completion** — As you type `session.zia.` or `cred.`, the IDE suggests available resources, methods, and properties.
- **Parameter hints** — Method signatures and parameter names appear in tooltips.
- **Docstrings** — Resource and method documentation is shown in hover tooltips.
- **Type checking** — Static type checkers (e.g., Pyright, mypy) can validate your code.

### What You Get

| Location              | Stub file(s)           | Purpose                                      |
|-----------------------|------------------------|----------------------------------------------|
| Session / services    | `session.pyi`          | `session.zia`, `session.zid`, etc.           |
| ZIA resources         | `zia_resources.pyi`    | `VpnCredential`, `AdminUser`, collections   |
| ZID resources         | `zid_resources.pyi`    | `UserProfile`, `Group`, `ApiClient`         |
| ZPA, ZCC, ZDX, ZTW    | `*_resources.pyi`     | Product-specific resources and methods      |
| Low-level clients     | `*_client.pyi`        | Service client method signatures             |

### Setup

No additional setup is required. The stubs are part of the installed package and are picked up automatically by IDEs that support PEP 484. Ensure your IDE's Python interpreter points to the environment where `zscaler-sdk-python` is installed.

### Example

When you type `session.zia.` in VS Code or PyCharm, you will see completions for resources such as `VpnCredential`, `AdminUser`, `vpn_credentials`, and `admin_users`. Selecting a resource shows its available properties and methods (e.g., `create()`, `load()`, `update()`, `delete()`).

---

## Examples

The repository includes runnable CLI scripts and inline examples under `examples/`, organized by product. Prerequisites and configuration are covered in the [Configuration](#configuration) section above.

All CLI scripts are self-documenting. Run any script with no arguments for a usage hint, or `--help` for the full reference:

```sh
python examples/zid/user_management/user_management.py --help
python examples/zia/vpn_credentials_management/vpn_credentials_management.py --help
```

---

## Migrating from zscaler-sdk-python

If you are coming from the previous [`zscaler-sdk-python`](https://github.com/zscaler/zscaler-sdk-python) package, here is what changes.

### Installation

```sh
# Remove the old package
pip uninstall zscaler-sdk-python

# Install the new package
pip install zscaler-sdk-python
```

The import name changes from `zscaler` to `zssdk`.

### Authentication

**Before:**

```python
from zscaler import ZscalerClient

config = {
    "clientId": "...",
    "clientSecret": "...",
    "vanityDomain": "...",
    "cloud": "beta",
}
client = ZscalerClient(config)
```

**After:**

```python
from zssdk import Session

session = Session(
    client_id="...",
    client_secret="...",
    vanity_domain="...",
    cloud="beta",
)
```

Key differences:
- Constructor uses Python keyword arguments (`snake_case`) instead of a config dictionary (`camelCase`).
- Configuration can also come from `~/.zscaler/config` (INI format) or environment variables. See [Configuration](#configuration).

### Working with resources

**Before** — dict access, error tuples:

```python
with ZscalerClient(config) as client:
    role, resp, err = client.zia.admin_roles.get_role(role_id="12345")
    if err:
        print(f"Error: {err}")
    else:
        print(role["name"])
```

**After** — resource objects, exceptions:

```python
from zssdk.zsresource.exceptions import NotFoundException

try:
    role = session.zia.AdminRole(id=12345)
    role.load()
    print(role.name)
except NotFoundException:
    print("Role not found")
```

### Pagination

**Before** — manual loop:

```python
groups, resp, err = client.zia.user_management.list_groups()
while resp.has_next():
    more, resp, err = resp.next()
    if err:
        break
    groups.extend(more)
```

**After** — iterator with chaining:

```python
# All groups, paginated automatically
for group in session.zia.admin_roles.all():
    print(group.name)

# With server-side filter and limit
for group in session.zia.admin_roles.filter(search="API").limit(50):
    print(group.name)
```

### Configuration

**Before** — YAML file at `~/.zscaler/zscaler.yaml`:

```yaml
zscaler:
  client:
    clientId: "..."
    clientSecret: "..."
    vanityDomain: "..."
```

**After** — INI file at `~/.zscaler/config`:

```ini
[default]
vanity_domain = ...
client_id = ...
client_secret = ...
```

### Quick reference

| What | `zscaler-sdk-python` v1 | `zscaler-sdk-python` v2 |
|---|---|---|
| Import | `from zscaler import ZscalerClient` | `from zssdk import Session` |
| Access field | `user["name"]` | `user.name` |
| Create resource | `client.zia.admin_roles.add_role(name="X")` | `role = session.zia.AdminRole(); role.name = "X"; role.create()` |
| Delete resource | `client.zia.admin_roles.delete_role(role_id="123")` | `role = session.zia.AdminRole(id=123); role.delete()` |
| Handle error | `result, resp, err = ...; if err: ...` | `try: ... except NotFoundException: ...` |
| Paginate | `while resp.has_next(): resp.next()` | `for item in collection.all():` |
| Filter | `query_params={"search": "X"}` | `.filter(search="X")` or `.search("[?name=='X']")` |
| Config file | `~/.zscaler/zscaler.yaml` | `~/.zscaler/config` (INI) |

---

## Contributing

Contributions are welcome. Please follow the project's coding standards and submit pull requests via the repository's normal workflow.

---

## Need Help?

- [Zscaler Community](https://community.zscaler.com/)
- [OneAPI Documentation](https://help.zscaler.com/oneapi/understanding-oneapi)
- [Zscaler Identity API](https://help.zscaler.com/zidentity/getting-started-zidentity-api)
- [ZIA API](https://help.zscaler.com/zia/getting-started-zia-api)
- [Automation Docs](http://automation.zsapidocs.net/docs/)
