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.
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/PKG-INFO +1 -1
- databricks_modules_vy-1.3.1/databricks_modules_vy/service_platform.py +485 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/sharepoint.py +5 -2
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/PKG-INFO +1 -1
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/pyproject.toml +1 -1
- databricks_modules_vy-1.2.2/databricks_modules_vy/service_platform.py +0 -128
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/LICENSE +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/__init__.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/data_product.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/data_quality_tests.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/dataframe.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/extend_class.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/glue.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/logging.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/oracle.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/qlik.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/s3.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/snaplogic.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/styringsinfo_postgres.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/unity_catalog.py +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/SOURCES.txt +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/dependency_links.txt +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy.egg-info/top_level.txt +0 -0
- {databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/setup.cfg +0 -0
|
@@ -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
|
+
)
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/sharepoint.py
RENAMED
|
@@ -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
|
-
|
|
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,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
|
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/__init__.py
RENAMED
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/data_product.py
RENAMED
|
File without changes
|
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/dataframe.py
RENAMED
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/extend_class.py
RENAMED
|
File without changes
|
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/snaplogic.py
RENAMED
|
File without changes
|
|
File without changes
|
{databricks_modules_vy-1.2.2 → databricks_modules_vy-1.3.1}/databricks_modules_vy/unity_catalog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|