databricks-modules-vy 1.2.2__tar.gz → 1.3.1__tar.gz

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 (24) hide show
  1. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/PKG-INFO +1 -1
  2. databricks_modules_vy-1.3.1/databricks_modules_vy/service_platform.py +485 -0
  3. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/sharepoint.py +5 -2
  4. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/PKG-INFO +1 -1
  5. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/pyproject.toml +1 -1
  6. databricks_modules_vy-1.2.2/databricks_modules_vy/service_platform.py +0 -128
  7. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/LICENSE +0 -0
  8. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/__init__.py +0 -0
  9. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/data_product.py +0 -0
  10. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/data_quality_tests.py +0 -0
  11. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/dataframe.py +0 -0
  12. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/extend_class.py +0 -0
  13. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/glue.py +0 -0
  14. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/logging.py +0 -0
  15. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/oracle.py +0 -0
  16. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/qlik.py +0 -0
  17. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/s3.py +0 -0
  18. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/snaplogic.py +0 -0
  19. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/styringsinfo_postgres.py +0 -0
  20. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/unity_catalog.py +0 -0
  21. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/SOURCES.txt +0 -0
  22. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/dependency_links.txt +0 -0
  23. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/top_level.txt +0 -0
  24. {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks_modules_vy
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Databricks modules for Vy
5
5
  Author-email: Sindre Kjetså <sindre.kjetsa@vy.no>
6
6
  Project-URL: Homepage, https://github.com/nsbno/
@@ -0,0 +1,485 @@
1
+ """Utilities for Service Platform API access and raw landing support.
2
+
3
+ This module contains:
4
+ - Authentication and request/retry handling via ``ServicePlatformApi``.
5
+ - Helpers for creating and maintaining landing run paths in Unity Catalog volumes.
6
+ - JSON writers for raw API landing.
7
+
8
+ The landing-oriented helpers are primarily intended for APIs with many pages,
9
+ where responses accumulate into large payload volumes and landing raw responses
10
+ first gives better stability and recovery options.
11
+ """
12
+
13
+ import datetime as dt
14
+ import json
15
+ import random
16
+ import time
17
+ import uuid
18
+ from dataclasses import dataclass
19
+ from typing import Any, Iterable
20
+
21
+ import requests
22
+
23
+ import databricks_modules_vy.logging as log_utils
24
+
25
+
26
+ def _json_default(value: Any) -> str:
27
+ if isinstance(value, (dt.datetime, dt.date, dt.time)):
28
+ return value.isoformat()
29
+ raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
30
+
31
+
32
+ def _serialize_json_compact(payload: dict[str, Any]) -> str:
33
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":"), default=_json_default)
34
+
35
+
36
+ @dataclass
37
+ class TokenConfig:
38
+ """A simple dataclass to hold the configuration for the Service Platform API.
39
+
40
+ Attributes:
41
+ client_id (str): The client ID for the Service Platform API.
42
+ client_secret (str): The client secret for the Service Platform API.
43
+ access_token_url (str): The URL for the access token endpoint.
44
+ i.e. "https://auth.cognito.vydev.io/oauth2/token"
45
+ scopes (str | Iterable[str]): Scope value(s) to authenticate with.
46
+ A single string is used as-is, while an iterable is joined with
47
+ spaces for the OAuth token request.
48
+ """
49
+
50
+ client_id: str
51
+ client_secret: str
52
+ access_token_url: str
53
+ scopes: str | Iterable[str]
54
+
55
+
56
+ def volume_fqn_to_path(volume_fqn: str) -> str:
57
+ """Convert a Unity Catalog volume FQN to a Databricks volume path.
58
+
59
+ Args:
60
+ volume_fqn (str): Volume identifier in the format
61
+ "<catalog>.<schema>.<volume>".
62
+
63
+ Returns:
64
+ str: Databricks volume path in the format
65
+ "/Volumes/<catalog>/<schema>/<volume>".
66
+
67
+ Raises:
68
+ ValueError: If ``volume_fqn`` is not in the expected three-part format.
69
+
70
+ Example:
71
+ person.catalog.raw_landing -> /Volumes/person/catalog/raw_landing
72
+ """
73
+ parts = volume_fqn.split(".")
74
+ if len(parts) != 3:
75
+ raise ValueError(
76
+ "volume_fqn must be in format '<catalog>.<schema>.<volume>', " f"got '{volume_fqn}'"
77
+ )
78
+ return f"/Volumes/{parts[0]}/{parts[1]}/{parts[2]}"
79
+
80
+
81
+ def build_landing_run_paths(
82
+ volume_fqn: str, dataset_name: str, run_timestamp: dt.datetime | None = None
83
+ ) -> tuple[str, str]:
84
+ """Build a dataset root path and a unique run path for raw API landing.
85
+
86
+ Args:
87
+ volume_fqn (str): Volume identifier in the format
88
+ "<catalog>.<schema>.<volume>".
89
+ dataset_name (str): Dataset subfolder under the volume root.
90
+ run_timestamp (datetime.datetime | None): UTC timestamp used to build
91
+ date and run identifiers. If ``None``, current UTC time is used.
92
+
93
+ Returns:
94
+ tuple[str, str]:
95
+ - dataset_root_path: Root path for the dataset.
96
+ - run_path: Unique run folder under ``date=YYYY-MM-DD/run=<run_id>``.
97
+ """
98
+ run_timestamp = run_timestamp or dt.datetime.now(dt.timezone.utc)
99
+ date_folder = run_timestamp.strftime("%Y-%m-%d")
100
+ run_id = f"{run_timestamp.strftime('%Y%m%dT%H%M%SZ')}_{uuid.uuid4().hex[:8]}"
101
+
102
+ dataset_root_path = f"{volume_fqn_to_path(volume_fqn)}/{dataset_name}"
103
+ run_path = f"{dataset_root_path}/date={date_folder}/run={run_id}"
104
+ return dataset_root_path, run_path
105
+
106
+
107
+ def cleanup_landing_runs_by_count(
108
+ dbutils: Any, dataset_root_path: str, keep_last_runs: int
109
+ ) -> list[str]:
110
+ """Delete old landed runs and keep only the newest N run folders.
111
+
112
+ Args:
113
+ dbutils (Any): Databricks ``dbutils`` object.
114
+ dataset_root_path (str): Dataset landing root path that contains
115
+ ``date=*`` folders.
116
+ keep_last_runs (int): Number of most recent runs to retain.
117
+
118
+ Returns:
119
+ list[str]: Deleted run paths.
120
+
121
+ Raises:
122
+ ValueError: If ``keep_last_runs`` is less than 1.
123
+ """
124
+ if keep_last_runs < 1:
125
+ raise ValueError("keep_last_runs must be >= 1")
126
+
127
+ try:
128
+ date_dirs = [
129
+ item.path
130
+ for item in dbutils.fs.ls(dataset_root_path)
131
+ if item.path.rstrip("/").split("/")[-1].startswith("date=")
132
+ ]
133
+ except Exception:
134
+ return []
135
+
136
+ run_paths: list[str] = []
137
+ for date_dir in sorted(date_dirs, reverse=True):
138
+ try:
139
+ runs_in_date = [
140
+ item.path
141
+ for item in dbutils.fs.ls(date_dir)
142
+ if item.path.rstrip("/").split("/")[-1].startswith("run=")
143
+ ]
144
+ except Exception:
145
+ continue
146
+ run_paths.extend(sorted(runs_in_date, reverse=True))
147
+
148
+ if len(run_paths) <= keep_last_runs:
149
+ return []
150
+
151
+ paths_to_delete = run_paths[keep_last_runs:]
152
+ for path in paths_to_delete:
153
+ dbutils.fs.rm(path, True)
154
+
155
+ # Remove now-empty date folders to keep the landing tree tidy.
156
+ for date_dir in date_dirs:
157
+ try:
158
+ children = dbutils.fs.ls(date_dir)
159
+ except Exception:
160
+ continue
161
+
162
+ if not children:
163
+ try:
164
+ dbutils.fs.rm(date_dir, True)
165
+ except Exception:
166
+ pass
167
+
168
+ return paths_to_delete
169
+
170
+
171
+ def get_latest_landing_run_path(dbutils: Any, volume_fqn: str, dataset_name: str) -> str | None:
172
+ """Get the most recent run path for a landed dataset.
173
+
174
+ Args:
175
+ dbutils (Any): Databricks ``dbutils`` object.
176
+ volume_fqn (str): Volume identifier in the format
177
+ "<catalog>.<schema>.<volume>".
178
+ dataset_name (str): Dataset subfolder under the volume root.
179
+
180
+ Returns:
181
+ str | None: Latest run path, or ``None`` when no runs are found.
182
+ """
183
+ dataset_root_path = f"{volume_fqn_to_path(volume_fqn)}/{dataset_name}"
184
+
185
+ try:
186
+ date_dirs = [
187
+ item.path
188
+ for item in dbutils.fs.ls(dataset_root_path)
189
+ if item.path.rstrip("/").split("/")[-1].startswith("date=")
190
+ ]
191
+ except Exception:
192
+ return None
193
+
194
+ if not date_dirs:
195
+ return None
196
+
197
+ for date_dir in sorted(date_dirs, reverse=True):
198
+ try:
199
+ run_dirs = [
200
+ item.path
201
+ for item in dbutils.fs.ls(date_dir)
202
+ if item.path.rstrip("/").split("/")[-1].startswith("run=")
203
+ ]
204
+ except Exception:
205
+ continue
206
+
207
+ if run_dirs:
208
+ return sorted(run_dirs, reverse=True)[0]
209
+
210
+ return None
211
+
212
+
213
+ def write_json_to_volume(
214
+ dbutils: Any,
215
+ file_path: str,
216
+ payload: dict[str, Any],
217
+ overwrite: bool = True,
218
+ max_attempts: int = 3,
219
+ backoff_seconds: float = 1.0,
220
+ ) -> None:
221
+ """Write a JSON payload to a volume path with retry.
222
+
223
+ Args:
224
+ dbutils (Any): Databricks ``dbutils`` object.
225
+ file_path (str): Target file path.
226
+ payload (dict[str, Any]): JSON-serializable payload to write.
227
+ overwrite (bool): Whether to overwrite existing file at ``file_path``.
228
+ max_attempts (int): Maximum write attempts before failing.
229
+ backoff_seconds (float): Base backoff in seconds used between retries.
230
+
231
+ Raises:
232
+ Exception: Re-raises the final write exception if all attempts fail.
233
+ """
234
+ if max_attempts < 1:
235
+ raise ValueError("max_attempts must be >= 1")
236
+
237
+ body = _serialize_json_compact(payload)
238
+
239
+ for attempt in range(1, max_attempts + 1):
240
+ try:
241
+ dbutils.fs.put(file_path, body, overwrite)
242
+ return
243
+ except Exception:
244
+ if attempt >= max_attempts:
245
+ raise
246
+ sleep_seconds = backoff_seconds * (2 ** (attempt - 1))
247
+ time.sleep(sleep_seconds)
248
+
249
+
250
+ def estimate_json_payload_bytes(payload: dict[str, Any]) -> int:
251
+ """Estimate UTF-8 byte size of a compact JSON payload."""
252
+ return len(_serialize_json_compact(payload).encode("utf-8"))
253
+
254
+
255
+ def write_json_lines_to_volume(
256
+ dbutils: Any,
257
+ file_path: str,
258
+ payloads: list[dict[str, Any]],
259
+ overwrite: bool = True,
260
+ max_attempts: int = 3,
261
+ backoff_seconds: float = 1.0,
262
+ ) -> None:
263
+ """Write newline-delimited JSON payloads to a volume path with retry."""
264
+ if max_attempts < 1:
265
+ raise ValueError("max_attempts must be >= 1")
266
+
267
+ body = "\n".join(_serialize_json_compact(payload) for payload in payloads)
268
+ if body:
269
+ body += "\n"
270
+
271
+ for attempt in range(1, max_attempts + 1):
272
+ try:
273
+ dbutils.fs.put(file_path, body, overwrite)
274
+ return
275
+ except Exception:
276
+ if attempt >= max_attempts:
277
+ raise
278
+ sleep_seconds = backoff_seconds * (2 ** (attempt - 1))
279
+ time.sleep(sleep_seconds)
280
+
281
+
282
+ class ServicePlatformApi:
283
+ """Class to interact with the Service Platform API.
284
+
285
+ This class provides methods to handle authentication and make HTTP GET and POST requests
286
+ to the Service Platform API. It uses OAuth2 client credentials flow to obtain access tokens.
287
+
288
+ Attributes:
289
+ api_url (str): The base URL for the Service Platform API.
290
+ token_config (TokenConfig): The configuration details for token generation.
291
+ """
292
+
293
+ def __init__(self, api_url: str, token_config: TokenConfig):
294
+ """Initialize the ServicePlatformApi class.
295
+
296
+ Args:
297
+ api_url (str): The base URL for the Service Platform API.
298
+ token_config (TokenConfig): An instance of TokenConfig containing authentication details.
299
+
300
+ Raises:
301
+ Exception: If the token generation fails.
302
+ """
303
+ self._api_url = api_url
304
+ self._token_config = token_config
305
+ self._session = requests.Session()
306
+ self._max_retries = 5
307
+ self._backoff_factor_seconds = 1.0
308
+ self._max_backoff_seconds = 30.0
309
+ self._token_max_retries = 3
310
+ self._token_timeout_seconds = 30
311
+ self._retryable_status_codes = {408, 425, 429, 500, 502, 503, 504}
312
+ self._access_token = self._get_access_token()
313
+
314
+ def _scope_value(self) -> str:
315
+ """Return OAuth scope(s) as a single space-delimited string.
316
+
317
+ Returns:
318
+ str: Scope value suitable for token request payload.
319
+ """
320
+ if isinstance(self._token_config.scopes, str):
321
+ return self._token_config.scopes
322
+ return " ".join(self._token_config.scopes)
323
+
324
+ def _sleep_with_backoff(self, attempt: int) -> None:
325
+ """Sleep using exponential backoff with jitter.
326
+
327
+ Args:
328
+ attempt (int): 1-based retry attempt number.
329
+ """
330
+ exponential = self._backoff_factor_seconds * (2 ** (attempt - 1))
331
+ jitter = random.uniform(0, 1)
332
+ sleep_seconds = min(self._max_backoff_seconds, exponential + jitter)
333
+ time.sleep(sleep_seconds)
334
+
335
+ def _request_with_retry(
336
+ self,
337
+ method: str,
338
+ path: str,
339
+ params: dict[str, Any] | None = None,
340
+ json_payload: dict[str, Any] | None = None,
341
+ headers: dict[str, str] | None = None,
342
+ timeout: int = 30,
343
+ ) -> requests.Response:
344
+ """Execute an HTTP request with retry and token-refresh handling.
345
+
346
+ Args:
347
+ method (str): HTTP method, for example ``GET`` or ``POST``.
348
+ path (str): Endpoint path relative to ``self._api_url``.
349
+ params (dict[str, Any] | None): Query string parameters.
350
+ json_payload (dict[str, Any] | None): JSON request body.
351
+ headers (dict[str, str] | None): Additional request headers.
352
+ timeout (int): Request timeout in seconds.
353
+
354
+ Returns:
355
+ requests.Response: Successful HTTP response.
356
+
357
+ Raises:
358
+ requests.exceptions.RequestException: If all retries fail.
359
+ RuntimeError: If an unexpected retry state is reached.
360
+ """
361
+ request_headers = dict(headers or {})
362
+ request_headers["Authorization"] = f"Bearer {self._access_token}"
363
+
364
+ refresh_token_once = True
365
+
366
+ for attempt in range(1, self._max_retries + 1):
367
+ try:
368
+ response = self._session.request(
369
+ method=method,
370
+ url=self._api_url + path,
371
+ params=params,
372
+ json=json_payload,
373
+ headers=request_headers,
374
+ timeout=timeout,
375
+ )
376
+ except requests.exceptions.RequestException:
377
+ if attempt >= self._max_retries:
378
+ raise
379
+ self._sleep_with_backoff(attempt)
380
+ continue
381
+
382
+ if response.status_code == 401 and refresh_token_once:
383
+ self._access_token = self._get_access_token()
384
+ request_headers["Authorization"] = f"Bearer {self._access_token}"
385
+ refresh_token_once = False
386
+ continue
387
+
388
+ if response.ok:
389
+ return response
390
+
391
+ if response.status_code in self._retryable_status_codes and attempt < self._max_retries:
392
+ log_utils.lp(
393
+ "Retryable status code "
394
+ f"{response.status_code} for {method} {path} "
395
+ f"(attempt {attempt}/{self._max_retries}). Retrying."
396
+ )
397
+ self._sleep_with_backoff(attempt)
398
+ continue
399
+
400
+ response.raise_for_status()
401
+
402
+ raise RuntimeError("Unexpected retry state reached in _request_with_retry")
403
+
404
+ def _get_access_token(self) -> str:
405
+ """Fetch an OAuth access token using client credentials flow.
406
+
407
+ Returns:
408
+ str: Access token.
409
+
410
+ Raises:
411
+ Exception: If token request fails.
412
+ """
413
+ token_response = None
414
+ for attempt in range(1, self._token_max_retries + 1):
415
+ try:
416
+ token_response = self._session.post(
417
+ self._token_config.access_token_url,
418
+ data={"grant_type": "client_credentials", "scope": self._scope_value()},
419
+ auth=(self._token_config.client_id, self._token_config.client_secret),
420
+ timeout=self._token_timeout_seconds,
421
+ )
422
+
423
+ if (
424
+ token_response.status_code in self._retryable_status_codes
425
+ and attempt < self._token_max_retries
426
+ ):
427
+ self._sleep_with_backoff(attempt)
428
+ continue
429
+
430
+ token_response.raise_for_status()
431
+ return token_response.json()["access_token"]
432
+ except requests.exceptions.RequestException as e:
433
+ if attempt >= self._token_max_retries:
434
+ if token_response is not None:
435
+ print(token_response.text)
436
+ raise e
437
+ self._sleep_with_backoff(attempt)
438
+
439
+ raise RuntimeError("Unexpected retry state reached in _get_access_token")
440
+
441
+ def post(
442
+ self, path: str, json: dict = None, headers: dict = None, timeout: int = 30
443
+ ) -> requests.Response:
444
+ """Send a POST request to the API.
445
+
446
+ Args:
447
+ path (str): The endpoint path (relative to the base URL).
448
+ json (dict, optional): The JSON payload to include in the request. Defaults to None.
449
+ headers (dict, optional): Additional headers to include in the request. Defaults to None.
450
+ timeout (int, optional): The request timeout in seconds. Defaults to 30.
451
+
452
+ Returns:
453
+ requests.Response: The response object from the API.
454
+
455
+ Raises:
456
+ requests.exceptions.HTTPError: If the API returns an error response.
457
+ """
458
+ request_headers = dict(headers or {})
459
+ request_headers["Connection"] = "keep-alive"
460
+ return self._request_with_retry(
461
+ method="POST", path=path, json_payload=json, headers=request_headers, timeout=timeout
462
+ )
463
+
464
+ def get(
465
+ self, path: str, params: dict = None, headers: dict = None, timeout: int = 30
466
+ ) -> requests.Response:
467
+ """Send a GET request to the API.
468
+
469
+ Args:
470
+ path (str): The endpoint path (relative to the base URL).
471
+ params (dict, optional): Query parameters to include in the request. Defaults to None.
472
+ headers (dict, optional): Additional headers to include in the request. Defaults to None.
473
+ timeout (int, optional): The request timeout in seconds. Defaults to 30.
474
+
475
+ Returns:
476
+ requests.Response: The response object from the API.
477
+
478
+ Raises:
479
+ requests.exceptions.HTTPError: If the API returns an error response.
480
+ """
481
+ request_headers = dict(headers or {})
482
+ request_headers["accept"] = "application/json"
483
+ return self._request_with_retry(
484
+ method="GET", path=path, params=params, headers=request_headers, timeout=timeout
485
+ )
@@ -70,13 +70,16 @@ def _execute_query_with_backoff(
70
70
 
71
71
 
72
72
  def df_from_sharepoint_masterdata(
73
- list_name, row_limit=5000, site_name="VyMasterdata", internal_name=True
73
+ list_name, row_limit=5000, site_name="VyMasterdata", internal_name=True, get_by_id=False
74
74
  ) -> DataFrame:
75
75
  # Connect and fetch data
76
76
  ctx = _get_sharepoint_ctx(site_name)
77
77
 
78
78
  # Get the list and metadata
79
- s_list = ctx.web.lists.get_by_title(list_name)
79
+ if get_by_id:
80
+ s_list = ctx.web.lists.get_by_id(list_name)
81
+ else:
82
+ s_list = ctx.web.lists.get_by_title(list_name)
80
83
  s_list_fields = s_list.fields
81
84
  ctx.load(s_list_fields)
82
85
  _execute_query_with_backoff(ctx)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks_modules_vy
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Databricks modules for Vy
5
5
  Author-email: Sindre Kjetså <sindre.kjetsa@vy.no>
6
6
  Project-URL: Homepage, https://github.com/nsbno/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "databricks_modules_vy"
3
- version = "1.2.2"
3
+ version = "1.3.1"
4
4
  authors = [
5
5
  { name="Sindre Kjetså", email="sindre.kjetsa@vy.no" },
6
6
  ]
@@ -1,128 +0,0 @@
1
- from dataclasses import dataclass
2
- import requests
3
-
4
-
5
- @dataclass
6
- class TokenConfig:
7
- """A simple dataclass to hold the configuration for the Service Platform API.
8
-
9
- Attributes:
10
- client_id (str): The client ID for the Service Platform API.
11
- client_secret (str): The client secret for the Service Platform API.
12
- access_token_url (str): The URL for the access token endpoint.
13
- i.e. "https://auth.cognito.vydev.io/oauth2/token"
14
- scopes (str): The scopes you wish to authenticate with.
15
- """
16
-
17
- client_id: str
18
- client_secret: str
19
- access_token_url: str
20
- scopes: str
21
-
22
-
23
- class ServicePlatformApi:
24
- """Class to interact with the Service Platform API.
25
-
26
- This class provides methods to handle authentication and make HTTP GET and POST requests
27
- to the Service Platform API. It uses OAuth2 client credentials flow to obtain access tokens.
28
-
29
- Attributes:
30
- api_url (str): The base URL for the Service Platform API.
31
- token_config (TokenConfig): The configuration details for token generation.
32
- """
33
-
34
- def __init__(self, api_url: str, token_config: TokenConfig):
35
- """Initialize the ServicePlatformApi class.
36
-
37
- Args:
38
- api_url (str): The base URL for the Service Platform API.
39
- token_config (TokenConfig): An instance of TokenConfig containing authentication details.
40
-
41
- Raises:
42
- Exception: If the token generation fails.
43
- """
44
- self._api_url = api_url
45
- self._token_config = token_config
46
- self._access_token = self._get_access_token()
47
-
48
- def _get_access_token(self) -> str:
49
- token_response = requests.post(
50
- self._token_config.access_token_url,
51
- data={"grant_type": "client_credentials", "scope": " ".join(self._token_config.scopes)},
52
- auth=(self._token_config.client_id, self._token_config.client_secret),
53
- )
54
- try:
55
- token_response.raise_for_status()
56
- except Exception as e:
57
- print(e)
58
- print(token_response.text)
59
- raise e
60
- return token_response.json()["access_token"]
61
-
62
- def post(
63
- self, path: str, json: dict = None, headers: dict = None, timeout: int = 30
64
- ) -> requests.Response:
65
- """Send a POST request to the API.
66
-
67
- Args:
68
- path (str): The endpoint path (relative to the base URL).
69
- json (dict, optional): The JSON payload to include in the request. Defaults to None.
70
- headers (dict, optional): Additional headers to include in the request. Defaults to None.
71
- timeout (int, optional): The request timeout in seconds. Defaults to 30.
72
-
73
- Returns:
74
- requests.Response: The response object from the API.
75
-
76
- Raises:
77
- requests.exceptions.HTTPError: If the API returns an error response.
78
- """
79
- if headers is None:
80
- headers = {}
81
- headers["Authorization"] = f"Bearer {self._access_token}"
82
- headers["Connection"] = "close"
83
-
84
- api_response: requests.Response = requests.post(
85
- url=self._api_url + path, json=json, headers=headers, timeout=timeout
86
- )
87
- try:
88
- api_response.raise_for_status()
89
- except requests.exceptions.HTTPError as e:
90
- print(e)
91
- print(api_response.text)
92
- raise e
93
-
94
- return api_response
95
-
96
- def get(
97
- self, path: str, params: dict = None, headers: dict = None, timeout: int = 30
98
- ) -> requests.Response:
99
- """Send a GET request to the API.
100
-
101
- Args:
102
- path (str): The endpoint path (relative to the base URL).
103
- params (dict, optional): Query parameters to include in the request. Defaults to None.
104
- headers (dict, optional): Additional headers to include in the request. Defaults to None.
105
- timeout (int, optional): The request timeout in seconds. Defaults to 30.
106
-
107
- Returns:
108
- requests.Response: The response object from the API.
109
-
110
- Raises:
111
- requests.exceptions.HTTPError: If the API returns an error response.
112
- """
113
- if headers is None:
114
- headers = {}
115
- headers["Authorization"] = f"Bearer {self._access_token}"
116
- headers["accept"] = "application/json"
117
-
118
- api_response: requests.Response = requests.get(
119
- url=self._api_url + path, params=params, headers=headers, timeout=timeout
120
- )
121
- try:
122
- api_response.raise_for_status()
123
- except requests.exceptions.HTTPError as e:
124
- print(e)
125
- print(api_response.text)
126
- raise e
127
-
128
- return api_response