anaplan-sdk 0.3.1b1__py3-none-any.whl → 0.4.0a2__py3-none-any.whl

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.
@@ -0,0 +1,80 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from anaplan_sdk._base import _AsyncBaseClient, construct_payload
6
+ from anaplan_sdk.models.flows import Flow, FlowInput, FlowSummary
7
+
8
+
9
+ class _AsyncFlowClient(_AsyncBaseClient):
10
+ def __init__(self, client: httpx.AsyncClient, retry_count: int) -> None:
11
+ self._url = "https://api.cloudworks.anaplan.com/2/0/integrationflows"
12
+ super().__init__(retry_count, client)
13
+
14
+ async def list_flows(self, current_user_only: bool = False) -> list[FlowSummary]:
15
+ """
16
+ List all flows in CloudWorks.
17
+ :param current_user_only: Filters the flows to only those created by the current user.
18
+ :return: A list of FlowSummaries.
19
+ """
20
+ params = {"myIntegrations": 1 if current_user_only else 0}
21
+ return [
22
+ FlowSummary.model_validate(e)
23
+ for e in await self._get_paginated(
24
+ self._url, "integrationFlows", page_size=25, params=params
25
+ )
26
+ ]
27
+
28
+ async def get_flow(self, flow_id: str) -> Flow:
29
+ """
30
+ Get a flow by its ID. This returns the full flow object, including the contained steps and
31
+ continuation behavior.
32
+ :param flow_id: The ID of the flow to get.
33
+ :return: The Flow object.
34
+ """
35
+ return Flow.model_validate((await self._get(f"{self._url}/{flow_id}"))["integrationFlow"])
36
+
37
+ async def run_flow(self, flow_id: str, only_steps: list[str] = None) -> str:
38
+ """
39
+ Run a flow by its ID. Make sure that neither the flow nor any of its contained are running.
40
+ If this is the case, the task will error. Anaplan neither schedules these tasks nor can it
41
+ handle concurrent executions.
42
+ :param flow_id: The ID of the flow to run.
43
+ :param only_steps: A list of step IDs to run. If not provided, only these will be run.
44
+ :return: The ID of the run.
45
+ """
46
+ url = f"{self._url}/{flow_id}/run"
47
+ res = await (
48
+ self._post(url, json={"stepsToRun": only_steps})
49
+ if only_steps
50
+ else self._post_empty(url)
51
+ )
52
+ return res["run"]["id"]
53
+
54
+ async def create_flow(self, flow: FlowInput | dict[str, Any]) -> str:
55
+ """
56
+ Create a new flow in CloudWorks. Be careful not to omit the `depends_on` field. Anaplan
57
+ will accept these values, but an invalid, corrupted flow will be created, as all Flows must
58
+ have at least 2 Steps, and they must always be sequential
59
+ :param flow: The flow to create. This can be a FlowInput object or a dictionary.
60
+ :return: The ID of the created flow.
61
+ """
62
+ res = await self._post(self._url, json=construct_payload(FlowInput, flow))
63
+ return res["integrationFlow"]["integrationFlowId"]
64
+
65
+ async def update_flow(self, flow_id: str, flow: FlowInput | dict[str, Any]) -> None:
66
+ """
67
+ Update a flow in CloudWorks. You must provide the full flow object, partial updates are not
68
+ supported.
69
+ :param flow_id: The ID of the flow to update.
70
+ :param flow: The flow to update. This can be a FlowInput object or a dictionary.
71
+ """
72
+ await self._put(f"{self._url}/{flow_id}", json=construct_payload(FlowInput, flow))
73
+
74
+ async def delete_flow(self, flow_id: str) -> None:
75
+ """
76
+ Delete a flow in CloudWorks. This will not delete its contained steps. This will fail if
77
+ the flow is running or if it has any running steps.
78
+ :param flow_id: The ID of the flow to delete.
79
+ """
80
+ await self._delete(f"{self._url}/{flow_id}")
@@ -1,4 +1,3 @@
1
- import warnings
2
1
  from asyncio import gather
3
2
  from itertools import chain
4
3
  from typing import Any
@@ -16,12 +15,9 @@ from anaplan_sdk.models import (
16
15
  Module,
17
16
  )
18
17
 
19
- warnings.filterwarnings("always", category=DeprecationWarning)
20
-
21
18
 
22
19
  class _AsyncTransactionalClient(_AsyncBaseClient):
23
20
  def __init__(self, client: httpx.AsyncClient, model_id: str, retry_count: int) -> None:
24
- self._client = client
25
21
  self._url = f"https://api.anaplan.com/2/0/models/{model_id}"
26
22
  super().__init__(retry_count, client)
27
23
 
@@ -170,25 +166,3 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
170
166
  """
171
167
  res = await self._post(f"{self._url}/modules/{module_id}/data", json=data)
172
168
  return res if "failures" in res else res["numberOfCellsChanged"]
173
-
174
- async def write_to_module(
175
- self, module_id: int, data: list[dict[str, Any]]
176
- ) -> int | dict[str, Any]:
177
- warnings.warn(
178
- "`write_to_module()` is deprecated and will be removed in a future version. "
179
- "Use `update_module_data()` instead.",
180
- DeprecationWarning,
181
- stacklevel=1,
182
- )
183
- return await self.update_module_data(module_id, data)
184
-
185
- async def add_items_to_list(
186
- self, list_id: int, items: list[dict[str, str | int | dict]]
187
- ) -> InsertionResult:
188
- warnings.warn(
189
- "`add_items_to_list()` is deprecated and will be removed in a future version. "
190
- "Use `insert_list_items()` instead.",
191
- DeprecationWarning,
192
- stacklevel=1,
193
- )
194
- return await self.insert_list_items(list_id, items)
anaplan_sdk/_auth.py CHANGED
@@ -1,17 +1,9 @@
1
- """
2
- Custom Authentication class to pass to httpx alongside some helper functions.
3
- """
4
-
5
1
  import logging
6
2
  import os
7
3
  from base64 import b64encode
4
+ from typing import Callable
8
5
 
9
6
  import httpx
10
- from cryptography.exceptions import InvalidKey, UnsupportedAlgorithm
11
- from cryptography.hazmat.backends import default_backend
12
- from cryptography.hazmat.primitives import hashes, serialization
13
- from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
14
- from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
15
7
 
16
8
  from .exceptions import AnaplanException, InvalidCredentialsException, InvalidPrivateKeyException
17
9
 
@@ -21,12 +13,12 @@ logger = logging.getLogger("anaplan_sdk")
21
13
  class _AnaplanAuth(httpx.Auth):
22
14
  requires_response_body = True
23
15
 
24
- def __init__(self):
25
- self._auth_request = self._build_auth_request()
26
- logger.info("Creating Authentication Token.")
27
- with httpx.Client(timeout=15.0) as client:
28
- res = client.send(self._auth_request)
29
- self._parse_auth_response(res)
16
+ def __init__(self, pre_authed: bool = False):
17
+ if not pre_authed:
18
+ logger.info("Creating Authentication Token.")
19
+ with httpx.Client(timeout=15.0) as client:
20
+ res = client.send(self._build_auth_request())
21
+ self._parse_auth_response(res)
30
22
 
31
23
  def _build_auth_request(self) -> httpx.Request:
32
24
  raise NotImplementedError("Must be implemented in subclass.")
@@ -36,7 +28,7 @@ class _AnaplanAuth(httpx.Auth):
36
28
  response = yield request
37
29
  if response.status_code == 401:
38
30
  logger.info("Token expired, refreshing.")
39
- auth_res = yield self._auth_request
31
+ auth_res = yield self._build_auth_request()
40
32
  self._parse_auth_response(auth_res)
41
33
  request.headers["Authorization"] = f"AnaplanAuthToken {self._token}"
42
34
  yield request
@@ -67,9 +59,14 @@ class AnaplanBasicAuth(_AnaplanAuth):
67
59
  class AnaplanCertAuth(_AnaplanAuth):
68
60
  requires_request_body = True
69
61
 
70
- def __init__(self, certificate: bytes, private_key: RSAPrivateKey):
71
- self._certificate = certificate
72
- self._private_key = private_key
62
+ def __init__(
63
+ self,
64
+ certificate: str | bytes,
65
+ private_key: str | bytes,
66
+ private_key_password: str | bytes | None = None,
67
+ ):
68
+ self.__set_certificate(certificate)
69
+ self.__set_private_key(private_key, private_key_password)
73
70
  super().__init__()
74
71
 
75
72
  def _build_auth_request(self) -> httpx.Request:
@@ -85,6 +82,9 @@ class AnaplanCertAuth(_AnaplanAuth):
85
82
  )
86
83
 
87
84
  def _prep_credentials(self) -> tuple[str, str, str]:
85
+ from cryptography.hazmat.primitives import hashes
86
+ from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
87
+
88
88
  message = os.urandom(150)
89
89
  return (
90
90
  b64encode(self._certificate).decode(),
@@ -92,44 +92,157 @@ class AnaplanCertAuth(_AnaplanAuth):
92
92
  b64encode(self._private_key.sign(message, PKCS1v15(), hashes.SHA512())).decode(),
93
93
  )
94
94
 
95
-
96
- def get_certificate(certificate: str | bytes) -> bytes:
97
- """
98
- Get the certificate from a file or string.
99
- :param certificate: The certificate to use.
100
- :return: The certificate as bytes.
101
- """
102
- if isinstance(certificate, str):
103
- if os.path.isfile(certificate):
104
- with open(certificate, "rb") as f:
105
- return f.read()
106
- return certificate.encode()
107
- return certificate
108
-
109
-
110
- def get_private_key(private_key: str | bytes, private_key_password: str | bytes) -> RSAPrivateKey:
111
- """
112
- Get the private key from a file or string.
113
- :param private_key: The private key to use.
114
- :param private_key_password: The password for the private key.
115
- :return: The private key as an RSAPrivateKey object.
116
- """
117
- try:
118
- if isinstance(private_key, str):
119
- if os.path.isfile(private_key):
120
- with open(private_key, "rb") as f:
121
- data = f.read()
95
+ def __set_certificate(self, certificate: str | bytes) -> None:
96
+ if isinstance(certificate, str):
97
+ if os.path.isfile(certificate):
98
+ with open(certificate, "rb") as f:
99
+ self._certificate = f.read()
122
100
  else:
123
- data = private_key.encode()
101
+ self._certificate = certificate.encode()
124
102
  else:
125
- data = private_key
126
-
127
- password = None
128
- if private_key_password:
129
- if isinstance(private_key_password, str):
130
- password = private_key_password.encode()
103
+ self._certificate = certificate
104
+
105
+ def __set_private_key(
106
+ self, private_key: str | bytes, private_key_password: str | bytes
107
+ ) -> None:
108
+ try:
109
+ from cryptography.exceptions import InvalidKey, UnsupportedAlgorithm
110
+ from cryptography.hazmat.backends import default_backend
111
+ from cryptography.hazmat.primitives import serialization
112
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
113
+ except ImportError as e:
114
+ raise AnaplanException(
115
+ "cryptography is not available. Please install anaplan-sdk with the cert extra "
116
+ "`pip install anaplan-sdk[cert]` or install cryptography separately."
117
+ ) from e
118
+ try:
119
+ if isinstance(private_key, str):
120
+ if os.path.isfile(private_key):
121
+ with open(private_key, "rb") as f:
122
+ data = f.read()
123
+ else:
124
+ data = private_key.encode()
131
125
  else:
132
- password = private_key_password
133
- return serialization.load_pem_private_key(data, password, backend=default_backend())
134
- except (IOError, InvalidKey, UnsupportedAlgorithm) as error:
135
- raise InvalidPrivateKeyException from error
126
+ data = private_key
127
+ password = (
128
+ private_key_password.encode()
129
+ if isinstance(private_key_password, str)
130
+ else private_key_password
131
+ )
132
+ self._private_key: RSAPrivateKey = serialization.load_pem_private_key(
133
+ data, password, backend=default_backend()
134
+ )
135
+ except (IOError, InvalidKey, UnsupportedAlgorithm) as error:
136
+ raise InvalidPrivateKeyException from error
137
+
138
+
139
+ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
140
+ def __init__(
141
+ self,
142
+ client_id: str,
143
+ client_secret: str,
144
+ redirect_uri: str,
145
+ refresh_token: str | None = None,
146
+ scope: str = "openid profile email offline_access",
147
+ on_token_refresh: Callable[[dict[str, str]], None] | None = None,
148
+ ):
149
+ try:
150
+ from oauthlib.oauth2 import WebApplicationClient
151
+ except ImportError as e:
152
+ raise AnaplanException(
153
+ "oauthlib is not available. Please install anaplan-sdk with the oauth extra "
154
+ "`pip install anaplan-sdk[oauth]` or install oauthlib separately."
155
+ ) from e
156
+ self._oauth = WebApplicationClient(
157
+ client_id=client_id, client_secret=client_secret, refresh_token=refresh_token
158
+ )
159
+ self._token_url = "https://us1a.app.anaplan.com/oauth/token"
160
+ self._client_id = client_id
161
+ self._client_secret = client_secret
162
+ self._redirect_uri = redirect_uri
163
+ self._refresh_token = refresh_token
164
+ self._scope = scope
165
+ self._id_token = None
166
+ self._on_token_refresh = on_token_refresh
167
+ if not refresh_token:
168
+ self.__auth_code_flow()
169
+ super().__init__(pre_authed=not refresh_token)
170
+
171
+ def _build_auth_request(self) -> httpx.Request:
172
+ url, headers, body = self._oauth.prepare_refresh_token_request(
173
+ token_url=self._token_url,
174
+ refresh_token=self._refresh_token,
175
+ client_secret=self._client_secret,
176
+ client_id=self._client_id,
177
+ )
178
+ return httpx.Request(method="post", url=url, headers=headers, content=body)
179
+
180
+ def _parse_auth_response(self, response: httpx.Response) -> None:
181
+ if response.status_code == 401:
182
+ raise InvalidCredentialsException
183
+ if not response.is_success:
184
+ raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
185
+ token = response.json()
186
+ self._token = token["access_token"]
187
+ self._refresh_token = token["refresh_token"]
188
+ if self._on_token_refresh:
189
+ self._on_token_refresh(token)
190
+ self._id_token = token.get("id_token")
191
+
192
+ def __auth_code_flow(self):
193
+ from oauthlib.oauth2 import OAuth2Error
194
+
195
+ try:
196
+ logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
197
+ url, _, _ = self._oauth.prepare_authorization_request(
198
+ "https://us1a.app.anaplan.com/auth/authorize",
199
+ redirect_url=self._redirect_uri,
200
+ scope=self._scope,
201
+ )
202
+ authorization_response = input(
203
+ f"Please go to {url} and authorize the app.\n"
204
+ "Then paste the entire redirect URL here: "
205
+ )
206
+ url, headers, body = self._oauth.prepare_token_request(
207
+ token_url=self._token_url,
208
+ redirect_url=self._redirect_uri,
209
+ authorization_response=authorization_response,
210
+ client_secret=self._client_secret,
211
+ )
212
+ self._parse_auth_response(httpx.post(url=url, headers=headers, content=body))
213
+ except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
214
+ raise AnaplanException("Error during OAuth2 authorization flow.") from error
215
+
216
+
217
+ def create_auth(
218
+ user_email: str | None = None,
219
+ password: str | None = None,
220
+ certificate: str | bytes | None = None,
221
+ private_key: str | bytes | None = None,
222
+ private_key_password: str | bytes | None = None,
223
+ client_id: str | None = None,
224
+ client_secret: str | None = None,
225
+ redirect_uri: str | None = None,
226
+ refresh_token: str | None = None,
227
+ oauth2_scope: str = "openid profile email offline_access",
228
+ on_token_refresh: Callable[[dict[str, str]], None] | None = None,
229
+ ) -> _AnaplanAuth:
230
+ if certificate and private_key:
231
+ return AnaplanCertAuth(certificate, private_key, private_key_password)
232
+ if user_email and password:
233
+ return AnaplanBasicAuth(user_email=user_email, password=password)
234
+ if client_id and client_secret and redirect_uri:
235
+ return AnaplanOauth2AuthCodeAuth(
236
+ client_id=client_id,
237
+ client_secret=client_secret,
238
+ redirect_uri=redirect_uri,
239
+ refresh_token=refresh_token,
240
+ scope=oauth2_scope,
241
+ on_token_refresh=on_token_refresh,
242
+ )
243
+ raise ValueError(
244
+ "No valid authentication parameters provided. Please provide either:\n"
245
+ "- user_email and password, or\n"
246
+ "- certificate and private_key, or\n"
247
+ "- client_id, client_secret, and redirect_uri"
248
+ )
anaplan_sdk/_base.py CHANGED
@@ -1,7 +1,3 @@
1
- """
2
- Provides Base Classes for this project.
3
- """
4
-
5
1
  import asyncio
6
2
  import logging
7
3
  import random
@@ -11,19 +7,34 @@ from concurrent.futures import ThreadPoolExecutor
11
7
  from gzip import compress
12
8
  from itertools import chain
13
9
  from math import ceil
14
- from typing import Any, Callable, Coroutine, Iterator, Literal
10
+ from typing import Any, Callable, Coroutine, Iterator, Literal, Type, TypeVar
15
11
 
16
12
  import httpx
17
13
  from httpx import HTTPError, Response
18
14
 
19
- from anaplan_sdk.exceptions import (
15
+ from .exceptions import (
20
16
  AnaplanException,
21
17
  AnaplanTimeoutException,
22
18
  InvalidIdentifierException,
23
19
  )
20
+ from .models import AnaplanModel
21
+ from .models.cloud_works import (
22
+ AmazonS3ConnectionInput,
23
+ AzureBlobConnectionInput,
24
+ ConnectionBody,
25
+ GoogleBigQueryConnectionInput,
26
+ IntegrationInput,
27
+ IntegrationProcessInput,
28
+ ScheduleInput,
29
+ )
24
30
 
25
31
  logger = logging.getLogger("anaplan_sdk")
26
32
 
33
+ _json_header = {"Content-Type": "application/json"}
34
+ _gzip_header = {"Content-Type": "application/x-gzip"}
35
+
36
+ T = TypeVar("T", bound=AnaplanModel)
37
+
27
38
 
28
39
  class _BaseClient:
29
40
  def __init__(self, retry_count: int, client: httpx.Client):
@@ -37,27 +48,34 @@ class _BaseClient:
37
48
  return self._run_with_retry(self._client.get, url).content
38
49
 
39
50
  def _post(self, url: str, json: dict | list) -> dict[str, Any]:
40
- return self._run_with_retry(
41
- self._client.post, url, headers={"Content-Type": "application/json"}, json=json
51
+ return self._run_with_retry(self._client.post, url, headers=_json_header, json=json).json()
52
+
53
+ def _put(self, url: str, json: dict | list) -> dict[str, Any]:
54
+ return (self._run_with_retry(self._client.put, url, headers=_json_header, json=json)).json()
55
+
56
+ def _patch(self, url: str, json: dict | list) -> dict[str, Any]:
57
+ return (
58
+ self._run_with_retry(self._client.patch, url, headers=_json_header, json=json)
42
59
  ).json()
43
60
 
44
- def _post_empty(self, url: str) -> None:
45
- self._run_with_retry(self._client.post, url)
61
+ def _delete(self, url: str) -> dict[str, Any]:
62
+ return (self._run_with_retry(self._client.delete, url, headers=_json_header)).json()
63
+
64
+ def _post_empty(self, url: str) -> dict[str, Any]:
65
+ res = self._run_with_retry(self._client.post, url)
66
+ return res.json() if res.num_bytes_downloaded > 0 else {}
46
67
 
47
68
  def _put_binary_gzip(self, url: str, content: bytes) -> Response:
48
69
  return self._run_with_retry(
49
- self._client.put,
50
- url,
51
- headers={"Content-Type": "application/x-gzip"},
52
- content=compress(content),
70
+ self._client.put, url, headers=_gzip_header, content=compress(content)
53
71
  )
54
72
 
55
73
  def __get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
56
- kwargs["params"] = kwargs.get("params", {}) | {"limit": limit, "offset": offset}
74
+ kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
57
75
  return self._get(url, **kwargs).get(result_key, [])
58
76
 
59
77
  def __get_first_page(self, url: str, limit: int, result_key: str, **kwargs) -> tuple[list, int]:
60
- kwargs["params"] = kwargs.get("params", {}) | {"limit": limit}
78
+ kwargs["params"] = kwargs.get("params") or {} | {"limit": limit}
61
79
  res = self._get(url, **kwargs)
62
80
  return res.get(result_key, []), res["meta"]["paging"]["totalSize"]
63
81
 
@@ -111,32 +129,41 @@ class _AsyncBaseClient:
111
129
 
112
130
  async def _post(self, url: str, json: dict | list) -> dict[str, Any]:
113
131
  return (
114
- await self._run_with_retry(
115
- self._client.post, url, headers={"Content-Type": "application/json"}, json=json
116
- )
132
+ await self._run_with_retry(self._client.post, url, headers=_json_header, json=json)
133
+ ).json()
134
+
135
+ async def _put(self, url: str, json: dict | list) -> dict[str, Any]:
136
+ return (
137
+ await self._run_with_retry(self._client.put, url, headers=_json_header, json=json)
138
+ ).json()
139
+
140
+ async def _patch(self, url: str, json: dict | list) -> dict[str, Any]:
141
+ return (
142
+ await self._run_with_retry(self._client.patch, url, headers=_json_header, json=json)
117
143
  ).json()
118
144
 
119
- async def _post_empty(self, url: str) -> None:
120
- await self._run_with_retry(self._client.post, url)
145
+ async def _delete(self, url: str) -> dict[str, Any]:
146
+ return (await self._run_with_retry(self._client.delete, url, headers=_json_header)).json()
147
+
148
+ async def _post_empty(self, url: str) -> dict[str, Any]:
149
+ res = await self._run_with_retry(self._client.post, url)
150
+ return res.json() if res.num_bytes_downloaded > 0 else {}
121
151
 
122
152
  async def _put_binary_gzip(self, url: str, content: bytes) -> Response:
123
153
  return await self._run_with_retry(
124
- self._client.put,
125
- url,
126
- headers={"Content-Type": "application/x-gzip"},
127
- content=compress(content),
154
+ self._client.put, url, headers=_gzip_header, content=compress(content)
128
155
  )
129
156
 
130
157
  async def __get_page(
131
158
  self, url: str, limit: int, offset: int, result_key: str, **kwargs
132
159
  ) -> list:
133
- kwargs["params"] = kwargs.get("params", {}) | {"limit": limit, "offset": offset}
160
+ kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
134
161
  return (await self._get(url, **kwargs)).get(result_key, [])
135
162
 
136
163
  async def __get_first_page(
137
164
  self, url: str, limit: int, result_key: str, **kwargs
138
165
  ) -> tuple[list, int]:
139
- kwargs["params"] = kwargs.get("params", {}) | {"limit": limit}
166
+ kwargs["params"] = kwargs.get("params") or {} | {"limit": limit}
140
167
  res = await self._get(url, **kwargs)
141
168
  return res.get(result_key, []), res["meta"]["paging"]["totalSize"]
142
169
 
@@ -178,6 +205,68 @@ class _AsyncBaseClient:
178
205
  raise AnaplanException("Exhausted all retries without a successful response or Error.")
179
206
 
180
207
 
208
+ def construct_payload(model: Type[T], body: T | dict[str, Any]) -> dict[str, Any]:
209
+ """
210
+ Construct a payload for the given model and body.
211
+ :param model: The model class to use for validation.
212
+ :param body: The body to validate and optionally convert to a dictionary.
213
+ :return: A dictionary representation of the validated body.
214
+ """
215
+ if isinstance(body, dict):
216
+ body = model.model_validate(body)
217
+ return body.model_dump(exclude_none=True, by_alias=True)
218
+
219
+
220
+ def connection_body_payload(body: ConnectionBody | dict[str, Any]) -> dict[str, Any]:
221
+ """
222
+ Construct a payload for the given integration body.
223
+ :param body: The body to validate and optionally convert to a dictionary.
224
+ :return: A dictionary representation of the validated body.
225
+ """
226
+ if isinstance(body, dict):
227
+ if "sasToken" in body:
228
+ body = AzureBlobConnectionInput.model_validate(body)
229
+ elif "secretAccessKey" in body:
230
+ body = AmazonS3ConnectionInput.model_validate(body)
231
+ else:
232
+ body = GoogleBigQueryConnectionInput.model_validate(body)
233
+ return body.model_dump(exclude_none=True, by_alias=True)
234
+
235
+
236
+ def integration_payload(
237
+ body: IntegrationInput | IntegrationProcessInput | dict[str, Any],
238
+ ) -> dict[str, Any]:
239
+ """
240
+ Construct a payload for the given integration body.
241
+ :param body: The body to validate and optionally convert to a dictionary.
242
+ :return: A dictionary representation of the validated body.
243
+ """
244
+ if isinstance(body, dict):
245
+ body = (
246
+ IntegrationInput.model_validate(body)
247
+ if "jobs" in body
248
+ else IntegrationProcessInput.model_validate(body)
249
+ )
250
+ return body.model_dump(exclude_none=True, by_alias=True)
251
+
252
+
253
+ def schedule_payload(
254
+ integration_id: str, schedule: ScheduleInput | dict[str, Any]
255
+ ) -> dict[str, Any]:
256
+ """
257
+ Construct a payload for the given integration ID and schedule.
258
+ :param integration_id: The ID of the integration.
259
+ :param schedule: The schedule to validate and optionally convert to a dictionary.
260
+ :return: A dictionary representation of the validated schedule.
261
+ """
262
+ if isinstance(schedule, dict):
263
+ schedule = ScheduleInput.model_validate(schedule)
264
+ return {
265
+ "integrationId": integration_id,
266
+ "schedule": schedule.model_dump(exclude_none=True, by_alias=True),
267
+ }
268
+
269
+
181
270
  def action_url(action_id: int) -> Literal["imports", "exports", "actions", "processes"]:
182
271
  """
183
272
  Determine the type of action based on its identifier.
@@ -1,35 +1,14 @@
1
- import warnings
2
-
3
1
  import httpx
4
2
 
5
3
  from anaplan_sdk._base import _BaseClient
6
- from anaplan_sdk.models import ModelRevision, Revision, SyncTask, User
7
-
8
- warnings.filterwarnings("always", category=DeprecationWarning)
4
+ from anaplan_sdk.models import ModelRevision, Revision, SyncTask
9
5
 
10
6
 
11
7
  class _AlmClient(_BaseClient):
12
8
  def __init__(self, client: httpx.Client, model_id: str, retry_count: int) -> None:
13
- self._client = client
14
9
  self._url = f"https://api.anaplan.com/2/0/models/{model_id}/alm"
15
10
  super().__init__(retry_count, client)
16
11
 
17
- def list_users(self) -> list[User]:
18
- """
19
- Lists all the Users in the authenticated users default tenant.
20
- :return: The List of Users.
21
- """
22
- warnings.warn(
23
- "`list_users()` on the ALM client is deprecated and will be removed in a "
24
- "future version. Use `list_users()` on the Audit client instead.",
25
- DeprecationWarning,
26
- stacklevel=1,
27
- )
28
- return [
29
- User.model_validate(e)
30
- for e in self._get("https://api.anaplan.com/2/0/users").get("users")
31
- ]
32
-
33
12
  def get_syncable_revisions(self, source_model_id: str) -> list[Revision]:
34
13
  """
35
14
  Use this call to return the list of revisions from your source model that can be
@@ -10,22 +10,35 @@ Event = Literal["all", "byok", "user_activity"]
10
10
 
11
11
  class _AuditClient(_BaseClient):
12
12
  def __init__(self, client: httpx.Client, retry_count: int, thread_count: int) -> None:
13
- self._client = client
14
13
  self._limit = 10_000
15
14
  self._thread_count = thread_count
16
15
  self._url = "https://audit.anaplan.com/audit/api/1/events"
17
16
  super().__init__(retry_count, client)
18
17
 
19
- def list_users(self) -> list[User]:
18
+ def list_users(self, search_pattern: str | None = None) -> list[User]:
20
19
  """
21
20
  Lists all the Users in the authenticated users default tenant.
21
+ :param search_pattern: Optional filter for users. When provided, case-insensitive matches
22
+ users with emails containing this string. When None (default), returns all users.
22
23
  :return: The List of Users.
23
24
  """
25
+ params = {"s": search_pattern} if search_pattern else None
24
26
  return [
25
27
  User.model_validate(e)
26
- for e in self._get_paginated("https://api.anaplan.com/2/0/users", "users")
28
+ for e in self._get_paginated(
29
+ "https://api.anaplan.com/2/0/users", "users", params=params
30
+ )
27
31
  ]
28
32
 
33
+ def get_user(self, user_id: str = "me") -> User:
34
+ """
35
+ Retrieves information about the specified user, or the authenticated user if none specified.
36
+ :return: The requested or currently authenticated User.
37
+ """
38
+ return User.model_validate(
39
+ self._get(f"https://api.anaplan.com/2/0/users/{user_id}").get("user")
40
+ )
41
+
29
42
  def get_events(self, days_into_past: int = 30, event_type: Event = "all") -> list:
30
43
  """
31
44
  Get audit events from Anaplan Audit API.