f40-sdk 0.1.0__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.
Files changed (95) hide show
  1. f40_sdk/__init__.py +48 -0
  2. f40_sdk/clients/__init__.py +19 -0
  3. f40_sdk/clients/ai_agents.py +61 -0
  4. f40_sdk/clients/auth_manager.py +440 -0
  5. f40_sdk/clients/base.py +316 -0
  6. f40_sdk/clients/cmms.py +69 -0
  7. f40_sdk/clients/data_manager.py +1487 -0
  8. f40_sdk/clients/models.py +90 -0
  9. f40_sdk/clients/wilson.py +141 -0
  10. f40_sdk/gunicorn.py +213 -0
  11. f40_sdk/mat/__init__.py +49 -0
  12. f40_sdk/mat/configs/__init__.py +6 -0
  13. f40_sdk/mat/configs/app_config.py +73 -0
  14. f40_sdk/mat/configs/profiles.py +1122 -0
  15. f40_sdk/mat/configs/toml_app_config/__init__.py +18 -0
  16. f40_sdk/mat/configs/toml_app_config/_common.py +13 -0
  17. f40_sdk/mat/configs/toml_app_config/conversions.py +65 -0
  18. f40_sdk/mat/configs/toml_app_config/deep_merge.py +34 -0
  19. f40_sdk/mat/configs/toml_app_config/feature_flags.py +80 -0
  20. f40_sdk/mat/configs/toml_app_config/handler.py +82 -0
  21. f40_sdk/mat/configs/toml_app_config/icon_utils.py +56 -0
  22. f40_sdk/mat/configs/toml_app_config/languages.py +28 -0
  23. f40_sdk/mat/configs/toml_app_config/module_settings.py +251 -0
  24. f40_sdk/mat/configs/toml_app_config/registries.py +141 -0
  25. f40_sdk/mat/configs/toml_app_config/sidenav.py +303 -0
  26. f40_sdk/mat/configs/toml_app_config/toolbar_options.py +26 -0
  27. f40_sdk/mat/configs/toml_app_config/translations_utils.py +54 -0
  28. f40_sdk/mat/configs/toml_profile/__init__.py +16 -0
  29. f40_sdk/mat/configs/toml_profile/components_health.py +29 -0
  30. f40_sdk/mat/configs/toml_profile/handler.py +276 -0
  31. f40_sdk/mat/configs/toml_profile/mappers/__init__.py +38 -0
  32. f40_sdk/mat/configs/toml_profile/mappers/advanced_changeover.py +48 -0
  33. f40_sdk/mat/configs/toml_profile/mappers/aggregations.py +83 -0
  34. f40_sdk/mat/configs/toml_profile/mappers/cmms.py +32 -0
  35. f40_sdk/mat/configs/toml_profile/mappers/consumables.py +78 -0
  36. f40_sdk/mat/configs/toml_profile/mappers/cycle_traceability.py +109 -0
  37. f40_sdk/mat/configs/toml_profile/mappers/flags.py +6 -0
  38. f40_sdk/mat/configs/toml_profile/mappers/homepage.py +233 -0
  39. f40_sdk/mat/configs/toml_profile/mappers/kpis.py +125 -0
  40. f40_sdk/mat/configs/toml_profile/mappers/mappings.py +29 -0
  41. f40_sdk/mat/configs/toml_profile/mappers/monitoring_states.py +29 -0
  42. f40_sdk/mat/configs/toml_profile/mappers/prices.py +36 -0
  43. f40_sdk/mat/configs/toml_profile/mappers/production_config.py +46 -0
  44. f40_sdk/mat/configs/toml_profile/mappers/scrap_columns.py +14 -0
  45. f40_sdk/mat/configs/toml_profile/mappers/sidenav_visibility.py +30 -0
  46. f40_sdk/mat/configs/toml_profile/mappers/sub_assets.py +16 -0
  47. f40_sdk/mat/configs/toml_profile/mappers/tables.py +142 -0
  48. f40_sdk/mat/configs/toml_profile/mappers/time_state_categories.py +47 -0
  49. f40_sdk/mat/configs/toml_profile/mappers/time_states.py +37 -0
  50. f40_sdk/mat/configs/toml_profile/mappers/units.py +15 -0
  51. f40_sdk/mat/configs/toml_profile/overlay.py +26 -0
  52. f40_sdk/mat/configs/toml_profile/source.py +81 -0
  53. f40_sdk/mat/configs/toml_profile/utils/__init__.py +12 -0
  54. f40_sdk/mat/configs/toml_profile/utils/deep_merge.py +26 -0
  55. f40_sdk/mat/configs/toml_profile/utils/icon.py +13 -0
  56. f40_sdk/mat/configs/toml_profile/utils/kpi_expansion.py +56 -0
  57. f40_sdk/mat/configs/toml_profile/utils/sub_machines_infos.py +17 -0
  58. f40_sdk/mat/configs/toml_profile/utils/subassets.py +20 -0
  59. f40_sdk/mat/configs/toml_variables/__init__.py +18 -0
  60. f40_sdk/mat/configs/toml_variables/handler.py +147 -0
  61. f40_sdk/mat/configs/toml_variables/mappers.py +375 -0
  62. f40_sdk/mat/configs/translations.py +179 -0
  63. f40_sdk/mat/configs/variables.py +56 -0
  64. f40_sdk/mat/console.py +498 -0
  65. f40_sdk/mat/tools.py +1257 -0
  66. f40_sdk/mat/utils/__init__.py +79 -0
  67. f40_sdk/mat/utils/_globals.py +143 -0
  68. f40_sdk/mat/utils/_numpy.py +57 -0
  69. f40_sdk/mat/utils/calculator/__init__.py +268 -0
  70. f40_sdk/mat/utils/calculator/builtin_methods.py +458 -0
  71. f40_sdk/mat/utils/calculator/expression.py +956 -0
  72. f40_sdk/mat/utils/cmms.py +434 -0
  73. f40_sdk/mat/utils/data_frame/__init__.py +11 -0
  74. f40_sdk/mat/utils/data_frame/arrays.py +1774 -0
  75. f40_sdk/mat/utils/data_frame/blocks.py +770 -0
  76. f40_sdk/mat/utils/data_frame/core.py +1117 -0
  77. f40_sdk/mat/utils/data_frame/formatter.py +153 -0
  78. f40_sdk/mat/utils/data_frame/index.py +299 -0
  79. f40_sdk/mat/utils/data_frame/io.py +21 -0
  80. f40_sdk/mat/utils/data_frame/matrixes.py +687 -0
  81. f40_sdk/mat/utils/data_frame/merger.py +423 -0
  82. f40_sdk/mat/utils/dt/__init__.py +1595 -0
  83. f40_sdk/mat/utils/exceptions.py +64 -0
  84. f40_sdk/mat/utils/filtering.py +929 -0
  85. f40_sdk/mat/utils/folder.py +580 -0
  86. f40_sdk/mat/utils/load_toml.py +1109 -0
  87. f40_sdk/mat/utils/multithreading.py +200 -0
  88. f40_sdk/mat/utils/objects.py +521 -0
  89. f40_sdk/mat/utils/parser.py +55 -0
  90. f40_sdk/py.typed +0 -0
  91. f40_sdk-0.1.0.dist-info/METADATA +64 -0
  92. f40_sdk-0.1.0.dist-info/RECORD +95 -0
  93. f40_sdk-0.1.0.dist-info/WHEEL +5 -0
  94. f40_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  95. f40_sdk-0.1.0.dist-info/top_level.txt +1 -0
f40_sdk/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """
2
+ f40_sdk — SDK clients for the MAT Auth Manager, Data Manager and related services.
3
+
4
+ Built on the dependency-light HTTP building block in :mod:`f40_toolkit.common.http`
5
+ (a dependency of this package). Those primitives are re-exported here under their
6
+ historical ``Mat*`` names for convenience:
7
+
8
+ MatHttpClient -> f40_toolkit.common.http.HttpClient
9
+ MatHttpToken -> f40_toolkit.common.http.BearerToken
10
+ MatHttpError -> f40_toolkit.common.http.HttpError
11
+ MatAuthError -> f40_toolkit.common.http.HttpAuthError
12
+
13
+ Requires: ``pip install f40-sdk``
14
+
15
+ Quick imports
16
+ -------------
17
+ from f40_sdk import MatDataManagerClient, MatAuthManagerClient
18
+ from f40_sdk import MatHttpClient, MatBasicClient # building blocks
19
+ from f40_sdk import MatHttpError # exception
20
+ """
21
+
22
+ from f40_toolkit.common.http import BearerToken as MatHttpToken
23
+ from f40_toolkit.common.http import HttpAuthError as MatAuthError
24
+ from f40_toolkit.common.http import HttpClient as MatHttpClient
25
+ from f40_toolkit.common.http import HttpError as MatHttpError
26
+ from .clients import (
27
+ MatAiAgentsMngClient,
28
+ MatAuthManagerClient,
29
+ MatBasicClient,
30
+ MatCmmsMngClient,
31
+ MatCompData,
32
+ MatDataManagerClient,
33
+ WilsonMngClient,
34
+ )
35
+
36
+ __all__ = [
37
+ "MatHttpClient",
38
+ "MatBasicClient",
39
+ "MatHttpToken",
40
+ "MatAuthManagerClient",
41
+ "MatDataManagerClient",
42
+ "MatCompData",
43
+ "MatAiAgentsMngClient",
44
+ "MatCmmsMngClient",
45
+ "WilsonMngClient",
46
+ "MatHttpError",
47
+ "MatAuthError",
48
+ ]
@@ -0,0 +1,19 @@
1
+ """Service SDK clients for the MAT platform."""
2
+
3
+ from .ai_agents import MatAiAgentsMngClient
4
+ from .auth_manager import MatAuthManagerClient
5
+ from .base import MatBasicClient
6
+ from .cmms import MatCmmsMngClient
7
+ from .data_manager import MatDataManagerClient
8
+ from .models import MatCompData
9
+ from .wilson import WilsonMngClient
10
+
11
+ __all__ = [
12
+ "MatBasicClient",
13
+ "MatAuthManagerClient",
14
+ "MatDataManagerClient",
15
+ "MatCompData",
16
+ "MatAiAgentsMngClient",
17
+ "MatCmmsMngClient",
18
+ "WilsonMngClient",
19
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ SDK client for the MAT AI-Agents (IoT-Agents) service.
3
+
4
+ Auth is handled by the parent :class:`MatBasicClient` (auth_mng / basic /
5
+ bearer_token / env_name). This client adds the agent listing and invocation API.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from typing import TYPE_CHECKING, List, Optional, Union
13
+
14
+ from .base import MatBasicClient
15
+
16
+ if TYPE_CHECKING:
17
+ from key_vault_interface.key_vault_interface import KeyVaultInterface # type: ignore[import]
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class MatAiAgentsMngClient(MatBasicClient):
23
+ """SDK client for the MAT AI-Agents service."""
24
+
25
+ def get_agents(self) -> List[dict]:
26
+ """Return the list of agents (empty list when none / unexpected shape)."""
27
+ res = self._send("GET", "iota/agents")
28
+ if isinstance(res, dict):
29
+ res = res.get("agents")
30
+ return res if isinstance(res, list) else []
31
+
32
+ def run_agent(self, agent_id: str, run_message: str) -> None:
33
+ """Invoke an agent with an alert message."""
34
+ self._send(
35
+ "POST", f"iota/agents/invoke/{agent_id}",
36
+ data=json.dumps({"alertMessage": run_message}), res_type="code",
37
+ )
38
+
39
+ @classmethod
40
+ def from_json( # type: ignore[override]
41
+ cls,
42
+ _json: Union[dict, str],
43
+ kvi: Optional["KeyVaultInterface"] = None,
44
+ ) -> "MatAiAgentsMngClient":
45
+ """
46
+ Construct from a config dict or a path to a JSON file.
47
+
48
+ The JSON may have the config nested under an ``"ai_agents"`` key::
49
+
50
+ {"ai_agents": {"url": "...", "auth": {...}}}
51
+ """
52
+ if isinstance(_json, str):
53
+ with open(_json, "r") as fh:
54
+ _json = json.load(fh)
55
+ if isinstance(_json, dict) and "ai_agents" in _json:
56
+ _json = _json["ai_agents"]
57
+
58
+ if not isinstance(_json, dict):
59
+ raise TypeError(f"Expected a dict or JSON file path, got {type(_json).__name__}.")
60
+
61
+ return cls.from_dict(_json, kvi=kvi)
@@ -0,0 +1,440 @@
1
+ """
2
+ SDK client for the MAT Auth Manager service.
3
+
4
+ Handles OAuth application-token acquisition and credential validation. Typically
5
+ used internally by :class:`MatBasicClient` to set up automatic bearer-token refresh
6
+ for downstream services.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import time
14
+ from base64 import b64encode
15
+ from typing import Any, Optional
16
+
17
+ from f40_toolkit.common.http import HttpClient
18
+ from key_vault_interface.key_vault_interface import KeyVaultInterface as KVI
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _TOKEN_ENDPOINT = "apif/oauth/application-token"
23
+
24
+
25
+ class MatAuthManagerClient(HttpClient):
26
+ """
27
+ HTTP client for the MAT Auth Manager service.
28
+
29
+ Extends :class:`~f40_toolkit.common.http.HttpClient` directly (not
30
+ :class:`MatBasicClient`) because the auth manager is itself the token issuer —
31
+ it authenticates with HTTP Basic credentials to obtain tokens for downstream
32
+ services.
33
+
34
+ Example
35
+ -------
36
+ am = MatAuthManagerClient(
37
+ base_url="http://mat-auth-manager",
38
+ auth=("svc-user", "svc-password"),
39
+ credentials={"client_id": "my-app", "client_secret": "abc123"},
40
+ )
41
+ token = am.get_bearer_token()
42
+ # {"access_token": "eyJ...", "expires_in": 3600, "token_type": "Bearer"}
43
+
44
+ Parameters
45
+ ----------
46
+ base_url : Auth Manager base URL.
47
+ auth : (user, password) tuple for HTTP Basic auth to the auth manager itself.
48
+ credentials : OAuth credentials forwarded to the token endpoint. Must contain
49
+ "client_id" and "client_secret" (or kvi resolves the secret).
50
+ app_name : Injected as the "App-Name" header on token requests.
51
+ kvi : KeyVaultInterface used to resolve a missing client_secret.
52
+ verify : TLS verification flag.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ base_url: str,
58
+ auth: tuple,
59
+ *,
60
+ credentials: Optional[dict] = None,
61
+ app_name: Optional[str] = None,
62
+ kvi: Optional["KVI"] = None,
63
+ verify: Optional[bool] = None,
64
+ ) -> None:
65
+ super().__init__(base_url=base_url, auth=auth, verify=verify)
66
+ self.app_name = app_name
67
+ self.credentials: Optional[dict] = credentials
68
+ # Cached "Bearer <token>" application token for user-management endpoints.
69
+ self._app_token: Optional[str] = None
70
+ self._app_token_exp: float = 0.0
71
+ if self.credentials is not None:
72
+ self.validate_credentials(self.credentials, kvi=kvi)
73
+
74
+ @staticmethod
75
+ def from_json(
76
+ config,
77
+ kvi=None,
78
+ ):
79
+
80
+ if isinstance(config, str):
81
+ with open(config) as rf:
82
+ config = json.load(rf)
83
+
84
+ if not isinstance(config, dict):
85
+ raise TypeError("Invalid config ...")
86
+
87
+ if "auth_mng" in config:
88
+ auth_mng = config.get("auth_mng")
89
+ if not isinstance(auth_mng, dict):
90
+ raise TypeError("Invalid config ...")
91
+ else:
92
+ auth_mng = config
93
+
94
+ base_url = auth_mng.get("url")
95
+ if not isinstance(base_url, str):
96
+ raise TypeError("Invalid config ...")
97
+
98
+ # kvi = KVI.from_json(
99
+ # config=config.get("key_vault"),
100
+ # secrets={
101
+ # "webapp-registration-secret": "webapp-registration-secret-1",
102
+ # "dm-to-am-secret": "dm-to-am-secret",
103
+ # },
104
+ # )
105
+
106
+ auth_basic_creds = auth_mng.get("auth")
107
+
108
+ am_usr = None
109
+ am_pwd = None
110
+ if isinstance(auth_basic_creds, (list, tuple)):
111
+
112
+ if len(auth_basic_creds) == 1:
113
+ am_usr = auth_basic_creds[0]
114
+
115
+ elif len(auth_basic_creds) == 2:
116
+ am_usr, am_pwd = auth_basic_creds
117
+
118
+ else:
119
+ raise Exception("....")
120
+
121
+ elif isinstance(auth_basic_creds, dict):
122
+
123
+ am_usr = auth_basic_creds.get("user")
124
+ am_pwd = auth_basic_creds.get("pwd")
125
+
126
+ elif isinstance(auth_basic_creds, str):
127
+ am_usr = auth_basic_creds
128
+
129
+ if not isinstance(am_usr, str):
130
+ am_usr = "dm-to-am-secret"
131
+
132
+ if not isinstance(am_pwd, str):
133
+ if not isinstance(kvi, KVI):
134
+ raise Exception("....")
135
+
136
+ am_pwd = kvi.get("dm-to-am-secret")
137
+ if not isinstance(am_pwd, str):
138
+ raise Exception("....")
139
+
140
+ app_name = auth_mng.get("app_name")
141
+ if not isinstance(app_name, str):
142
+ raise TypeError("Invalid config ...")
143
+
144
+ return MatAuthManagerClient(
145
+ base_url=base_url,
146
+ auth=(am_usr, am_pwd),
147
+ app_name=app_name,
148
+ credentials=auth_mng.get("credentials"),
149
+ kvi=kvi,
150
+ )
151
+
152
+ # ------------------------------------------------------------------
153
+ # Token acquisition
154
+ # ------------------------------------------------------------------
155
+
156
+ def get_bearer_token(self, credentials: Optional[dict] = None) -> dict:
157
+ """
158
+ Request an application bearer token from the auth manager.
159
+
160
+ Returns the full token response dict
161
+ ``{"access_token": "…", "expires_in": 3600, …}``.
162
+
163
+ :class:`BearerToken` calls this automatically when the token is near expiry.
164
+
165
+ Parameters
166
+ ----------
167
+ credentials : Per-call override; uses instance credentials when None.
168
+ """
169
+ if credentials is not None:
170
+ self.validate_credentials(credentials)
171
+ else:
172
+ credentials = self.credentials
173
+
174
+ if credentials is None:
175
+ raise ValueError(
176
+ "No credentials available. Supply them at construction time "
177
+ "or pass them to get_bearer_token()."
178
+ )
179
+
180
+ headers: dict[str, str] = {}
181
+ if self.app_name is not None:
182
+ headers["App-Name"] = self.app_name
183
+
184
+ logger.debug("Requesting bearer token from %s.", _TOKEN_ENDPOINT)
185
+ return self.request(
186
+ "POST",
187
+ _TOKEN_ENDPOINT,
188
+ data=json.dumps({"credentials": credentials}),
189
+ headers=headers,
190
+ )
191
+
192
+ def refresh_token(self, refresh_token: str) -> Any:
193
+ """
194
+ Exchange a refresh token for a fresh user token (POST ``refresh-token``).
195
+
196
+ The app credentials (OAuth ``client_id``/``client_secret``) are sent as a
197
+ Basic ``credentials`` header and the refresh token in the JSON body. Returns
198
+ the parsed response dict (contains ``access_token`` on success).
199
+ """
200
+ headers: dict[str, str] = {}
201
+ creds = self.credentials or {}
202
+ client_id = creds.get("client_id")
203
+ client_secret = creds.get("client_secret")
204
+ if client_id and client_secret:
205
+ basic = b64encode(f"{client_id}:{client_secret}".encode()).decode()
206
+ headers["credentials"] = f"Basic {basic}"
207
+ if self.app_name is not None:
208
+ headers["App-Name"] = self.app_name
209
+
210
+ return self.request(
211
+ "POST",
212
+ "refresh-token",
213
+ data=json.dumps({"refresh_token": refresh_token}),
214
+ headers=headers,
215
+ )
216
+
217
+ # ------------------------------------------------------------------
218
+ # Credential validation
219
+ # ------------------------------------------------------------------
220
+
221
+ def validate_credentials(
222
+ self,
223
+ credentials: dict,
224
+ *,
225
+ kvi: Optional["KVI"] = None,
226
+ ) -> None:
227
+ """
228
+ Ensure credentials contain "client_id" and "client_secret".
229
+
230
+ Mutates the dict in place: a missing/invalid client_secret is fetched
231
+ from the key vault under the key "webapp-registration-secret".
232
+
233
+ Raises
234
+ ------
235
+ TypeError : credentials is not a dict, or client_id has the wrong type.
236
+ KeyError : a required key is missing and can't be sourced from the KV.
237
+ ValueError : the secret could not be retrieved from the key vault.
238
+ """
239
+ if not isinstance(credentials, dict):
240
+ raise TypeError(
241
+ f"credentials must be a dict, got {type(credentials).__name__}."
242
+ )
243
+
244
+ cs = credentials.get("client_secret")
245
+ if not isinstance(cs, str):
246
+ if kvi is None:
247
+ raise KeyError(
248
+ "'client_secret' is missing from credentials and no "
249
+ "KeyVaultInterface was provided to fetch it."
250
+ )
251
+ secret = kvi.get("webapp-registration-secret")
252
+ if not isinstance(secret, str):
253
+ raise ValueError(
254
+ "Could not retrieve 'client_secret' from the key vault "
255
+ "(key: 'webapp-registration-secret')."
256
+ )
257
+ credentials["client_secret"] = secret
258
+
259
+ if "client_id" not in credentials:
260
+ raise KeyError("'client_id' is missing from credentials.")
261
+
262
+ if not isinstance(credentials["client_id"], str):
263
+ raise TypeError(
264
+ f"'client_id' must be a string, got {type(credentials['client_id']).__name__}."
265
+ )
266
+
267
+ def check_user_permissions(
268
+ self,
269
+ token,
270
+ permissions=None,
271
+ ):
272
+ headers = {"appName": self.app_name, "App-Name": self.app_name}
273
+
274
+ if permissions is None:
275
+ permissions = {"permissions": {}}
276
+ body = {"user_token": token, "permissions": permissions}
277
+
278
+ return self.request(
279
+ method="POST",
280
+ endpoint="apif/check-user-permissions",
281
+ headers=headers,
282
+ json=body,
283
+ )
284
+
285
+ # ------------------------------------------------------------------
286
+ # User & group management
287
+ # ------------------------------------------------------------------
288
+
289
+ _USERS_ENDPOINT = "apif/users"
290
+ _USERS_SELECT = (
291
+ "id,displayName,mail,givenName,surname,"
292
+ "externalUserState,userType,userPrincipalName"
293
+ )
294
+
295
+ def _application_token(self) -> str:
296
+ """
297
+ Return a cached ``"Bearer <token>"`` application token for the user-management
298
+ endpoints, refreshing it from ``get_bearer_token`` shortly before expiry.
299
+
300
+ The token is cached in-memory on the client (a process-wide singleton), so
301
+ it is fetched at most once per ``expires_in`` window.
302
+ """
303
+ now = time.monotonic()
304
+ if self._app_token is not None and now < self._app_token_exp:
305
+ return self._app_token
306
+
307
+ resp = self.get_bearer_token()
308
+ access = resp.get("access_token") if isinstance(resp, dict) else None
309
+ if not access:
310
+ raise ValueError(
311
+ "Auth Manager did not return an application 'access_token'."
312
+ )
313
+ try:
314
+ ttl = int(resp.get("expires_in", 600))
315
+ except (TypeError, ValueError):
316
+ ttl = 600
317
+ self._app_token = f"Bearer {access}"
318
+ # refresh 180s early to avoid using a token that expires mid-flight
319
+ self._app_token_exp = now + max(0, ttl - 180)
320
+ return self._app_token
321
+
322
+ def _um_send(self, method: str, endpoint: str, **kwargs: Any) -> Any:
323
+ """
324
+ ``request`` variant for user-management endpoints: injects the cached
325
+ application token as the ``Application-Token`` header. Caller-supplied
326
+ headers take precedence on overlap.
327
+ """
328
+ headers = {
329
+ "Application-Token": self._application_token(),
330
+ **(kwargs.pop("headers", None) or {}),
331
+ }
332
+ return self.request(method, endpoint, headers=headers, **kwargs)
333
+
334
+ def get_users(self) -> list:
335
+ """List users from the Auth Manager user-management API.
336
+
337
+ Returns the parsed JSON list of users (a curated ``$select`` projection).
338
+ """
339
+ return self._um_send(
340
+ "GET", f"{self._USERS_ENDPOINT}?$select={self._USERS_SELECT}"
341
+ )
342
+
343
+ def create_users(self, users_data) -> Any:
344
+ """Create users via the Auth Manager user-management API."""
345
+ return self._um_send("POST", self._USERS_ENDPOINT, data=json.dumps(users_data))
346
+
347
+ def invite_users(self, users_data) -> Any:
348
+ """Invite users (POST ``apif/invitations``)."""
349
+ return self._um_send("POST", "apif/invitations", data=json.dumps(users_data))
350
+
351
+ def get_user_um(self, user_id: str) -> Any:
352
+ """Fetch a single user (GET ``apif/users/<id>``)."""
353
+ return self._um_send("GET", f"{self._USERS_ENDPOINT}/{user_id}")
354
+
355
+ def put_user(self, user_id: str, user_data) -> Any:
356
+ """Update a user (PUT ``apif/users/<id>``)."""
357
+ return self._um_send(
358
+ "PUT", f"{self._USERS_ENDPOINT}/{user_id}", data=json.dumps(user_data)
359
+ )
360
+
361
+ def delete_user(self, user_id: str) -> Any:
362
+ """Delete a user (DELETE ``apif/users/<id>``)."""
363
+ return self._um_send("DELETE", f"{self._USERS_ENDPOINT}/{user_id}")
364
+
365
+ def get_user_memberships(self, user_id: str) -> Any:
366
+ """List a user's group memberships (GET ``apif/users/<id>/memberships``)."""
367
+ return self._um_send("GET", f"{self._USERS_ENDPOINT}/{user_id}/memberships")
368
+
369
+ # --- groups ---
370
+
371
+ def get_groups(self, filters: Optional[str] = None) -> Any:
372
+ """List groups (GET ``apif/groups``), optionally filtered."""
373
+ endpoint = "apif/groups?$select=id,displayName"
374
+ if filters:
375
+ endpoint += f"&{filters}"
376
+ return self._um_send("GET", endpoint)
377
+
378
+ def create_groups(self, group_data) -> Any:
379
+ """Create groups (POST ``apif/groups``)."""
380
+ return self._um_send("POST", "apif/groups", data=json.dumps(group_data))
381
+
382
+ def get_group(self, group_id: str) -> Any:
383
+ """Fetch a single group (GET ``apif/groups/<id>``)."""
384
+ return self._um_send("GET", f"apif/groups/{group_id}")
385
+
386
+ def delete_group(self, group_id: str) -> Any:
387
+ """Delete a group (DELETE ``apif/groups/<id>``)."""
388
+ return self._um_send("DELETE", f"apif/groups/{group_id}")
389
+
390
+ def get_group_members(self, group_id: str) -> Any:
391
+ """List a group's members (GET ``apif/groups/<id>/members``)."""
392
+ return self._um_send("GET", f"apif/groups/{group_id}/members")
393
+
394
+ def add_members_to_group(
395
+ self, group_id: str, members, raise_on_error: bool = True
396
+ ) -> Any:
397
+ """Add members to a group (POST ``apif/groups/<id>/members``)."""
398
+ params = None if raise_on_error else {"raise": 0}
399
+ return self._um_send(
400
+ "POST",
401
+ f"apif/groups/{group_id}/members",
402
+ data=json.dumps(members),
403
+ params=params,
404
+ )
405
+
406
+ def remove_member_from_group(self, group_id: str, member_id: str) -> Any:
407
+ """Remove a member from a group (DELETE ``apif/groups/<id>/members?user_id=…``)."""
408
+ return self._um_send(
409
+ "DELETE", f"apif/groups/{group_id}/members", params={"user_id": member_id}
410
+ )
411
+
412
+ def remove_user_from_group(self, group_id: str, user_id: str) -> Any:
413
+ """Remove a user from a group (alias of :meth:`remove_member_from_group`)."""
414
+ return self.remove_member_from_group(group_id, user_id)
415
+
416
+ def put_group(self, group_id: str, group_data) -> Any:
417
+ """Update a group (PUT ``apif/groups/<id>``)."""
418
+ return self._um_send(
419
+ "PUT", f"apif/groups/{group_id}", data=json.dumps(group_data)
420
+ )
421
+
422
+ # --- profiles ---
423
+
424
+ def get_profiles(self, simple: bool = False) -> Any:
425
+ """List profiles (GET ``apif/profiles``)."""
426
+ endpoint = "apif/profiles?simple=1" if simple else "apif/profiles"
427
+ return self._um_send("GET", endpoint)
428
+
429
+ def create_profile(self, profile_data) -> Any:
430
+ """Create a profile (POST ``apif/profiles``)."""
431
+ return self._um_send("POST", "apif/profiles", data=json.dumps(profile_data))
432
+
433
+ def put_profile(self, profile_id: str, profile_data) -> Any:
434
+ """Update a profile (PUT ``apif/profiles/<id>``)."""
435
+ return self._um_send(
436
+ "PUT", f"apif/profiles/{profile_id}", data=json.dumps(profile_data)
437
+ )
438
+
439
+ def __repr__(self) -> str:
440
+ return f"MatAuthManagerClient(base_url={self.base_url!r}, app_name={self.app_name!r})"