anaplan-sdk 0.5.0a6__py3-none-any.whl → 0.5.1__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.
anaplan_sdk/__init__.py CHANGED
@@ -2,6 +2,7 @@ from ._async_clients import AsyncClient
2
2
  from ._auth import AnaplanLocalOAuth, AnaplanRefreshTokenAuth
3
3
  from ._clients import Client
4
4
  from ._oauth import AsyncOauth, Oauth
5
+ from .models.scim import field
5
6
 
6
7
  __all__ = [
7
8
  "AsyncClient",
@@ -12,4 +13,5 @@ __all__ = [
12
13
  "Oauth",
13
14
  "models",
14
15
  "exceptions",
16
+ "field",
15
17
  ]
@@ -2,6 +2,8 @@ from ._alm import _AsyncAlmClient
2
2
  from ._audit import _AsyncAuditClient
3
3
  from ._bulk import AsyncClient
4
4
  from ._cloud_works import _AsyncCloudWorksClient
5
+ from ._cw_flow import _AsyncFlowClient
6
+ from ._scim import _AsyncScimClient
5
7
  from ._transactional import _AsyncTransactionalClient
6
8
 
7
9
  __all__ = [
@@ -9,5 +11,7 @@ __all__ = [
9
11
  "_AsyncAlmClient",
10
12
  "_AsyncAuditClient",
11
13
  "_AsyncCloudWorksClient",
14
+ "_AsyncFlowClient",
12
15
  "_AsyncTransactionalClient",
16
+ "_AsyncScimClient",
13
17
  ]
@@ -11,7 +11,6 @@ UserSortBy = Literal["first_name", "last_name", "email", "active", "last_login_d
11
11
  class _AsyncAuditClient:
12
12
  def __init__(self, http: _AsyncHttpService) -> None:
13
13
  self._http = http
14
- self._limit = 10_000
15
14
  self._url = "https://audit.anaplan.com/audit/api/1/events"
16
15
 
17
16
  async def get_users(
@@ -26,6 +26,7 @@ from anaplan_sdk.models import (
26
26
  from ._alm import _AsyncAlmClient
27
27
  from ._audit import _AsyncAuditClient
28
28
  from ._cloud_works import _AsyncCloudWorksClient
29
+ from ._scim import _AsyncScimClient
29
30
  from ._transactional import _AsyncTransactionalClient
30
31
 
31
32
  SortBy = Literal["id", "name"] | None
@@ -135,7 +136,8 @@ class AsyncClient:
135
136
  _AsyncTransactionalClient(self._http, model_id) if model_id else None
136
137
  )
137
138
  self._alm_client = _AsyncAlmClient(self._http, model_id) if model_id else None
138
- self._audit = _AsyncAuditClient(self._http)
139
+ self._audit_client = _AsyncAuditClient(self._http)
140
+ self._scim_client = _AsyncScimClient(self._http)
139
141
  self._cloud_works = _AsyncCloudWorksClient(self._http)
140
142
  self.upload_chunk_size = upload_chunk_size
141
143
  self.allow_file_creation = allow_file_creation
@@ -169,7 +171,7 @@ class AsyncClient:
169
171
  The Audit Client provides access to the Anaplan Audit API.
170
172
  For details, see https://vinzenzklass.github.io/anaplan-sdk/guides/audit/.
171
173
  """
172
- return self._audit
174
+ return self._audit_client
173
175
 
174
176
  @property
175
177
  def cw(self) -> _AsyncCloudWorksClient:
@@ -218,6 +220,15 @@ class AsyncClient:
218
220
  )
219
221
  return self._alm_client
220
222
 
223
+ @property
224
+ def scim(self) -> _AsyncScimClient:
225
+ """
226
+ To use the SCIM API, you must be User Admin. The SCIM API allows managing internal users.
227
+ Visiting users are excluded from the SCIM API.
228
+ :return: The SCIM Client.
229
+ """
230
+ return self._scim_client
231
+
221
232
  async def get_workspaces(
222
233
  self,
223
234
  search_pattern: str | None = None,
@@ -0,0 +1,148 @@
1
+ import logging
2
+ from asyncio import gather
3
+ from itertools import chain
4
+ from typing import Any
5
+
6
+ from anaplan_sdk._services import _AsyncHttpService
7
+ from anaplan_sdk._utils import construct_payload
8
+ from anaplan_sdk.models.scim import (
9
+ Operation,
10
+ ReplaceUserInput,
11
+ Resource,
12
+ Schema,
13
+ ServiceProviderConfig,
14
+ User,
15
+ UserInput,
16
+ field,
17
+ )
18
+
19
+ logger = logging.getLogger("anaplan_sdk")
20
+
21
+
22
+ class _AsyncScimClient:
23
+ def __init__(self, http: _AsyncHttpService) -> None:
24
+ self._http = http
25
+ self._url = "https://api.anaplan.com/scim/1/0/v2"
26
+
27
+ async def get_service_provider_config(self) -> ServiceProviderConfig:
28
+ """
29
+ Get the SCIM Service Provider Configuration.
30
+ :return: The ServiceProviderConfig object describing the available SCIM features.
31
+ """
32
+ res = await self._http.get(f"{self._url}/ServiceProviderConfig")
33
+ return ServiceProviderConfig.model_validate(res)
34
+
35
+ async def get_resource_types(self) -> list[Resource]:
36
+ """
37
+ Get the SCIM Resource Types.
38
+ :return: A list of Resource objects describing the SCIM resource types.
39
+ """
40
+ res = await self._http.get(f"{self._url}/ResourceTypes")
41
+ return [Resource.model_validate(e) for e in res.get("Resources", [])]
42
+
43
+ async def get_resource_schemas(self) -> list[Schema]:
44
+ """
45
+ Get the SCIM Resource Schemas.
46
+ :return: A list of Schema objects describing the SCIM resource schemas.
47
+ """
48
+ res = await self._http.get(f"{self._url}/Schemas")
49
+ return [Schema.model_validate(e) for e in res.get("Resources", [])]
50
+
51
+ async def get_users(self, predicate: str | field = None, page_size: int = 100) -> list[User]:
52
+ """
53
+ Get a list of users, optionally filtered by a predicate. Keep in mind that this will only
54
+ return internal users. To get a list of all users in the tenant, use the `get_users()`
55
+ in the `audit` namespace instead.
56
+ :param predicate: A filter predicate to filter the users. This can either be a string,
57
+ in which case it will be passed as-is, or an expression. Anaplan supports filtering
58
+ on the following fields: "id", "externalId", "userName", "name.familyName",
59
+ "name.givenName" and "active". It supports the operators "eq", "ne", "gt", "ge",
60
+ "lt", "le" and "pr". It supports logical operators "and" and "or", "not" is not
61
+ supported. It supports grouping with parentheses.
62
+ :param page_size: The number of users to fetch per page. Values above 100 will error.
63
+ :return: The internal users optionally matching the filter.
64
+ """
65
+ params: dict[str, int | str] = {"startIndex": 1, "count": page_size}
66
+ if predicate is not None:
67
+ _predicate = predicate if isinstance(predicate, str) else str(predicate)
68
+ logger.debug(f"Searching for users with predicate: {_predicate}")
69
+ params["filter"] = _predicate
70
+ res = await self._http.get(f"{self._url}/Users", params=params)
71
+ users = [User.model_validate(e) for e in res.get("Resources", [])]
72
+ if (total := res["totalResults"]) <= page_size:
73
+ return users
74
+ pages = await gather(
75
+ *(
76
+ self._http.get(
77
+ f"{self._url}/Users", params=(params | {"startIndex": i, "count": page_size})
78
+ )
79
+ for i in range(page_size + 1, total + 1, page_size)
80
+ )
81
+ )
82
+ for user in chain(*(p.get("Resources", []) for p in pages)):
83
+ users.append(User.model_validate(user))
84
+ return users
85
+
86
+ async def get_user(self, user_id: str) -> User:
87
+ """
88
+ Get a user by their ID.
89
+ :param user_id: The ID of the user to fetch.
90
+ :return: The User object.
91
+ """
92
+ res = await self._http.get(f"{self._url}/Users/{user_id}")
93
+ return User.model_validate(res)
94
+
95
+ async def add_user(self, user: UserInput | dict[str, Any]) -> User:
96
+ """
97
+ Add a new user to your Anaplan tenant.
98
+ :param user: The user info to add. Can either be a UserInput object or a dict. If you pass
99
+ a dict, it will be validated against the UserInput model before sending. If the info
100
+ you provided is invalid or incomplete, this will raise a pydantic.ValidationError.
101
+ :return: The created User object.
102
+ """
103
+ res = await self._http.post(f"{self._url}/Users", json=construct_payload(UserInput, user))
104
+ user = User.model_validate(res)
105
+ logger.info(f"Added user '{user.user_name}' with ID '{user.id}'.")
106
+ return user
107
+
108
+ async def replace_user(self, user_id: str, user: ReplaceUserInput | dict[str, Any]):
109
+ """
110
+ Replace an existing user with new information. Note that this will replace all fields of the
111
+ :param user_id: ID of the user to replace.
112
+ :param user: The new user info. Can either be a ReplaceUserInput object or a dict. If you
113
+ pass a dict, it will be validated against the ReplaceUserInput model before sending.
114
+ If the info you provided is invalid or incomplete, this will raise a
115
+ pydantic.ValidationError.
116
+ :return: The updated User object.
117
+ """
118
+ res = await self._http.put(
119
+ f"{self._url}/Users/{user_id}", json=construct_payload(ReplaceUserInput, user)
120
+ )
121
+ user = User.model_validate(res)
122
+ logger.info(f"Replaced user with ID '{user_id}' with '{user.user_name}'.")
123
+ return user
124
+
125
+ async def update_user(
126
+ self, user_id: str, operations: list[Operation] | list[dict[str, Any]]
127
+ ) -> User:
128
+ """
129
+ Update an existing user with a list of operations. This allows you to update only specific
130
+ fields of the user without replacing the entire user.
131
+ :param user_id: The ID of the user to update.
132
+ :param operations: A list of operations to perform on the user. Each operation can either be
133
+ an Operation object or a dict. If you pass a dict, it will be validated against
134
+ the Operation model before sending. If the operation is invalid, this will raise a
135
+ pydantic.ValidationError. You can also use the models Replace, Add and Remove which
136
+ are subclasses of Operation and provide a more convenient way to create operations.
137
+ :return: The updated User object.
138
+ """
139
+ res = await self._http.patch(
140
+ f"{self._url}/Users/{user_id}",
141
+ json={
142
+ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
143
+ "Operations": [construct_payload(Operation, e) for e in operations],
144
+ },
145
+ )
146
+ user = User.model_validate(res)
147
+ logger.info(f"Updated user with ID '{user_id}'.")
148
+ return user
@@ -1,6 +1,17 @@
1
1
  from ._alm import _AlmClient
2
2
  from ._audit import _AuditClient
3
3
  from ._bulk import Client
4
+ from ._cloud_works import _CloudWorksClient
5
+ from ._cw_flow import _FlowClient
6
+ from ._scim import _ScimClient
4
7
  from ._transactional import _TransactionalClient
5
8
 
6
- __all__ = ["Client", "_AlmClient", "_AuditClient", "_TransactionalClient"]
9
+ __all__ = [
10
+ "Client",
11
+ "_AlmClient",
12
+ "_AuditClient",
13
+ "_CloudWorksClient",
14
+ "_FlowClient",
15
+ "_ScimClient",
16
+ "_TransactionalClient",
17
+ ]
@@ -27,6 +27,7 @@ from anaplan_sdk.models import (
27
27
  from ._alm import _AlmClient
28
28
  from ._audit import _AuditClient
29
29
  from ._cloud_works import _CloudWorksClient
30
+ from ._scim import _ScimClient
30
31
  from ._transactional import _TransactionalClient
31
32
 
32
33
  SortBy = Literal["id", "name"] | None
@@ -139,9 +140,10 @@ class Client:
139
140
  _TransactionalClient(self._http, model_id) if model_id else None
140
141
  )
141
142
  self._alm_client = _AlmClient(self._http, model_id) if model_id else None
143
+ self._audit_client = _AuditClient(self._http)
144
+ self._scim_client = _ScimClient(self._http)
142
145
  self._cloud_works = _CloudWorksClient(self._http)
143
146
  self._thread_count = multiprocessing.cpu_count()
144
- self._audit = _AuditClient(self._http)
145
147
  self.status_poll_delay = status_poll_delay
146
148
  self.upload_parallel = upload_parallel
147
149
  self.upload_chunk_size = upload_chunk_size
@@ -174,7 +176,7 @@ class Client:
174
176
  The Audit Client provides access to the Anaplan Audit API.
175
177
  For details, see https://vinzenzklass.github.io/anaplan-sdk/guides/audit/.
176
178
  """
177
- return self._audit
179
+ return self._audit_client
178
180
 
179
181
  @property
180
182
  def cw(self) -> _CloudWorksClient:
@@ -223,6 +225,15 @@ class Client:
223
225
  )
224
226
  return self._alm_client
225
227
 
228
+ @property
229
+ def scim(self) -> _ScimClient:
230
+ """
231
+ To use the SCIM API, you must be User Admin. The SCIM API allows managing internal users.
232
+ Visiting users are excluded from the SCIM API.
233
+ :return: The SCIM Client.
234
+ """
235
+ return self._scim_client
236
+
226
237
  def get_workspaces(
227
238
  self,
228
239
  search_pattern: str | None = None,
@@ -0,0 +1,145 @@
1
+ import logging
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from itertools import chain
4
+ from typing import Any
5
+
6
+ from anaplan_sdk._services import _HttpService
7
+ from anaplan_sdk._utils import construct_payload
8
+ from anaplan_sdk.models.scim import (
9
+ Operation,
10
+ ReplaceUserInput,
11
+ Resource,
12
+ Schema,
13
+ ServiceProviderConfig,
14
+ User,
15
+ UserInput,
16
+ field,
17
+ )
18
+
19
+ logger = logging.getLogger("anaplan_sdk")
20
+
21
+
22
+ class _ScimClient:
23
+ def __init__(self, http: _HttpService) -> None:
24
+ self._http = http
25
+ self._url = "https://api.anaplan.com/scim/1/0/v2"
26
+
27
+ def get_service_provider_config(self) -> ServiceProviderConfig:
28
+ """
29
+ Get the SCIM Service Provider Configuration.
30
+ :return: The ServiceProviderConfig object describing the available SCIM features.
31
+ """
32
+ res = self._http.get(f"{self._url}/ServiceProviderConfig")
33
+ return ServiceProviderConfig.model_validate(res)
34
+
35
+ def get_resource_types(self) -> list[Resource]:
36
+ """
37
+ Get the SCIM Resource Types.
38
+ :return: A list of Resource objects describing the SCIM resource types.
39
+ """
40
+ res = self._http.get(f"{self._url}/ResourceTypes")
41
+ return [Resource.model_validate(e) for e in res.get("Resources", [])]
42
+
43
+ def get_resource_schemas(self) -> list[Schema]:
44
+ """
45
+ Get the SCIM Resource Schemas.
46
+ :return: A list of Schema objects describing the SCIM resource schemas.
47
+ """
48
+ res = self._http.get(f"{self._url}/Schemas")
49
+ return [Schema.model_validate(e) for e in res.get("Resources", [])]
50
+
51
+ def get_users(self, predicate: str | field = None, page_size: int = 100) -> list[User]:
52
+ """
53
+ Get a list of users, optionally filtered by a predicate. Keep in mind that this will only
54
+ return internal users. To get a list of all users in the tenant, use the `get_users()`
55
+ in the `audit` namespace instead.
56
+ :param predicate: A filter predicate to filter the users. This can either be a string,
57
+ in which case it will be passed as-is, or an expression. Anaplan supports filtering
58
+ on the following fields: "id", "externalId", "userName", "name.familyName",
59
+ "name.givenName" and "active". It supports the operators "eq", "ne", "gt", "ge",
60
+ "lt", "le" and "pr". It supports logical operators "and" and "or", "not" is not
61
+ supported. It supports grouping with parentheses.
62
+ :param page_size: The number of users to fetch per page. Values above 100 will error.
63
+ :return: The internal users optionally matching the filter.
64
+ """
65
+ params: dict[str, int | str] = {"startIndex": 1, "count": page_size}
66
+ if predicate is not None:
67
+ _predicate = predicate if isinstance(predicate, str) else str(predicate)
68
+ logger.debug(f"Searching for users with predicate: {_predicate}")
69
+ params["filter"] = _predicate
70
+ res = self._http.get(f"{self._url}/Users", params=params)
71
+ users = [User.model_validate(e) for e in res.get("Resources", [])]
72
+ if (total := res["totalResults"]) <= page_size:
73
+ return users
74
+ with ThreadPoolExecutor() as executor:
75
+ pages = executor.map(
76
+ lambda i: self._http.get(
77
+ f"{self._url}/Users", params=(params | {"startIndex": i, "count": page_size})
78
+ ),
79
+ range(page_size + 1, total + 1, page_size),
80
+ )
81
+ for user in chain(*(p.get("Resources", []) for p in pages)):
82
+ users.append(User.model_validate(user))
83
+ return users
84
+
85
+ def get_user(self, user_id: str) -> User:
86
+ """
87
+ Get a user by their ID.
88
+ :param user_id: The ID of the user to fetch.
89
+ :return: The User object.
90
+ """
91
+ res = self._http.get(f"{self._url}/Users/{user_id}")
92
+ return User.model_validate(res)
93
+
94
+ def add_user(self, user: UserInput | dict[str, Any]) -> User:
95
+ """
96
+ Add a new user to your Anaplan tenant.
97
+ :param user: The user info to add. Can either be a UserInput object or a dict. If you pass
98
+ a dict, it will be validated against the UserInput model before sending. If the info
99
+ you provided is invalid or incomplete, this will raise a pydantic.ValidationError.
100
+ :return: The created User object.
101
+ """
102
+ res = self._http.post(f"{self._url}/Users", json=construct_payload(UserInput, user))
103
+ user = User.model_validate(res)
104
+ logger.info(f"Added user '{user.user_name}' with ID '{user.id}'.")
105
+ return user
106
+
107
+ def replace_user(self, user_id: str, user: ReplaceUserInput | dict[str, Any]):
108
+ """
109
+ Replace an existing user with new information. Note that this will replace all fields of the
110
+ :param user_id: ID of the user to replace.
111
+ :param user: The new user info. Can either be a ReplaceUserInput object or a dict. If you
112
+ pass a dict, it will be validated against the ReplaceUserInput model before sending.
113
+ If the info you provided is invalid or incomplete, this will raise a
114
+ pydantic.ValidationError.
115
+ :return: The updated User object.
116
+ """
117
+ res = self._http.put(
118
+ f"{self._url}/Users/{user_id}", json=construct_payload(ReplaceUserInput, user)
119
+ )
120
+ user = User.model_validate(res)
121
+ logger.info(f"Replaced user with ID '{user_id}' with '{user.user_name}'.")
122
+ return user
123
+
124
+ def update_user(self, user_id: str, operations: list[Operation] | list[dict[str, Any]]) -> User:
125
+ """
126
+ Update an existing user with a list of operations. This allows you to update only specific
127
+ fields of the user without replacing the entire user.
128
+ :param user_id: The ID of the user to update.
129
+ :param operations: A list of operations to perform on the user. Each operation can either be
130
+ an Operation object or a dict. If you pass a dict, it will be validated against
131
+ the Operation model before sending. If the operation is invalid, this will raise a
132
+ pydantic.ValidationError. You can also use the models Replace, Add and Remove which
133
+ are subclasses of Operation and provide a more convenient way to create operations.
134
+ :return: The updated User object.
135
+ """
136
+ res = self._http.patch(
137
+ f"{self._url}/Users/{user_id}",
138
+ json={
139
+ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
140
+ "Operations": [construct_payload(Operation, e) for e in operations],
141
+ },
142
+ )
143
+ user = User.model_validate(res)
144
+ logger.info(f"Updated user with ID '{user_id}'.")
145
+ return user
anaplan_sdk/_services.py CHANGED
@@ -257,7 +257,7 @@ def _extract_first_page(
257
257
  actual_page_size = res["meta"]["paging"]["currentPageSize"]
258
258
  if actual_page_size < page_size and not actual_page_size == total_items:
259
259
  logger.warning(
260
- f"Page size {page_size} was silently truncated to {actual_page_size}."
260
+ f"Page size {page_size} was silently truncated to {actual_page_size}. "
261
261
  f"Using the server-side enforced page size {actual_page_size} for further requests."
262
262
  )
263
263
  logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
@@ -0,0 +1,282 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import Field
4
+ from typing_extensions import Self
5
+
6
+ from anaplan_sdk.models import AnaplanModel
7
+
8
+ AnaplanFilterFields = Literal[
9
+ "id", "externalId", "userName", "name.familyName", "name.givenName", "active"
10
+ ]
11
+
12
+
13
+ class _FilterExpression:
14
+ def __init__(self, _field: AnaplanFilterFields) -> None:
15
+ self._field: AnaplanFilterFields = _field
16
+ self._exprs: list[str] = []
17
+ self._operators = []
18
+
19
+ def __str__(self) -> str:
20
+ if not self._exprs:
21
+ return "active eq true" if self._field == "active" else f"{self._field} pr"
22
+ parts = []
23
+ for i, expr in enumerate(self._exprs):
24
+ if i > 0:
25
+ parts.append(self._operators[i - 1])
26
+ parts.append(expr)
27
+ return " ".join(parts)
28
+
29
+ def __invert__(self) -> Self:
30
+ self._exprs.append(
31
+ "active eq false" if self._field == "active" else f"{self._field} eq null"
32
+ )
33
+ return self
34
+
35
+ def __and__(self, other: Self) -> Self:
36
+ return self._combine(other, "and")
37
+
38
+ def __or__(self, other: Self) -> Self:
39
+ return self._combine(other, "or")
40
+
41
+ def _combine(self, other: Self, operator: Literal["and", "or"]) -> Self:
42
+ if self._requires_grouping(operator):
43
+ self._exprs = [f"({str(self)})"]
44
+ self._operators = []
45
+ self._operators.append(operator)
46
+ if not self._exprs:
47
+ self._exprs.append(str(self))
48
+ self._exprs.append(f"({str(other)})" if other._requires_grouping(operator) else str(other))
49
+ return self
50
+
51
+ def _requires_grouping(self, operator: Literal["and", "or"]) -> bool:
52
+ return len(self._operators) > 0 and ("or" in self._operators or operator == "or")
53
+
54
+ def __eq__(self, other: Any) -> Self:
55
+ self._exprs.append(f'{self._field} eq "{other}"')
56
+ return self
57
+
58
+ def __ne__(self, other: Any) -> Self:
59
+ self._exprs.append(f'{self._field} ne "{other}"')
60
+ return self
61
+
62
+ def __gt__(self, other: Any) -> Self:
63
+ self._exprs.append(f'{self._field} gt "{other}"')
64
+ return self
65
+
66
+ def __ge__(self, other: Any) -> Self:
67
+ self._exprs.append(f'{self._field} ge "{other}"')
68
+ return self
69
+
70
+ def __lt__(self, other: Any) -> Self:
71
+ self._exprs.append(f'{self._field} lt "{other}"')
72
+ return self
73
+
74
+ def __le__(self, other: Any) -> Self:
75
+ self._exprs.append(f'{self._field} le "{other}"')
76
+ return self
77
+
78
+
79
+ field = _FilterExpression
80
+
81
+
82
+ class NameInput(AnaplanModel):
83
+ family_name: str = Field(
84
+ description="The family name of the User, or last name in most Western languages"
85
+ )
86
+ given_name: str = Field(
87
+ description="The given name of the User, or first name in most Western languages"
88
+ )
89
+
90
+
91
+ class Name(NameInput):
92
+ formatted: str = Field(
93
+ description=(
94
+ "The formatted full name, including given name and family name. Anaplan does as of now "
95
+ "not have other standard SCIM fields such as middle name or honorific pre- or suffixes."
96
+ )
97
+ )
98
+
99
+
100
+ class Email(AnaplanModel):
101
+ value: str = Field(description="Email address of the User")
102
+ type: Literal["work", "home", "other"] = Field(
103
+ default=None, description="A label indicating the emails's function, e.g., 'work' or 'home'"
104
+ )
105
+ primary: bool | None = Field(
106
+ default=None, description="Indicates if this is the primary or 'preferred' email"
107
+ )
108
+
109
+
110
+ class EntitlementInput(AnaplanModel):
111
+ value: str = Field(description="The value of an entitlement.")
112
+ type: Literal["WORKSPACE", "WORKSPACE_IDS", "WORKSPACE_NAMES"] = Field(
113
+ description="A label indicating the attribute's function."
114
+ )
115
+
116
+
117
+ class Entitlement(EntitlementInput):
118
+ display: str | None = Field(
119
+ default=None, description="A human-readable name, primarily used for display purposes."
120
+ )
121
+ primary: bool | None = Field(
122
+ default=None,
123
+ description="Indicating the 'primary' or preferred attribute value for this attribute.",
124
+ )
125
+
126
+
127
+ class _BaseUser(AnaplanModel):
128
+ schemas: list[str] = Field(default=["urn:ietf:params:scim:schemas:core:2.0:User"])
129
+ user_name: str = Field(description="Unique name for the User.")
130
+
131
+
132
+ class User(_BaseUser):
133
+ id: str = Field(description="The unique identifier for the User.")
134
+ name: Name = Field(description="The user's real name.")
135
+ active: bool = Field(description="Indicating the User's active status.")
136
+ emails: list[Email] = Field(default=[], description="Email addresses for the user.")
137
+ display_name: str = Field(description="Display Name for the User.")
138
+ entitlements: list[Entitlement] = Field(
139
+ default=[], description="A list of entitlements (Workspaces) the User has."
140
+ )
141
+
142
+
143
+ class ReplaceUserInput(_BaseUser):
144
+ id: str = Field(description="The unique identifier for the User.")
145
+ name: NameInput = Field(description="The user's real name.")
146
+ active: bool | None = Field(default=None, description="Indicating the User's active status.")
147
+ display_name: str | None = Field(default=None, description="Display Name for the User.")
148
+ entitlements: list[EntitlementInput] | None = Field(
149
+ default=None, description="A list of entitlements (Workspaces) the User has."
150
+ )
151
+
152
+
153
+ class UserInput(_BaseUser):
154
+ external_id: str = Field(
155
+ description="Your unique id for this user (as stored in your company systems)."
156
+ )
157
+ name: NameInput = Field(description="The user's real name.")
158
+
159
+
160
+ class Meta(AnaplanModel):
161
+ resource_type: str = Field(description="The type of the resource.")
162
+ location: str = Field(description="The URI of the resource.")
163
+
164
+
165
+ class MetaWithDates(Meta):
166
+ created: str = Field(description="The timestamp when the resource was created.")
167
+ last_modified: str = Field(description="The timestamp when the resource was last modified.")
168
+
169
+
170
+ class Supported(AnaplanModel):
171
+ supported: bool = Field(description="Indicates whether the Feature is supported.")
172
+
173
+
174
+ class BulkConfig(Supported):
175
+ max_operations: int = Field(
176
+ description="The maximum number of operations permitted in a single request."
177
+ )
178
+ max_payload_size: int = Field(description="The maximum payload size in bytes.")
179
+
180
+
181
+ class FilterConfig(Supported):
182
+ max_results: int = Field(
183
+ description="The maximum number of results returned from a filtered query."
184
+ )
185
+
186
+
187
+ class AuthenticationScheme(AnaplanModel):
188
+ name: str = Field(description="The name of the authentication scheme.")
189
+ type: str = Field(description="The type of the authentication scheme.")
190
+ description: str = Field(description="A description of the authentication scheme.")
191
+ spec_uri: str = Field(
192
+ description="The URI that points to the specification of the authentication scheme."
193
+ )
194
+ documentation_uri: str = Field(
195
+ description="The URI that points to the documentation of the authentication scheme."
196
+ )
197
+
198
+
199
+ class ServiceProviderConfig(AnaplanModel):
200
+ schemas: list[str] = Field(
201
+ default=["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
202
+ description="Schemas for this resource.",
203
+ )
204
+ meta: MetaWithDates = Field(description="Metadata about the resource.")
205
+ documentation_uri: str = Field(description="URI of the service provider's documentation.")
206
+ patch: Supported = Field(description="Configuration for PATCH operations.")
207
+ bulk: BulkConfig = Field(description="Configuration for bulk operations.")
208
+ filter: FilterConfig = Field(description="Configuration for filtering.")
209
+ change_password: Supported = Field(description="Configuration for password changes.")
210
+ sort: Supported = Field(description="Configuration for sorting.")
211
+ etag: Supported = Field(description="Configuration for ETags.")
212
+ authentication_schemes: list[AuthenticationScheme] = Field(
213
+ description="List of supported authentication schemes."
214
+ )
215
+
216
+
217
+ class Resource(AnaplanModel):
218
+ schemas: list[str] = Field(
219
+ default=["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
220
+ description="Schemas for this resource.",
221
+ )
222
+ meta: Meta = Field(description="Metadata about the resource.")
223
+ id: str = Field(description="The identifier of the resource type.")
224
+ name: str = Field(description="The name of the resource type.")
225
+ endpoint: str = Field(description="The endpoint where resources of this type may be accessed.")
226
+ description: str = Field(description="A description of the resource type.")
227
+
228
+
229
+ class Attribute(AnaplanModel):
230
+ name: str = Field(description="The name of the attribute.")
231
+ type: str = Field(description="The data type of the attribute.")
232
+ multi_valued: bool = Field(description="Indicates if the attribute can have multiple values.")
233
+ description: str = Field(description="A human-readable description of the attribute.")
234
+ required: bool = Field(description="Indicates if the attribute is required.")
235
+ case_exact: bool = Field(
236
+ description="Indicates if case sensitivity should be considered when comparing values."
237
+ )
238
+ mutability: str = Field(description="Indicates if and how the attribute can be modified.")
239
+ returned: str = Field(
240
+ description="Indicates when the attribute's values are returned in a response."
241
+ )
242
+ uniqueness: str = Field(
243
+ description="Indicates how uniqueness is enforced on the attribute value."
244
+ )
245
+ sub_attributes: list["Attribute"] | None = Field(
246
+ default=None, description="A list of sub-attributes if the attribute is complex."
247
+ )
248
+
249
+
250
+ class Schema(AnaplanModel):
251
+ meta: Meta = Field(description="Metadata about the schema resource.")
252
+ id: str = Field(description="The unique identifier for the schema.")
253
+ name: str = Field(description="The name of the schema.")
254
+ description: str = Field(description="A description of the schema.")
255
+ attributes: list[Attribute] = Field(description="A list of attributes that define the schema.")
256
+
257
+
258
+ class Operation(AnaplanModel):
259
+ op: Literal["add", "remove", "replace"] = Field(description="The operation to be performed.")
260
+ path: str = Field(
261
+ description=(
262
+ "A string containing a JSON-Pointer value that references a location within the target "
263
+ "resource."
264
+ )
265
+ )
266
+ value: Any | None = Field(default=None, description="The value to be used in the operation.")
267
+
268
+
269
+ class Replace(Operation):
270
+ op: Literal["replace"] = Field(
271
+ default="replace", description="Replace the value at path with the new given value."
272
+ )
273
+
274
+
275
+ class Add(Operation):
276
+ op: Literal["add"] = Field(
277
+ default="add", description="Add the given value to the attribute at path."
278
+ )
279
+
280
+
281
+ class Remove(Operation):
282
+ op: Literal["remove"] = Field(default="remove", description="Remove the value at path.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.5.0a6
3
+ Version: 0.5.1
4
4
  Summary: Streamlined Python Interface for the Anaplan API.
5
5
  Project-URL: Homepage, https://vinzenzklass.github.io/anaplan-sdk/
6
6
  Project-URL: Repository, https://github.com/VinzenzKlass/anaplan-sdk
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
29
29
  </h3>
30
30
 
31
31
  <p align="center" style="font-size: 1.2rem; font-weight: 300; margin: 15px 0">
32
- Streamlined Python Interface for Anaplan
32
+ Streamlined Python Interface for the Anaplan API.
33
33
  </p>
34
34
 
35
35
  <div align="center">
@@ -45,9 +45,11 @@ Description-Content-Type: text/markdown
45
45
  </div>
46
46
 
47
47
  ---
48
+ Streamlined Python Interface for the Anaplan API. Get up and running with the Anaplan API in minutes.
48
49
 
49
50
  Anaplan SDK is an independent, unofficial project providing pythonic access to Anaplan. It delivers high-level
50
- abstractions over all Anaplan APIs, allowing you to focus on business requirements rather than implementation details.
51
+ abstractions over all parts of the Anaplan API, allowing you to focus on business requirements rather than
52
+ implementation details.
51
53
 
52
54
  ## Key Features
53
55
 
@@ -1,22 +1,24 @@
1
- anaplan_sdk/__init__.py,sha256=WScEKtXlnRLjCb-j3qW9W4kEACTyPsTLFs-L54et2TQ,351
1
+ anaplan_sdk/__init__.py,sha256=wkK6fPTp2-YiR9XVC-O1Iyf1FhVAcAxvJbX_YrF0p_c,397
2
2
  anaplan_sdk/_auth.py,sha256=l5z2WCcfQ05OkuQ1dcmikp6dB87Rw1qy2zu8bbaAQTs,16620
3
3
  anaplan_sdk/_oauth.py,sha256=AynlJDrGIinQT0jwxI2RSvtU4D7Wasyw3H1uicdlLVI,12672
4
- anaplan_sdk/_services.py,sha256=n14DvTC2I5ub7yzjWBaAAX9YMF7LwI_BU7zuofAj0l4,13026
4
+ anaplan_sdk/_services.py,sha256=zW0ePBQNyP759OvagDuM02SwkXxyE71IuJNz6wcWVsw,13027
5
5
  anaplan_sdk/_utils.py,sha256=jsrhdfLpriMoANukVvXpjpEJ5hWDNx7ZJKAguLvKgJA,7517
6
6
  anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
7
- anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
7
+ anaplan_sdk/_async_clients/__init__.py,sha256=oDYqtZIHWMA0iAfAv49htKaiS3sBBouqD_hEqaTWQL8,491
8
8
  anaplan_sdk/_async_clients/_alm.py,sha256=_PTD9Eght879HAadjcsfdvS0KCN93jgwpPOF8r3_E14,13178
9
- anaplan_sdk/_async_clients/_audit.py,sha256=MkCTt7s6039GfDoyyINmDA1vAkZn2xPL4hFK8D4CYsE,2927
10
- anaplan_sdk/_async_clients/_bulk.py,sha256=TsUUIGK_RPo1yKUyEbaGoFXFMBd0H4YHXI-ABYieZog,30100
9
+ anaplan_sdk/_async_clients/_audit.py,sha256=6s6IceSyz1GXxigH1JAYarL9WMCxUdaCCAj4AjCgY_E,2897
10
+ anaplan_sdk/_async_clients/_bulk.py,sha256=v-nyyf68NTs0PbQ-_5tGKbyUJZhRwZ8k0Pop5Yfg2K8,30517
11
11
  anaplan_sdk/_async_clients/_cloud_works.py,sha256=aSgmJQvE7dSJawwK0A7GEBWs7wokWk7eCwRiQuiVg6I,17701
12
12
  anaplan_sdk/_async_clients/_cw_flow.py,sha256=qJJFfnwLR7zIdZ_ay4fVI9zr3eP5B-qMcs4GlC9vqQY,3966
13
+ anaplan_sdk/_async_clients/_scim.py,sha256=pBIrxP31n5YhFnDEu0eu8VpQas8SBy6JIdCBn_Z9oNM,7036
13
14
  anaplan_sdk/_async_clients/_transactional.py,sha256=f-NlIjvRJ0NIKRcI6ZaO2YatLERwIaC0TY0fKvgUJ5Q,18050
14
- anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
15
+ anaplan_sdk/_clients/__init__.py,sha256=RB2ZYSW5vDrw8oAeDAkf7jmxu-Fr4yFLt3ebTtigE4Y,421
15
16
  anaplan_sdk/_clients/_alm.py,sha256=oRHTjnCuwQYZDE2CBWA1u410jSImIroCsDOSeP9U9w8,12905
16
17
  anaplan_sdk/_clients/_audit.py,sha256=Am6VviRw88_VC5i0N7TQTXEm6pJAHZb7xNaJTljD0fc,2850
17
- anaplan_sdk/_clients/_bulk.py,sha256=9kN8LU3c0iDF-Ov47utBl-dj_hxX3twf7TOcolzogyI,30257
18
+ anaplan_sdk/_clients/_bulk.py,sha256=e1yRHcskAALhG1JClLYknRo_kXZUQkmG_Zc6TXY6IUk,30659
18
19
  anaplan_sdk/_clients/_cloud_works.py,sha256=1bbMYM_g8MSorxTI9Au_dFzbJZgKGJVBE6DYlbWBR0U,17492
19
20
  anaplan_sdk/_clients/_cw_flow.py,sha256=x64Ua2FwCpt8vab6gaLV8tDwW_ugJrDfU5dv-TnmM2M,3855
21
+ anaplan_sdk/_clients/_scim.py,sha256=a5i7CNVNzF45YMbLY70ta_UumFumthZRmlfNm8rj2O0,6960
20
22
  anaplan_sdk/_clients/_transactional.py,sha256=IgkvBaq1Ep5mB-uxu6QoE17cUCfsodvV8dppASQrIT4,17875
21
23
  anaplan_sdk/models/__init__.py,sha256=zfwDQJQrXuLEXSpbJcm08a_YK1P7a7u-kMhwtJiJFmA,1783
22
24
  anaplan_sdk/models/_alm.py,sha256=oeENd0YM7-LoIRBq2uATIQTxVgIP9rXx3UZE2UnQAp0,4670
@@ -25,7 +27,8 @@ anaplan_sdk/models/_bulk.py,sha256=C0s6XdvHxuJHrPXU-pnZ1JXK1PJOl9FScHArpaox_mQ,8
25
27
  anaplan_sdk/models/_transactional.py,sha256=2bH10zvtMb5Lfh6DC7iQk72aEwq6tyLQ-XnH_0wYSqI,14172
26
28
  anaplan_sdk/models/cloud_works.py,sha256=APUGDt_e-JshtXkba5cQh5rZkXOZBz0Aix0qVNdEWgw,19501
27
29
  anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
28
- anaplan_sdk-0.5.0a6.dist-info/METADATA,sha256=VZfvzZQlLtBEOFFXMexsiEaetmGPe8fLilZBewT7iOg,3678
29
- anaplan_sdk-0.5.0a6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- anaplan_sdk-0.5.0a6.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
31
- anaplan_sdk-0.5.0a6.dist-info/RECORD,,
30
+ anaplan_sdk/models/scim.py,sha256=jxPrn-LgoZFYm5rtfCwmceD6i8rkJwAZUlgLx036gjg,11099
31
+ anaplan_sdk-0.5.1.dist-info/METADATA,sha256=Mmspki0fIDEGKOA4hjAmUdi6eGz1iOO8G9TM5lT1Rto,3799
32
+ anaplan_sdk-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ anaplan_sdk-0.5.1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
34
+ anaplan_sdk-0.5.1.dist-info/RECORD,,