pydplus 1.0.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.
pydplus/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ :Module: pydplus
4
+ :Synopsis: This is the ``__init__`` module for the pydplus package
5
+ :Created By: Jeff Shurtliff
6
+ :Last Modified: Jeff Shurtliff
7
+ :Modified Date: 30 Mar 2026
8
+ """
9
+
10
+ from . import core
11
+ from .core import PyDPlus
12
+ from .utils import version
13
+
14
+ __all__ = ['core', 'PyDPlus']
15
+
16
+ # Define the package version by pulling from the pydplus.utils.version module
17
+ __version__ = version.get_full_version()
pydplus/api.py ADDED
@@ -0,0 +1,556 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ :Module: pydplus.api
4
+ :Synopsis: Defines the basic functions associated with the RSA ID Plus API
5
+ :Created By: Jeff Shurtliff
6
+ :Last Modified: Jeff Shurtliff
7
+ :Modified Date: 30 Mar 2026
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Optional, Union
14
+
15
+ import requests
16
+
17
+ from . import constants as const
18
+ from . import errors
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def get(
24
+ pydp_object,
25
+ endpoint: str,
26
+ params: Optional[dict] = None,
27
+ headers: Optional[dict] = None,
28
+ api_type: str = const.DEFAULT_API_TYPE,
29
+ timeout: int = const.DEFAULT_API_TIMEOUT_SECONDS,
30
+ show_full_error: bool = True,
31
+ return_json: bool = True,
32
+ allow_failed_response: Optional[bool] = None,
33
+ ):
34
+ """Perform a GET request against the ID Plus tenant.
35
+
36
+ :param pydp_object: The instantiated pydplus object
37
+ :type pydp_object: class[pydplus.PyDPlus]
38
+ :param endpoint: The API endpoint to query
39
+ :type endpoint: str
40
+ :param params: The query parameters (where applicable)
41
+ :type params: dict, None
42
+ :param headers: Specific API headers to use when performing the API call (beyond the base headers)
43
+ :type headers: dict, None
44
+ :param api_type: Indicates if the ``admin`` (default) or ``auth`` API will be leveraged.
45
+ :type api_type: str
46
+ :param timeout: The timeout period in seconds (defaults to ``30``)
47
+ :type timeout: int
48
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
49
+ :type show_full_error: bool
50
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
51
+ :type return_json: bool
52
+ :param allow_failed_response: Indicates that failed responses should return and should not raise an exception
53
+ (If not explicitly defined then ``True`` if Strict Mode is disabled)
54
+ :type allow_failed_response: bool, None
55
+ :returns: The API response in JSON format or as a ``requests`` object
56
+ :raises: :py:exc:`errors.exceptions.APIRequestError`,
57
+ :py:exc:`errors.exceptions.APIResponseConversionError`,
58
+ :py:exc:`errors.exceptions.InvalidFieldError`
59
+ """
60
+ # Define the parameters as an empty dictionary if none are provided
61
+ params = {} if params is None else params
62
+
63
+ # Define the headers
64
+ additional_headers = {} if headers is None else dict(headers)
65
+ request_headers = _get_headers(
66
+ pydp_object,
67
+ _additional_headers=additional_headers,
68
+ _api_type=api_type,
69
+ )
70
+
71
+ # Perform the API call
72
+ full_api_url = _get_full_api_url(pydp_object, endpoint, api_type)
73
+ response = requests.get(full_api_url, headers=request_headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl)
74
+
75
+ # Retry once after a forced OAuth token refresh when the token is rejected.
76
+ if _should_retry_oauth_401(pydp_object, api_type, response):
77
+ logger.debug('The OAuth token was rejected and will be refreshed before trying the API call again')
78
+ request_headers = _get_headers(
79
+ pydp_object,
80
+ _additional_headers=additional_headers,
81
+ _api_type=api_type,
82
+ _force_oauth_refresh=True,
83
+ )
84
+ response = requests.get(
85
+ full_api_url, headers=request_headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
86
+ )
87
+
88
+ # Examine the result
89
+ allow_failed_response = _should_allow_failed_responses(pydp_object, allow_failed_response)
90
+ if response.status_code >= 300 and not allow_failed_response:
91
+ _raise_status_code_exception(response, const.API_REQUEST_TYPES.GET, show_full_error)
92
+ if return_json:
93
+ response = _convert_response_to_json(response, allow_failed_response)
94
+ return response
95
+
96
+
97
+ def api_call_with_payload(
98
+ pydp_object,
99
+ method: str,
100
+ endpoint: str,
101
+ payload: Union[Optional[dict], Optional[str]] = None,
102
+ params: Optional[dict] = None,
103
+ headers: Optional[dict] = None,
104
+ api_type: str = const.DEFAULT_API_TYPE,
105
+ timeout: int = const.DEFAULT_API_TIMEOUT_SECONDS,
106
+ show_full_error: bool = True,
107
+ return_json: bool = True,
108
+ allow_failed_response: Optional[bool] = None,
109
+ ):
110
+ """Perform an API call with payload against the ID Plus tenant.
111
+
112
+ :param pydp_object: The instantiated pydplus object
113
+ :type pydp_object: class[pydplus.PyDPlus]
114
+ :param method: The API method (``post``, ``put``, or ``patch``)
115
+ :type method: str
116
+ :param endpoint: The API endpoint to query
117
+ :type endpoint: str
118
+ :param payload: The payload to leverage in the API call
119
+ :type payload: dict, str, None
120
+ :param params: The query parameters (where applicable)
121
+ :type params: dict, None
122
+ :param headers: Specific API headers to use when performing the API call (beyond the base headers)
123
+ :type headers: dict, None
124
+ :param api_type: Indicates if the ``admin`` (default) or ``auth`` API will be leveraged.
125
+ :type api_type: str
126
+ :param timeout: The timeout period in seconds (defaults to ``30``)
127
+ :type timeout: int
128
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
129
+ :type show_full_error: bool
130
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
131
+ :type return_json: bool
132
+ :param allow_failed_response: Indicates that failed responses should return and should not raise an exception
133
+ (If not explicitly defined then ``True`` if Strict Mode is disabled)
134
+ :type allow_failed_response: bool, None
135
+ :returns: The API response in JSON format or as a ``requests`` object
136
+ :raises: :py:exc:`TypeError`,
137
+ :py:exc:`errors.exceptions.APIMethodError`,
138
+ :py:exc:`errors.exceptions.APIRequestError`,
139
+ :py:exc:`errors.exceptions.APIResponseConversionError`,
140
+ :py:exc:`errors.exceptions.InvalidFieldError`
141
+ """
142
+
143
+ def _raise_exception_for_payload():
144
+ """Raise a :py:exc:`TypeError` exception when the payload is an invalid data type."""
145
+ _error_msg = f'The API payload must be a dictionary or string (provided: {type(payload)})'
146
+ logger.error(_error_msg)
147
+ raise TypeError(_error_msg)
148
+
149
+ # Define the parameters as an empty dictionary if none are provided
150
+ params = {} if params is None else params
151
+
152
+ # Define the headers
153
+ additional_headers = {} if headers is None else dict(headers)
154
+ request_headers = _get_headers(
155
+ pydp_object,
156
+ _additional_headers=additional_headers,
157
+ _api_type=api_type,
158
+ )
159
+
160
+ # Perform the API call
161
+ full_api_url = _get_full_api_url(pydp_object, endpoint, api_type)
162
+ response = _perform_api_call_with_payload(
163
+ pydp_object=pydp_object,
164
+ method=method,
165
+ payload=payload,
166
+ params=params,
167
+ headers=request_headers,
168
+ timeout=timeout,
169
+ full_api_url=full_api_url,
170
+ raise_payload_exception=_raise_exception_for_payload,
171
+ )
172
+
173
+ # Retry once after a forced OAuth token refresh when the token is rejected.
174
+ if response is not None and _should_retry_oauth_401(pydp_object, api_type, response):
175
+ request_headers = _get_headers(
176
+ pydp_object,
177
+ _additional_headers=additional_headers,
178
+ _api_type=api_type,
179
+ _force_oauth_refresh=True,
180
+ )
181
+ response = _perform_api_call_with_payload(
182
+ pydp_object=pydp_object,
183
+ method=method,
184
+ payload=payload,
185
+ params=params,
186
+ headers=request_headers,
187
+ timeout=timeout,
188
+ full_api_url=full_api_url,
189
+ raise_payload_exception=_raise_exception_for_payload,
190
+ )
191
+
192
+ # Examine the result
193
+ allow_failed_response = _should_allow_failed_responses(pydp_object, allow_failed_response)
194
+ if response and response.status_code >= 300 and not allow_failed_response:
195
+ _raise_status_code_exception(response, method, show_full_error)
196
+ if response and return_json:
197
+ response = _convert_response_to_json(response, allow_failed_response)
198
+ return response
199
+
200
+
201
+ def post(
202
+ pydp_object,
203
+ endpoint: str,
204
+ payload: Union[Optional[dict], Optional[str]] = None,
205
+ params: Optional[dict] = None,
206
+ headers: Optional[dict] = None,
207
+ api_type: str = const.DEFAULT_API_TYPE,
208
+ timeout: int = const.DEFAULT_API_TIMEOUT_SECONDS,
209
+ show_full_error: bool = True,
210
+ return_json: bool = True,
211
+ allow_failed_response: Optional[bool] = None,
212
+ ):
213
+ """Perform a POST call with payload against the ID Plus tenant.
214
+
215
+ :param pydp_object: The instantiated pydplus object
216
+ :type pydp_object: class[pydplus.PyDPlus]
217
+ :param endpoint: The API endpoint to query
218
+ :type endpoint: str
219
+ :param payload: The payload to leverage in the API call
220
+ :type payload: dict, str, None
221
+ :param params: The query parameters (where applicable)
222
+ :type params: dict, None
223
+ :param headers: Specific API headers to use when performing the API call (beyond the base headers)
224
+ :type headers: dict, None
225
+ :param api_type: Indicates if the ``admin`` (default) or ``auth`` API will be leveraged.
226
+ :type api_type: str
227
+ :param timeout: The timeout period in seconds (defaults to ``30``)
228
+ :type timeout: int
229
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
230
+ :type show_full_error: bool
231
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
232
+ :type return_json: bool
233
+ :param allow_failed_response: Indicates that failed responses should return and should not raise an exception
234
+ (If not explicitly defined then ``True`` if Strict Mode is disabled)
235
+ :type allow_failed_response: bool, None
236
+ :returns: The API response in JSON format or as a ``requests`` object
237
+ :raises: :py:exc:`errors.exceptions.APIMethodError`,
238
+ :py:exc:`errors.exceptions.APIRequestError`,
239
+ :py:exc:`errors.exceptions.APIResponseConversionError`,
240
+ :py:exc:`errors.exceptions.InvalidFieldError`
241
+ """
242
+ return api_call_with_payload(
243
+ pydp_object=pydp_object,
244
+ method=const.API_REQUEST_TYPES.POST,
245
+ endpoint=endpoint,
246
+ payload=payload,
247
+ params=params,
248
+ headers=headers,
249
+ api_type=api_type,
250
+ timeout=timeout,
251
+ show_full_error=show_full_error,
252
+ return_json=return_json,
253
+ allow_failed_response=allow_failed_response,
254
+ )
255
+
256
+
257
+ def patch(
258
+ pydp_object,
259
+ endpoint: str,
260
+ payload: Union[Optional[dict], Optional[str]] = None,
261
+ params: Optional[dict] = None,
262
+ headers: Optional[dict] = None,
263
+ api_type: str = const.DEFAULT_API_TYPE,
264
+ timeout: int = const.DEFAULT_API_TIMEOUT_SECONDS,
265
+ show_full_error: bool = True,
266
+ return_json: bool = True,
267
+ allow_failed_response: Optional[bool] = None,
268
+ ):
269
+ """Perform a PATCH call with payload against the ID Plus tenant.
270
+
271
+ :param pydp_object: The instantiated pydplus object
272
+ :type pydp_object: class[pydplus.PyDPlus]
273
+ :param endpoint: The API endpoint to query
274
+ :type endpoint: str
275
+ :param payload: The payload to leverage in the API call
276
+ :type payload: dict, str, None
277
+ :param params: The query parameters (where applicable)
278
+ :type params: dict, None
279
+ :param headers: Specific API headers to use when performing the API call (beyond the base headers)
280
+ :type headers: dict, None
281
+ :param api_type: Indicates if the ``admin`` (default) or ``auth`` API will be leveraged.
282
+ :type api_type: str
283
+ :param timeout: The timeout period in seconds (defaults to ``30``)
284
+ :type timeout: int
285
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
286
+ :type show_full_error: bool
287
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
288
+ :type return_json: bool
289
+ :param allow_failed_response: Indicates that failed responses should return and should not raise an exception
290
+ (If not explicitly defined then ``True`` if Strict Mode is disabled)
291
+ :type allow_failed_response: bool, None
292
+ :returns: The API response in JSON format or as a ``requests`` object
293
+ :raises: :py:exc:`errors.exceptions.APIMethodError`,
294
+ :py:exc:`errors.exceptions.APIRequestError`,
295
+ :py:exc:`errors.exceptions.APIResponseConversionError`,
296
+ :py:exc:`errors.exceptions.InvalidFieldError`
297
+ """
298
+ return api_call_with_payload(
299
+ pydp_object=pydp_object,
300
+ method=const.API_REQUEST_TYPES.PATCH,
301
+ endpoint=endpoint,
302
+ payload=payload,
303
+ params=params,
304
+ headers=headers,
305
+ api_type=api_type,
306
+ timeout=timeout,
307
+ show_full_error=show_full_error,
308
+ return_json=return_json,
309
+ allow_failed_response=allow_failed_response,
310
+ )
311
+
312
+
313
+ def put(
314
+ pydp_object,
315
+ endpoint: str,
316
+ payload: Union[Optional[dict], Optional[str]] = None,
317
+ params: Optional[dict] = None,
318
+ headers: Optional[dict] = None,
319
+ api_type: str = const.DEFAULT_API_TYPE,
320
+ timeout: int = const.DEFAULT_API_TIMEOUT_SECONDS,
321
+ show_full_error: bool = True,
322
+ return_json: bool = True,
323
+ allow_failed_response: Optional[bool] = None,
324
+ ):
325
+ """Perform a PUT call with payload against the ID Plus tenant.
326
+
327
+ :param pydp_object: The instantiated pydplus object
328
+ :type pydp_object: class[pydplus.PyDPlus]
329
+ :param endpoint: The API endpoint to query
330
+ :type endpoint: str
331
+ :param payload: The payload to leverage in the API call
332
+ :type payload: dict, str, None
333
+ :param params: The query parameters (where applicable)
334
+ :type params: dict, None
335
+ :param headers: Specific API headers to use when performing the API call (beyond the base headers)
336
+ :type headers: dict, None
337
+ :param api_type: Indicates if the ``admin`` (default) or ``auth`` API will be leveraged.
338
+ :type api_type: str
339
+ :param timeout: The timeout period in seconds (defaults to ``30``)
340
+ :type timeout: int
341
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
342
+ :type show_full_error: bool
343
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
344
+ :type return_json: bool
345
+ :param allow_failed_response: Indicates that failed responses should return and should not raise an exception
346
+ (If not explicitly defined then ``True`` if Strict Mode is disabled)
347
+ :type allow_failed_response: bool, None
348
+ :returns: The API response in JSON format or as a ``requests`` object
349
+ :raises: :py:exc:`errors.exceptions.APIMethodError`,
350
+ :py:exc:`errors.exceptions.APIRequestError`,
351
+ :py:exc:`errors.exceptions.APIResponseConversionError`,
352
+ :py:exc:`errors.exceptions.InvalidFieldError`
353
+ """
354
+ return api_call_with_payload(
355
+ pydp_object=pydp_object,
356
+ method=const.API_REQUEST_TYPES.PUT,
357
+ endpoint=endpoint,
358
+ payload=payload,
359
+ params=params,
360
+ headers=headers,
361
+ api_type=api_type,
362
+ timeout=timeout,
363
+ show_full_error=show_full_error,
364
+ return_json=return_json,
365
+ allow_failed_response=allow_failed_response,
366
+ )
367
+
368
+
369
+ def _should_allow_failed_responses(_pydp_object, _allow_failed_response: Optional[bool]) -> bool:
370
+ """Determine if failed responses are allowed based on the defined value or strict mode setting."""
371
+ # Only define the value if not already defined
372
+ if not isinstance(_allow_failed_response, bool) or _allow_failed_response is None:
373
+ try:
374
+ # Define the value based on the strict mode define in the instantiated object
375
+ _allow_failed_response = False if _pydp_object.strict_mode is True else True
376
+ except Exception as _exc:
377
+ # Use the default strict mode value to define the value if an exception is raised
378
+ _allow_failed_response = False if const.DEFAULT_STRICT_MODE is True else True
379
+ _exc_type = errors.handlers.get_exception_type(_exc)
380
+ _error_msg = f'Using default strict mode due to the following {_exc_type} exception: {_exc}'
381
+ logger.error(_error_msg)
382
+ return _allow_failed_response
383
+
384
+
385
+ def _is_admin_oauth_request(_pydp_object, _api_type: str) -> bool:
386
+ """Return whether the request targets Admin API over an OAuth connection."""
387
+ if not isinstance(_api_type, str):
388
+ return False
389
+ return (
390
+ _api_type.lower() == const.ADMIN_API_TYPE
391
+ and getattr(_pydp_object, const.CLIENT_SETTINGS.CONNECTION_TYPE, None) == const.CONNECTION_INFO.OAUTH
392
+ )
393
+
394
+
395
+ def _should_retry_oauth_401(_pydp_object, _api_type: str, _response) -> bool:
396
+ """Return whether a failed response is eligible for OAuth token refresh retry."""
397
+ return (
398
+ _is_admin_oauth_request(_pydp_object, _api_type)
399
+ and _response is not None
400
+ and getattr(_response, const.RESPONSE_KEYS.STATUS_CODE, None) == 401
401
+ )
402
+
403
+
404
+ def _get_headers(
405
+ _pydp_object,
406
+ _additional_headers: Optional[dict] = None,
407
+ _api_type: str = const.DEFAULT_API_TYPE,
408
+ _header_type: str = const.DEFAULT_HEADER_TYPE,
409
+ _force_oauth_refresh: bool = const.AUTH_VALUES.OAUTH_DEFAULT_FORCE_REFRESH,
410
+ ) -> dict:
411
+ """Return the appropriate HTTP headers to use for different types of API calls."""
412
+ _additional_headers = {} if _additional_headers is None else _additional_headers
413
+ _headers = dict(_pydp_object.base_headers) if isinstance(_pydp_object.base_headers, dict) else {}
414
+
415
+ if _is_admin_oauth_request(_pydp_object, _api_type):
416
+ if _force_oauth_refresh:
417
+ _headers = _pydp_object.refresh_oauth_token()
418
+ else:
419
+ _headers = _pydp_object._ensure_oauth_headers()
420
+
421
+ # TODO: Define additional headers as needed based on header type
422
+ _headers.update(_additional_headers)
423
+ return _headers
424
+
425
+
426
+ def _perform_api_call_with_payload(
427
+ pydp_object,
428
+ method: str,
429
+ payload: Union[Optional[dict], Optional[str]] = None,
430
+ params: Optional[dict] = None,
431
+ headers: Optional[dict] = None,
432
+ timeout: int = const.DEFAULT_API_TIMEOUT_SECONDS,
433
+ full_api_url: Optional[str] = None,
434
+ raise_payload_exception=None,
435
+ ):
436
+ """Perform API requests that include payload data and return the response object."""
437
+ if not full_api_url:
438
+ error_msg = 'A full API URL must be defined before calling _perform_api_call_with_payload()'
439
+ logger.error(error_msg)
440
+ raise errors.exceptions.APIMethodError(error_msg)
441
+
442
+ if isinstance(method, str) and method.upper() == const.API_REQUEST_TYPES.POST:
443
+ if isinstance(payload, dict):
444
+ return requests.post(
445
+ full_api_url, json=payload, headers=headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
446
+ )
447
+ if isinstance(payload, str):
448
+ return requests.post(
449
+ full_api_url, data=payload, headers=headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
450
+ )
451
+ if callable(raise_payload_exception):
452
+ raise_payload_exception()
453
+ elif isinstance(method, str) and method.upper() == const.API_REQUEST_TYPES.PATCH:
454
+ if isinstance(payload, dict):
455
+ return requests.patch(
456
+ full_api_url, json=payload, headers=headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
457
+ )
458
+ if isinstance(payload, str):
459
+ return requests.patch(
460
+ full_api_url, data=payload, headers=headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
461
+ )
462
+ if callable(raise_payload_exception):
463
+ raise_payload_exception()
464
+ elif isinstance(method, str) and method.upper() == const.API_REQUEST_TYPES.PUT:
465
+ if isinstance(payload, dict):
466
+ return requests.put(
467
+ full_api_url, json=payload, headers=headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
468
+ )
469
+ if isinstance(payload, str):
470
+ return requests.put(
471
+ full_api_url, data=payload, headers=headers, params=params, timeout=timeout, verify=pydp_object.verify_ssl
472
+ )
473
+ if callable(raise_payload_exception):
474
+ raise_payload_exception()
475
+ elif isinstance(method, str) and method.upper() == const.API_REQUEST_TYPES.GET:
476
+ error_msg = 'The GET API call method is not valid when a payload has been provided.'
477
+ logger.error(error_msg)
478
+ raise errors.exceptions.APIMethodError(error_msg)
479
+ else:
480
+ error_msg = 'A valid API call method (POST or PATCH or PUT) must be defined.'
481
+ logger.error(error_msg)
482
+ raise errors.exceptions.APIMethodError(error_msg)
483
+ return None
484
+
485
+
486
+ def _get_full_api_url(_pydp_object, _endpoint: str, _api_type: str = const.DEFAULT_API_TYPE) -> str:
487
+ """Construct the full API URL to use in an API call based on the API type.
488
+
489
+ :param _pydp_object: The instantiated pydplus object
490
+ :type _pydp_object: class[pydplus.PyDPlus]
491
+ :param _endpoint: The API endpoint to be called
492
+ :type _endpoint: str
493
+ :param _api_type: Indicates which API to leverage: ``admin`` (default) or ``auth``
494
+ :type _api_type: str
495
+ :returns: The full API URL path including the base URL as a string
496
+ :raises: :py:exc:`pydplus.errors.exceptions.InvalidFieldError`
497
+ """
498
+ # Define the base URL to leverage based on the API type or raise an exception if API type is invalid
499
+ if _api_type.lower() == const.ADMIN_API_TYPE:
500
+ _base_url = _pydp_object.admin_base_rest_url
501
+ elif _api_type.lower() == const.AUTH_API_TYPE:
502
+ _base_url = _pydp_object.auth_base_rest_url
503
+ else:
504
+ if not isinstance(_api_type, str):
505
+ _error_msg = f'The API Type value must be a string. (provided: {type(_api_type)})'
506
+ else:
507
+ _error_msg = f"The value '{_api_type}' is not a valid API type. "
508
+ _error_msg += f"(expected: '{const.ADMIN_API_TYPE}' or '{const.AUTH_API_TYPE}')"
509
+ logger.error(_error_msg)
510
+ raise errors.exceptions.InvalidFieldError(_error_msg)
511
+
512
+ # Make sure the endpoint begins with a slash
513
+ _endpoint = f'/{_endpoint}' if not _endpoint.startswith('/') else _endpoint
514
+
515
+ # Return the crafted full API URL
516
+ return f'{_base_url}{_endpoint}'
517
+
518
+
519
+ def _raise_status_code_exception(_response, _method: str, _show_full_error: bool = True) -> None:
520
+ """Raise an exception when a non-OK status code is returned for an API call.
521
+
522
+ :param _response: The API response
523
+ :param _method: The API request type (``GET``, ``POST``, ``PATCH``, ``PUT``, or ``DELETE``)
524
+ :type _method: str
525
+ :param _show_full_error: Determine if the full error message should be reported (``True`` by default)
526
+ :type _show_full_error: bool
527
+ :returns: None
528
+ :raises: :py:exc:`pydplus.errors.exceptions.APIRequestError`
529
+ """
530
+ _exc_msg = f'The {_method.upper()} request failed with a {_response.status_code} status code.'
531
+ if _show_full_error:
532
+ _exc_msg += f'\n{_response.text}'
533
+ logger.error(_exc_msg)
534
+ raise errors.exceptions.APIRequestError(_exc_msg)
535
+
536
+
537
+ def _convert_response_to_json(_response, _allow_failed_response: bool = False):
538
+ """Attempt to convert an API response to JSON format and raises an exception if unsuccessful.
539
+
540
+ :param _response: The API response
541
+ :param _allow_failed_response: Determines if failed responses are accepted (``False`` by default) or if an
542
+ exception should be raised if the conversion fails
543
+ :type _allow_failed_response: bool
544
+ :returns: The API response converted to a JSON dictionary (or returned unchanged if the conversion failed and
545
+ no exception was raised)
546
+ :raises: :py:exc:`pydplus.errors.exceptions.APIResponseConversionError`
547
+ """
548
+ try:
549
+ _response = _response.json()
550
+ except Exception as _exc:
551
+ _exc_type = errors.handlers.get_exception_type(_exc)
552
+ _error_msg = f'Failed to convert the API response to JSON format due to the following {_exc_type} exception: {_exc}'
553
+ logger.error(_error_msg)
554
+ if not _allow_failed_response:
555
+ raise errors.exceptions.APIResponseConversionError(_error_msg)
556
+ return _response