Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/build-sdk/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ runs:
pip install -r requirements.txt
pip install -e .
if [ "${{ inputs.skip-tests }}" != "true" ]; then
python generic_test.py -v
python validation_test.py -v
fi
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ specs:
clean-specs:
rm -rf ./api-specs

APIS_DIR ?= api-specs/idn/src/main/yaml/apis
APIS_DIR ?= api-specs/idn/apis

.PHONY: build
build:
Expand All @@ -16,8 +16,15 @@ build:
build-partition:
node sdk-resources/build-versioned-sdk.js $(APIS_DIR) --partition $(PARTITION)

# Builds only sailpoint/generic from sdk-resources/generic-api.yaml.
# Needs no api-specs checkout.
.PHONY: build-generic
build-generic:
node sdk-resources/build-versioned-sdk.js --generic-only

.PHONY: test
test:
pip install -r requirements.txt
pip install -e .
python generic_test.py -v
python validation_test.py -v
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ The Python SDK allows you to use Python to interact with the SailPoint APIs and

To get started with the Python SDK and learn about its various functionalities, refer to the [documentation](https://developer.sailpoint.com/docs/tools/sdk/python) on <https://developer.sailpoint.com>.

## Calling any API with the generic client

The generic client calls any SailPoint versioned API by its path, including APIs
that do not have a resource-specific client or documentation yet. The path
includes the API version, for example `accounts/v1`. The client uses the same
configuration, authentication and retries as the other clients.

- Each path segment is URL-encoded. Pass the segments unencoded.
- Anything after a `?` in the path is sent as query parameters.
- A PATCH body is a JSON Patch list, sent as `application/json-patch+json`.

```python
from sailpoint import ApiClient, Configuration, GenericApi

with ApiClient(Configuration()) as api_client:
api = GenericApi(api_client)

accounts = api.generic_get(path="accounts/v1", limit=10, filters='sourceId eq "2c91808a"')
api.generic_patch(
path="transforms/v1/2c91808a",
request_body=[{"op": "replace", "path": "/attributes/input", "value": "lower"}],
)
```

## Contributing

The SailPoint Developer Relations Team is always actively working to improve the SailPoint Python SDK to make it as useful as possible to the SailPoint Developer Community.
Expand Down
106 changes: 106 additions & 0 deletions generic_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Offline tests for the generic API client.

The requests go to a local HTTP server, so these tests need no tenant. They
require the SDK to be built first:
node sdk-resources/build-versioned-sdk.js --generic-only
"""

import json
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlsplit

from sailpoint import ApiClient, GenericApi
from sailpoint.configuration import Configuration, ConfigurationParams


class _CapturingHandler(BaseHTTPRequestHandler):
"""Records each request and answers with the server's canned JSON body."""

def _handle(self):
length = int(self.headers.get("Content-Length") or 0)
self.server.captured = {
"method": self.command,
"url": urlsplit(self.path),
"content_type": self.headers.get("Content-Type"),
"auth": self.headers.get("Authorization"),
"body": self.rfile.read(length).decode() if length else "",
}
payload = self.server.response.encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)

do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = _handle

def log_message(self, format, *args):
pass


class TestGenericApi(unittest.TestCase):

def setUp(self):
self.server = HTTPServer(("127.0.0.1", 0), _CapturingHandler)
self.server.response = "{}"
threading.Thread(target=self.server.serve_forever, daemon=True).start()

params = ConfigurationParams()
params.base_url = f"http://127.0.0.1:{self.server.server_port}"
params.access_token = "token-1"
self.api = GenericApi(ApiClient(Configuration(params)))

def tearDown(self):
self.server.shutdown()
self.server.server_close()

def test_keeps_slashes_and_adds_no_version(self):
self.server.response = '[{"id": "1"}]'

result = self.api.generic_get(
path="/entitlements/v1/identities/2c91808a/entitlements", limit=10, filters='name eq "x"'
)

captured = self.server.captured
self.assertEqual(captured["url"].path, "/entitlements/v1/identities/2c91808a/entitlements")
query = parse_qs(captured["url"].query)
self.assertEqual(query["limit"], ["10"])
self.assertEqual(query["filters"], ['name eq "x"'])
self.assertEqual(captured["auth"], "Bearer token-1")
self.assertEqual(result, [{"id": "1"}])

def test_encodes_segments_and_keeps_query_from_path(self):
self.api.generic_get(path="accounts/v1/a b?detailLevel=SLIM", limit=5)

captured = self.server.captured
self.assertEqual(captured["url"].path, "/accounts/v1/a%20b")
query = parse_qs(captured["url"].query)
self.assertEqual(query["detailLevel"], ["SLIM"])
self.assertEqual(query["limit"], ["5"])

def test_patch_sends_json_patch_list(self):
patch = [{"op": "replace", "path": "/description", "value": "New"}]

self.api.generic_patch(path="transforms/v1/1234", request_body=patch)

captured = self.server.captured
self.assertEqual(captured["method"], "PATCH")
self.assertEqual(captured["content_type"], "application/json-patch+json")
self.assertEqual(json.loads(captured["body"]), patch)

def test_post_sends_json_body_to_v2_path(self):
self.server.response = '{"id": "1"}'

result = self.api.generic_post(path="sources/v2/2c91808a/provisioning-policies", request_body={"name": "Test", "type": "lower"})

captured = self.server.captured
self.assertEqual(captured["method"], "POST")
self.assertEqual(captured["url"].path, "/sources/v2/2c91808a/provisioning-policies")
self.assertEqual(json.loads(captured["body"]), {"name": "Test", "type": "lower"})
self.assertEqual(result, {"id": "1"})


if __name__ == "__main__":
unittest.main()
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"description": "Build tooling for the SailPoint Python SDK",
"scripts": {
"build": "node sdk-resources/build-versioned-sdk.js"
"build": "node sdk-resources/build-versioned-sdk.js",
"build:generic": "node sdk-resources/build-versioned-sdk.js --generic-only"
},
"dependencies": {
"@redocly/cli": "^2.32.2"
Expand Down
5 changes: 5 additions & 0 deletions sailpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"DimensionsApi": (("sailpoint.dimensions.api.dimensions_api", "DimensionsApi"),),
"EntitlementConnectionsApi": (("sailpoint.entitlement_connections.api.entitlement_connections_api", "EntitlementConnectionsApi"),),
"EntitlementsApi": (("sailpoint.entitlements.api.entitlements_api", "EntitlementsApi"),),
"GenericApi": (("sailpoint.generic.api.generic_api", "GenericApi"),),
"GenericRequestApprovalConfigApi": (("sailpoint.generic_request_approval_config.api.generic_request_approval_config_api", "GenericRequestApprovalConfigApi"),),
"GlobalTenantSecuritySettingsApi": (("sailpoint.global_tenant_security_settings.api.global_tenant_security_settings_api", "GlobalTenantSecuritySettingsApi"),),
"NextGenerationCertificationCampaignsApi": (("sailpoint.gov_certification.api.next_generation_certification_campaigns_api", "NextGenerationCertificationCampaignsApi"),),
Expand Down Expand Up @@ -237,6 +238,7 @@
"DimensionsApi",
"EntitlementConnectionsApi",
"EntitlementsApi",
"GenericApi",
"GenericRequestApprovalConfigApi",
"GlobalTenantSecuritySettingsApi",
"NextGenerationCertificationCampaignsApi",
Expand Down Expand Up @@ -431,6 +433,7 @@ def __dir__(cls) -> List[str]:
from sailpoint.dimensions.api.dimensions_api import DimensionsApi as _dimensions__DimensionsApi
from sailpoint.entitlement_connections.api.entitlement_connections_api import EntitlementConnectionsApi as _entitlement_connections__EntitlementConnectionsApi
from sailpoint.entitlements.api.entitlements_api import EntitlementsApi as _entitlements__EntitlementsApi
from sailpoint.generic.api.generic_api import GenericApi as _generic__GenericApi
from sailpoint.generic_request_approval_config.api.generic_request_approval_config_api import GenericRequestApprovalConfigApi as _generic_request_approval_config__GenericRequestApprovalConfigApi
from sailpoint.global_tenant_security_settings.api.global_tenant_security_settings_api import GlobalTenantSecuritySettingsApi as _global_tenant_security_settings__GlobalTenantSecuritySettingsApi
from sailpoint.gov_certification.api.next_generation_certification_campaigns_api import NextGenerationCertificationCampaignsApi as _gov_certification__NextGenerationCertificationCampaignsApi
Expand Down Expand Up @@ -597,6 +600,7 @@ class _WorkflowsApiCombined(_workflows__WorkflowsApi, _nerm__WorkflowsApi): ...
DimensionsApi = _dimensions__DimensionsApi
EntitlementConnectionsApi = _entitlement_connections__EntitlementConnectionsApi
EntitlementsApi = _entitlements__EntitlementsApi
GenericApi = _generic__GenericApi
GenericRequestApprovalConfigApi = _generic_request_approval_config__GenericRequestApprovalConfigApi
GlobalTenantSecuritySettingsApi = _global_tenant_security_settings__GlobalTenantSecuritySettingsApi
NextGenerationCertificationCampaignsApi = _gov_certification__NextGenerationCertificationCampaignsApi
Expand Down Expand Up @@ -758,6 +762,7 @@ class SailPoint:
DimensionsApi = _dimensions__DimensionsApi
EntitlementConnectionsApi = _entitlement_connections__EntitlementConnectionsApi
EntitlementsApi = _entitlements__EntitlementsApi
GenericApi = _generic__GenericApi
GenericRequestApprovalConfigApi = _generic_request_approval_config__GenericRequestApprovalConfigApi
GlobalTenantSecuritySettingsApi = _global_tenant_security_settings__GlobalTenantSecuritySettingsApi
NextGenerationCertificationCampaignsApi = _gov_certification__NextGenerationCertificationCampaignsApi
Expand Down
33 changes: 33 additions & 0 deletions sailpoint/generic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# coding: utf-8

# flake8: noqa

"""
SailPoint Identity Security API - Generic

Generic client for the SailPoint Identity Security versioned APIs. Use it to call any endpoint, in any version, by its path, for example `sources/v1` or `sources/v2/{sourceId}/provisioning-policies`, including endpoints that do not have a resource-specific client or documentation yet. The version is part of the path, so this client is not tied to one API version and the base URL has no version segment.

The version of the OpenAPI document: any (set in the path)
Generated by OpenAPI Generator (https://openapi-generator.tech)

Do not edit the class manually.
""" # noqa: E501


__version__ = "1.0.0"

# import apis into sdk package
from sailpoint.generic.api.generic_api import GenericApi

# import ApiClient
from sailpoint.generic.api_response import ApiResponse
from sailpoint.generic.api_client import ApiClient
from sailpoint.generic.configuration import Configuration
from sailpoint.generic.exceptions import OpenApiException
from sailpoint.generic.exceptions import ApiTypeError
from sailpoint.generic.exceptions import ApiValueError
from sailpoint.generic.exceptions import ApiKeyError
from sailpoint.generic.exceptions import ApiAttributeError
from sailpoint.generic.exceptions import ApiException

# import models into sdk package
5 changes: 5 additions & 0 deletions sailpoint/generic/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# flake8: noqa

# import apis into api package
from sailpoint.generic.api.generic_api import GenericApi

Loading
Loading