|
| 1 | +# Release Notes: Python SDK V3.0.0 - OpenAPI 3.0 Migration |
| 2 | + |
| 3 | +**Date:** 2025-10-25 |
| 4 | + |
| 5 | +## Summary |
| 6 | + |
| 7 | +This is a major release of our Python SDK, introducing support for OpenAPI Specification 3.0. This update allows for more robust, flexible, and descriptive API integrations. |
| 8 | + |
| 9 | +**IMPORTANT:** This version introduces significant breaking changes as we are discontinuing support for OpenAPI 2.0 (formerly Swagger). All users will need to update their integration code to accommodate the changes outlined below. |
| 10 | + |
| 11 | +## New Features |
| 12 | + |
| 13 | +Our migration from a legacy Swagger-based generator to the modern OpenAPI Generator unlocks a new level of functionality and developer experience. |
| 14 | + |
| 15 | + * **Strongly-Typed Data Models:** By integrating Pydantic, all API models now offer robust data validation and type enforcement at runtime, reducing bugs and improving code clarity. |
| 16 | + * **Auto-Generated Documentation:** The SDK now includes a `docs/` directory with comprehensive, automatically generated documentation for all API endpoints and models, ensuring the documentation is always in sync with the code. |
| 17 | + * **Full OpenAPI 3.0 Feature Support:** The SDK now fully supports key OAS 3.0 features, providing a more flexible and powerful integration experience: |
| 18 | + * **Multiple Server Support:** Dynamically switch between server environments (e.g., production, staging). |
| 19 | + * **Enhanced Request Body Support:** Use complex request bodies with multiple media types. |
| 20 | + * **Callback Support:** Handle asynchronous operations. |
| 21 | + |
| 22 | +## Improvements |
| 23 | + |
| 24 | +This architectural overhaul provides significant long-term benefits for the stability and usability of the SDK. |
| 25 | + |
| 26 | + * **Faster, More Frequent Updates:** By adopting the `openapi-generator` and a centralized API specification (`management.yaml`), we can now update the SDK much more frequently. This ensures the SDK remains in close alignment with our production APIs, giving you faster access to new features. |
| 27 | + * **Enhanced Developer Experience:** Stricter typing with Pydantic provides better IDE support, including more accurate autocompletion and inline error checking, making development faster and more efficient. |
| 28 | + * **Improved Code & API Consistency:** The SDK's code is now a direct, machine-readable translation of the official API spec. This guarantees that function names, models, and parameters are always consistent with the API documentation. |
| 29 | + * **Robust Error Handling:** With a wider range of specific exception classes, you can now write more precise error-handling logic to manage different failure scenarios. |
| 30 | + |
| 31 | +## Breaking Changes |
| 32 | + |
| 33 | +This release introduces fundamental changes to the SDK's architecture, dependencies, and project structure. Please review the following points carefully to migrate your application. |
| 34 | + |
| 35 | +1. **Core Dependencies: Pydantic & Typing** |
| 36 | + The SDK now leverages the Pydantic and `typing` libraries to enforce stricter data type binding for function parameters and return types. This improves code reliability and provides a better developer experience with more explicit type checking. |
| 37 | +2. **Model Class Implementation** |
| 38 | + Model classes have been refactored to use Pydantic's `BaseModel` instead of the previous `OktaObject`. |
| 39 | + * **Inheritance:** Models now inherit from `pydantic.BaseModel`. |
| 40 | + * **Initialization:** The `__init__` method for value assignment has been removed. Model properties are now declared directly in the class with expected data types, making them required or optional and allowing default values. This ensures type validation upon object creation. |
| 41 | + * **New Methods:** Models now include several new utility methods for data handling: `to_str()`, `to_json()`, `from_json()`, `to_dict()`, and `from_dict()`. |
| 42 | +3. **Directory and File Structure** |
| 43 | + The project's directory structure has been significantly reorganized. |
| 44 | + * **API Directory (`okta/api`):** |
| 45 | + * Previous: API endpoint files were located in `okta/resource_clients/`. |
| 46 | + * Current: All feature API files are now located in the `okta/api/` directory. |
| 47 | + * **OpenAPI Generation Directory (`openapi/`):** |
| 48 | + * **Generator Tool:** We have migrated from `okta-sdk-generator` to the standard `openapi-generator`. |
| 49 | + * **Templating:** Templates now use Mustache instead of Handlebars. |
| 50 | + * **Configuration (`config.yaml`):** A new `config.yaml` file controls the SDK generation process. It defines: |
| 51 | + * `packageName`: The directory name under which the SDK will be generated. |
| 52 | + * `outputDir`: The path in your repo where the package will be generated. |
| 53 | + * `templateDir`: The path to the template directory location. |
| 54 | + * `files`: Details about custom code templates and their corresponding output Python filenames. |
| 55 | + * `modelPackage`: The name for your models directory. |
| 56 | + * `apiPackage`: The name for your APIs directory. |
| 57 | + * `additionalProperties`: A section to declare custom data objects which can be used in Mustache templates. |
| 58 | + * **Spec File (`management.yaml`):** This new file contains all API definitions, schemas, and properties used by `openapi-generator`. |
| 59 | + * **Tool Versioning (`openapitools.json`):** Replaces `package.json` for managing the `openapi-generator` version. |
| 60 | + * **Generation Scripts:** |
| 61 | + * `regen.sh` has been replaced by `generate.sh`, which now orchestrates the `openapi-generator`. |
| 62 | + * `update_config.py`: A Python script to generate pre-processing custom data objects, append them to `config.yaml`, and make them available for use in Mustache templates. |
| 63 | + * **Test Directory (`tests/`):** |
| 64 | + * Existing integration tests have been fixed and updated. |
| 65 | + * New integration tests have been added to provide coverage for the new API files. |
| 66 | + * Unit tests have been temporarily removed and will be reintroduced in future updates. |
| 67 | + * **New Docs Directory (`docs/`):** |
| 68 | + * This new directory contains comprehensive documentation for all model schemas and APIs generated by the `management.yaml` specification. |
| 69 | +4. **API File Logic** |
| 70 | + The internal logic and structure of API endpoint files have been standardized. |
| 71 | + * **Function Naming:** Function names are now generated directly from the `operationId` in the `management.yaml` spec (e.g., `operationId: createApplication` becomes `def create_application(...)`). |
| 72 | + * **Serialization:** Each API operation now has a corresponding `serialize` function responsible for validating and processing all input parameters. |
| 73 | + * **Execution Flow:** The flow within each API operation function is now as follows: |
| 74 | + 1. A `response_type` dictionary is declared to map status codes to return types. |
| 75 | + 2. The `serialize` function is called to parse inputs and return request components (`method`, `url`, `header_params`, etc.). |
| 76 | + 3. A `request` object is created via `request_executor.create_request`. |
| 77 | + 4. The API is called via `request_executor.execute`. |
| 78 | + 5. The response is processed: |
| 79 | + * If the response is empty or the status code is 204, it will return (`None`, `response`, `None`). |
| 80 | + * If an error is present, the error is returned. |
| 81 | + * Otherwise, the response is converted using the `RESTResponse` class and then deserialized via the API client's `response_deserialize` method. |
| 82 | +5. **API Client (`api_client.py`)** |
| 83 | + The `api_client.py` module has expanded responsibilities beyond being a simple base class. |
| 84 | + * **Initialization:** The `__init__` method now accepts a configuration object and sets up parameters for API calls (e.g., Authorization, private key, org URL). |
| 85 | + * **Parameter Serialization (`param_serialize`):** A new method that validates and assembles all parts of a request, including URL, path/query params, headers, body, and authentication settings. It has supporting methods such as `update_params_for_auth`, `_apply_auth_params`, `parameters_to_tuples`, and `parameters_to_url_query`. |
| 86 | + * **Response Deserialization (`response_deserialize`):** A new method for formatting and handling the API response. It has supporting methods such as `deserialize`, `__deserialize_object`, and `__deserialize_model`. |
| 87 | +6. **Exception Handling (`exceptions/`)** |
| 88 | + The `exceptions.py` file has been expanded to include a wider range of specific exceptions, such as `OpenApi Exception`, `ApiTypeError`, and `ApiValueError`, in addition to the existing `OktaAPIException`. |
| 89 | +7. **New and Modified Modules** |
| 90 | + * **`configuration.py` (New):** This new file defines a `Configuration` class that holds all configuration parameters, which is initialized and used by the `api_client`. |
| 91 | + * **`rest.py` (New):** This module introduces the `RESTResponse` class, used to wrap raw API responses into a standardized object before final processing. |
| 92 | + * **`request_executor.py` (Modified):** The `execute` method now returns the raw `response`, `response_body`, and `error`, as response formatting is now handled by the `api_client`'s `response_deserialize` method. |
| 93 | + * Previous: `return (OktaAPIResponse (...), error)` |
| 94 | + * New: `return response, response_body, error` |
| 95 | + * **`client.py` (Modified):** No major logical changes. It still handles configuration setup and `request_executor` initialization. It now calls `super().__init__` to pass the configuration object to the `api_client` base class. |
| 96 | + * **`api_response.py` (Deprecated):** The `OktaAPIResponse` class is no longer used for response handling. The file is kept for backward compatibility purposes if users wish to reintegrate it manually. |
| 97 | + |
| 98 | +## Migration Guide |
| 99 | + |
| 100 | +We recommend the following step-by-step process for a smooth migration. |
| 101 | + |
| 102 | +### Step 1: Update SDK Version |
| 103 | + |
| 104 | +First, update your `requirements.txt` or `pyproject.toml` to use the new SDK version: |
| 105 | + |
| 106 | +```python |
| 107 | +okta-sdk-python>=3.0.0 |
| 108 | +``` |
| 109 | + |
| 110 | +### Step 2: No change to how client initialization process. |
| 111 | + |
| 112 | +### Step 3: Update Your Import Paths |
| 113 | + |
| 114 | +Due to the directory structure changes, you will need to update your import statements. |
| 115 | + |
| 116 | + * API clients are now in `okta.api` (e.g., `from okta.api.user_api import UserApi`). |
| 117 | + * Models are now in `okta.models` (e.g., `from okta.models.user import User`). |
| 118 | + |
| 119 | +### Step 4: Refactor Model Usage |
| 120 | + |
| 121 | +Models are now Pydantic `BaseModel` objects and are instantiated with keyword arguments instead of an `__init__` method. |
| 122 | + |
| 123 | +**Before:** |
| 124 | + |
| 125 | +```python |
| 126 | +# Old OktaObject model |
| 127 | +user_profile = { "firstName": "John", "lastName": "Doe" } |
| 128 | +new_user = User(profile=user_profile) |
| 129 | +``` |
| 130 | + |
| 131 | +**After:** |
| 132 | + |
| 133 | +```python |
| 134 | +# New Pydantic model |
| 135 | +from okta import UserProfile, PasswordCredential, \ |
| 136 | +CreateUserRequest, UserNextLogin |
| 137 | + |
| 138 | +# Instantiate nested models directly with keyword arguments |
| 139 | +profile = UserProfile(firstName="John", lastName="Doe") |
| 140 | +create_request = CreateUserRequest(**{"profile":profile}) |
| 141 | +``` |
| 142 | + |
| 143 | +### Step 5: Refactor API Method Calls |
| 144 | + |
| 145 | +This is the most significant change. Function names are now based on the `operationId` from the API spec, and parameters (especially request bodies) are passed as typed model objects. |
| 146 | + |
| 147 | +**Before:** |
| 148 | + |
| 149 | +```python |
| 150 | +# Old method name and dictionary-based body |
| 151 | +user_profile = { "firstName": "John", "lastName": "Doe" } |
| 152 | +created_user, response, error await client.create_user( |
| 153 | + {"profile": user_profile}, activate=True |
| 154 | +) |
| 155 | +``` |
| 156 | + |
| 157 | +**After:** |
| 158 | + |
| 159 | +```python |
| 160 | +# New operationId-based method name and Pydantic model for the body |
| 161 | +from okta.api.user_api import UserApi |
| 162 | +from okta import UserProfile, PasswordCredential, \ |
| 163 | +CreateUserRequest, UserNextLogin |
| 164 | + |
| 165 | +user_api = UserApi(client.api_client) |
| 166 | +profile = UserProfile(firstName="John", lastName="Doe") |
| 167 | +create_request = CreateUserRequest(**{"profile":profile}) |
| 168 | +# Call the new method with the typed request object |
| 169 | +created_user await user_api.create_user( |
| 170 | + create_user_request=create_request, activate=True |
| 171 | +) |
| 172 | +``` |
| 173 | + |
| 174 | +### Step 6: Update Exception Handling |
| 175 | + |
| 176 | +Review your `try...except` blocks to catch the new, more specific exceptions for better error handling. |
| 177 | + |
| 178 | +**Before:** |
| 179 | + |
| 180 | +```python |
| 181 | +from okta.exceptions import OktaAPIException |
| 182 | + |
| 183 | +try: |
| 184 | + # API call |
| 185 | +except OktaAPIException as e: |
| 186 | + print(e) |
| 187 | +``` |
| 188 | + |
| 189 | +**After:** |
| 190 | + |
| 191 | +```python |
| 192 | +from okta.exceptions.exceptions import ApiValueError, ApiException |
| 193 | + |
| 194 | +try: |
| 195 | + #API call |
| 196 | +except ApiValueError as e: |
| 197 | + print(f"Validation Error: {e}") |
| 198 | +except ApiException as e: |
| 199 | + print(f"Generic API Error: {e}") |
| 200 | +``` |
| 201 | + |
| 202 | +### Step 7: Test Thoroughly |
| 203 | + |
| 204 | +Finally, execute your test suite against a non-production environment. Pay close attention to `ApiValueError` exceptions, as they will indicate where data types or required fields in your models do not match the new, stricter validation rules. |
| 205 | + |
| 206 | +## Example: Create a User |
| 207 | + |
| 208 | +**Before:** |
| 209 | + |
| 210 | +```python |
| 211 | +from okta.client import Client as OktaClient |
| 212 | + |
| 213 | +config = { |
| 214 | + 'orgUrl': 'https://{your_org}.okta.com', |
| 215 | + 'token': 'YOUR_API_TOKEN', |
| 216 | +} |
| 217 | + |
| 218 | +async def main(): |
| 219 | + client = OktaClient() |
| 220 | + # create user with custom attribute |
| 221 | + body = { |
| 222 | + "profile": { |
| 223 | + "firstName": "John", |
| 224 | + "lastName": "Smith", |
| 225 | + |
| 226 | + |
| 227 | + "customAttr": "custom value" |
| 228 | + }, |
| 229 | + "credentials": { |
| 230 | + "password": { "value": "Knock knock*neo*111" } |
| 231 | + } |
| 232 | + } |
| 233 | + result = await client.create_user(body) |
| 234 | + |
| 235 | + #create user without custom attribute |
| 236 | + body = { |
| 237 | + "profile": { |
| 238 | + "firstName": "Neo", |
| 239 | + "lastName": "Anderson", |
| 240 | + |
| 241 | + |
| 242 | + }, |
| 243 | + "credentials": { |
| 244 | + "password": { "value": "Knock*knock*neo*111" } |
| 245 | + } |
| 246 | + } |
| 247 | + result = await client.create_user(body) |
| 248 | + |
| 249 | + users, resp, err await client.list_users() |
| 250 | + for user in users: |
| 251 | + print(user.profile.first_name, user.profile.last_name) |
| 252 | + try: |
| 253 | + print(user.profile.customAttr) |
| 254 | + except: |
| 255 | + print('User has no customAttr') |
| 256 | + |
| 257 | +loop = asyncio.get_event_loop() |
| 258 | +loop.run_until_complete(main()) |
| 259 | +``` |
| 260 | + |
| 261 | +**After:** |
| 262 | + |
| 263 | +```python |
| 264 | +import asyncio |
| 265 | +from okta import UserProfile, PasswordCredential, \ |
| 266 | +CreateUserRequest, UserNextLogin, UserCredentials |
| 267 | +from okta.client import Client as OktaClient |
| 268 | + |
| 269 | +config = { |
| 270 | + 'orgUrl': 'https://{your_org}.okta.com', |
| 271 | + 'token': 'YOUR_API_TOKEN', |
| 272 | +} |
| 273 | +okta_client = OktaClient(config) |
| 274 | + |
| 275 | +user_config = { |
| 276 | + "firstName": "Sample12", |
| 277 | + "lastName": "Sample12", |
| 278 | + |
| 279 | + |
| 280 | + "mobilePhone": "555-415-1337" |
| 281 | +} |
| 282 | +user_profile = UserProfile(**user_config) |
| 283 | + |
| 284 | +password_value = { |
| 285 | + "value": "Knock knock*neo*111" |
| 286 | +} |
| 287 | +password_credential = PasswordCredential(**password_value) |
| 288 | + |
| 289 | +user_credentials = { |
| 290 | + "password": password_credential |
| 291 | +} |
| 292 | +user_credentials = UserCredentials(**user_credentials) |
| 293 | + |
| 294 | +create_user_request = { |
| 295 | + "profile": user_profile, |
| 296 | + "credentials": user_credentials, |
| 297 | +} |
| 298 | +user_request = CreateUserRequest(**create_user_request) |
| 299 | + |
| 300 | +async def users(): |
| 301 | + next_login = UserNextLogin(UserNextLogin.CHANGEPASSWORD) |
| 302 | + app, resp, err = await okta_client.create_user(user_request, |
| 303 | + activate=True, provider=False, next_login=next_login) |
| 304 | + print("The response of ApplicationApi->create_application:\n") |
| 305 | + print(app) |
| 306 | + print(resp, err) |
| 307 | + |
| 308 | + users, resp, err = await okta_client.list_users() |
| 309 | + for user in users: |
| 310 | + print(user.profile.first_name, user.profile.last_name) |
| 311 | + try: |
| 312 | + print(user.profile.customAttr) |
| 313 | + except: |
| 314 | + print('User has no customAttr') |
| 315 | +``` |
0 commit comments