django-api-utility 0.1.0__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 (37) hide show
  1. django_api_utility-0.1.0/LICENSE +21 -0
  2. django_api_utility-0.1.0/MANIFEST.in +10 -0
  3. django_api_utility-0.1.0/PKG-INFO +72 -0
  4. django_api_utility-0.1.0/README.md +53 -0
  5. django_api_utility-0.1.0/config/external_definition_keys.json +1 -0
  6. django_api_utility-0.1.0/django_api_utility/__init__.py +68 -0
  7. django_api_utility-0.1.0/django_api_utility/apps.py +7 -0
  8. django_api_utility-0.1.0/django_api_utility/domain/__init__.py +15 -0
  9. django_api_utility-0.1.0/django_api_utility/domain/definition_keys.py +53 -0
  10. django_api_utility-0.1.0/django_api_utility/domain/endpoint_service.py +34 -0
  11. django_api_utility-0.1.0/django_api_utility/domain/schema_service.py +58 -0
  12. django_api_utility-0.1.0/django_api_utility/domain/token_service.py +125 -0
  13. django_api_utility-0.1.0/django_api_utility/domain/types.py +14 -0
  14. django_api_utility-0.1.0/django_api_utility/exceptions.py +34 -0
  15. django_api_utility-0.1.0/django_api_utility/migrations/0001_initial.py +110 -0
  16. django_api_utility-0.1.0/django_api_utility/migrations/__init__.py +1 -0
  17. django_api_utility-0.1.0/django_api_utility/models.py +142 -0
  18. django_api_utility-0.1.0/django_api_utility/orchestration/__init__.py +12 -0
  19. django_api_utility-0.1.0/django_api_utility/orchestration/api_service.py +234 -0
  20. django_api_utility-0.1.0/django_api_utility/tests/__init__.py +0 -0
  21. django_api_utility-0.1.0/django_api_utility/tests/settings.py +16 -0
  22. django_api_utility-0.1.0/django_api_utility/tests/test_api_service.py +150 -0
  23. django_api_utility-0.1.0/django_api_utility/tests/test_definition_keys.py +39 -0
  24. django_api_utility-0.1.0/django_api_utility/tests/test_endpoint_service.py +27 -0
  25. django_api_utility-0.1.0/django_api_utility/tests/test_http_client.py +44 -0
  26. django_api_utility-0.1.0/django_api_utility/transport/__init__.py +6 -0
  27. django_api_utility-0.1.0/django_api_utility/transport/http_client.py +31 -0
  28. django_api_utility-0.1.0/django_api_utility/transport/outbound_service.py +25 -0
  29. django_api_utility-0.1.0/django_api_utility.egg-info/PKG-INFO +72 -0
  30. django_api_utility-0.1.0/django_api_utility.egg-info/SOURCES.txt +35 -0
  31. django_api_utility-0.1.0/django_api_utility.egg-info/dependency_links.txt +1 -0
  32. django_api_utility-0.1.0/django_api_utility.egg-info/requires.txt +8 -0
  33. django_api_utility-0.1.0/django_api_utility.egg-info/top_level.txt +1 -0
  34. django_api_utility-0.1.0/pyproject.toml +36 -0
  35. django_api_utility-0.1.0/requirements.txt +4 -0
  36. django_api_utility-0.1.0/setup.cfg +4 -0
  37. django_api_utility-0.1.0/setup.py +12 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Django MQTT Publisher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include README.md
2
+ include LICENSE
3
+ include MANIFEST.in
4
+ include pyproject.toml
5
+ include setup.py
6
+ include requirements.txt
7
+ recursive-include django_api_utility *.py
8
+ recursive-include config *.json
9
+ global-exclude __pycache__
10
+ global-exclude *.py[co]
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-api-utility
3
+ Version: 0.1.0
4
+ Summary: Reusable API integration utility for Django services
5
+ Author: Nexgensis
6
+ License: MIT
7
+ Project-URL: Repository, https://example.com/django-api-utility
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: Django>=3.2
12
+ Requires-Dist: requests>=2.28
13
+ Requires-Dist: cryptography>=41.0
14
+ Requires-Dist: jsonschema>=4.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7.0; extra == "test"
17
+ Requires-Dist: pytest-django>=4.5; extra == "test"
18
+ Dynamic: license-file
19
+
20
+ # django-api-utility
21
+
22
+ Reusable API integration utility for Django services.
23
+
24
+ ## Features
25
+ - Service + endpoint registry models
26
+ - Definition-key based endpoint resolution
27
+ - OAuth token fetch/refresh flow with encrypted token storage
28
+ - Consistent utility exceptions for config, token, and request errors
29
+ - Thin request wrapper with retry support
30
+
31
+ ## Install
32
+ ```bash
33
+ pip install -e .
34
+ ```
35
+
36
+ ## Django setup
37
+ Add app:
38
+ ```python
39
+ INSTALLED_APPS = [
40
+ # ...
41
+ "django_api_utility",
42
+ ]
43
+ ```
44
+
45
+ ## Definition key mapping
46
+ Provide a JSON mapping file via `EXTERNAL_DEFINITION_KEYS_FILE`, or use the packaged default at `config/external_definition_keys.json`.
47
+
48
+ Example:
49
+ ```json
50
+ {
51
+ "users_get": "service_users_get"
52
+ }
53
+ ```
54
+
55
+ ## Usage
56
+ ```python
57
+ from django_api_utility import get_by_key, post_by_key
58
+
59
+ response = get_by_key("users_get", params={"page": 1})
60
+ create_response = post_by_key("users_create", payload={"name": "Alice"})
61
+ ```
62
+
63
+ ## Internal layout
64
+ - `django_api_utility/transport/`: HTTP transport and outbound execution primitives
65
+ - `django_api_utility/domain/`: definition keys, endpoint resolution, token lifecycle
66
+ - `django_api_utility/orchestration/`: request orchestration with retries and token refresh fallback
67
+
68
+ ## Tests
69
+ ```bash
70
+ pip install -e ".[test]"
71
+ pytest -q
72
+ ```
@@ -0,0 +1,53 @@
1
+ # django-api-utility
2
+
3
+ Reusable API integration utility for Django services.
4
+
5
+ ## Features
6
+ - Service + endpoint registry models
7
+ - Definition-key based endpoint resolution
8
+ - OAuth token fetch/refresh flow with encrypted token storage
9
+ - Consistent utility exceptions for config, token, and request errors
10
+ - Thin request wrapper with retry support
11
+
12
+ ## Install
13
+ ```bash
14
+ pip install -e .
15
+ ```
16
+
17
+ ## Django setup
18
+ Add app:
19
+ ```python
20
+ INSTALLED_APPS = [
21
+ # ...
22
+ "django_api_utility",
23
+ ]
24
+ ```
25
+
26
+ ## Definition key mapping
27
+ Provide a JSON mapping file via `EXTERNAL_DEFINITION_KEYS_FILE`, or use the packaged default at `config/external_definition_keys.json`.
28
+
29
+ Example:
30
+ ```json
31
+ {
32
+ "users_get": "service_users_get"
33
+ }
34
+ ```
35
+
36
+ ## Usage
37
+ ```python
38
+ from django_api_utility import get_by_key, post_by_key
39
+
40
+ response = get_by_key("users_get", params={"page": 1})
41
+ create_response = post_by_key("users_create", payload={"name": "Alice"})
42
+ ```
43
+
44
+ ## Internal layout
45
+ - `django_api_utility/transport/`: HTTP transport and outbound execution primitives
46
+ - `django_api_utility/domain/`: definition keys, endpoint resolution, token lifecycle
47
+ - `django_api_utility/orchestration/`: request orchestration with retries and token refresh fallback
48
+
49
+ ## Tests
50
+ ```bash
51
+ pip install -e ".[test]"
52
+ pytest -q
53
+ ```
@@ -0,0 +1,68 @@
1
+ """Reusable API integration utility for Django services."""
2
+
3
+ from .exceptions import (
4
+ ApiUtilityError,
5
+ ConfigurationError,
6
+ DefinitionKeyError,
7
+ EndpointConfigError,
8
+ EndpointRequestError,
9
+ HttpClientError,
10
+ TokenConfigError,
11
+ TokenRequestError,
12
+ SchemaValidationError,
13
+ )
14
+
15
+
16
+ def request_by_key(*args, **kwargs):
17
+ from .orchestration.api_service import request_by_key as _request_by_key
18
+
19
+ return _request_by_key(*args, **kwargs)
20
+
21
+
22
+ def get_by_key(*args, **kwargs):
23
+ from .orchestration.api_service import get_by_key as _get_by_key
24
+
25
+ return _get_by_key(*args, **kwargs)
26
+
27
+
28
+ def post_by_key(*args, **kwargs):
29
+ from .orchestration.api_service import post_by_key as _post_by_key
30
+
31
+ return _post_by_key(*args, **kwargs)
32
+
33
+
34
+ def put_by_key(*args, **kwargs):
35
+ from .orchestration.api_service import put_by_key as _put_by_key
36
+
37
+ return _put_by_key(*args, **kwargs)
38
+
39
+
40
+ def patch_by_key(*args, **kwargs):
41
+ from .orchestration.api_service import patch_by_key as _patch_by_key
42
+
43
+ return _patch_by_key(*args, **kwargs)
44
+
45
+
46
+ def delete_by_key(*args, **kwargs):
47
+ from .orchestration.api_service import delete_by_key as _delete_by_key
48
+
49
+ return _delete_by_key(*args, **kwargs)
50
+
51
+
52
+ __all__ = [
53
+ "ApiUtilityError",
54
+ "ConfigurationError",
55
+ "DefinitionKeyError",
56
+ "EndpointConfigError",
57
+ "EndpointRequestError",
58
+ "HttpClientError",
59
+ "TokenConfigError",
60
+ "TokenRequestError",
61
+ "SchemaValidationError",
62
+ "request_by_key",
63
+ "get_by_key",
64
+ "post_by_key",
65
+ "put_by_key",
66
+ "patch_by_key",
67
+ "delete_by_key",
68
+ ]
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoApiUtilityConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "django_api_utility"
7
+ verbose_name = "Django API Utility"
@@ -0,0 +1,15 @@
1
+ """Domain layer: config lookup, endpoint resolution, and token flow."""
2
+
3
+ from .definition_keys import clear_definition_key_cache, get_definition_key
4
+ from .endpoint_service import build_url, resolve_endpoint
5
+ from .token_service import get_or_refresh_service_token
6
+ from .types import ResolvedEndpoint
7
+
8
+ __all__ = [
9
+ "get_definition_key",
10
+ "clear_definition_key_cache",
11
+ "build_url",
12
+ "resolve_endpoint",
13
+ "get_or_refresh_service_token",
14
+ "ResolvedEndpoint",
15
+ ]
@@ -0,0 +1,53 @@
1
+ import json
2
+ import os
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+
6
+ from django.conf import settings
7
+
8
+ from django_api_utility.exceptions import DefinitionKeyError
9
+
10
+
11
+ _DEFAULT_KEYS_FILE = Path(__file__).resolve().parents[2] / "config" / "external_definition_keys.json"
12
+
13
+
14
+ def _keys_file_path() -> Path:
15
+ configured = (
16
+ getattr(settings, "EXTERNAL_DEFINITION_KEYS_FILE", "")
17
+ or os.getenv("EXTERNAL_DEFINITION_KEYS_FILE", "")
18
+ or str(_DEFAULT_KEYS_FILE)
19
+ )
20
+ return Path(configured)
21
+
22
+
23
+ @lru_cache(maxsize=1)
24
+ def _load_keys() -> dict:
25
+ keys_file = _keys_file_path()
26
+ if not keys_file.exists():
27
+ raise DefinitionKeyError(
28
+ f"Definition key config file not found at {keys_file}. "
29
+ "Set EXTERNAL_DEFINITION_KEYS_FILE to a valid JSON file."
30
+ )
31
+
32
+ try:
33
+ with keys_file.open("r", encoding="utf-8") as file_obj:
34
+ data = json.load(file_obj) or {}
35
+ except json.JSONDecodeError as exc:
36
+ raise DefinitionKeyError(f"Definition key file is invalid JSON at {keys_file}: {exc}") from exc
37
+
38
+ if not isinstance(data, dict):
39
+ raise DefinitionKeyError("external_definition_keys.json must contain a JSON object")
40
+ return data
41
+
42
+
43
+ def get_definition_key(name: str) -> str:
44
+ """Resolve runtime key alias to configured endpoint definition key."""
45
+ keys = _load_keys()
46
+ value = keys.get(name)
47
+ if not value:
48
+ raise DefinitionKeyError(f"Definition key '{name}' not found in {_keys_file_path()}")
49
+ return str(value)
50
+
51
+
52
+ def clear_definition_key_cache() -> None:
53
+ _load_keys.cache_clear()
@@ -0,0 +1,34 @@
1
+ from django_api_utility.exceptions import EndpointConfigError
2
+ from django_api_utility.models import ExternalServiceEndpoint
3
+
4
+ from .token_service import get_or_refresh_service_token
5
+ from .types import ResolvedEndpoint
6
+
7
+
8
+ def build_url(base_url: str, path: str) -> str:
9
+ """Join base URL and endpoint path with normalized slashes."""
10
+ return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
11
+
12
+
13
+ def resolve_endpoint(definition_key: str, force_token_refresh: bool = False) -> ResolvedEndpoint:
14
+ """Resolve active endpoint config and authorization headers for execution."""
15
+ endpoint = (
16
+ ExternalServiceEndpoint.objects.select_related("service")
17
+ .filter(definition_key=definition_key, is_active=True, service__is_active=True)
18
+ .first()
19
+ )
20
+ if not endpoint:
21
+ raise EndpointConfigError(f"Active endpoint definition_key '{definition_key}' is not configured")
22
+
23
+ headers: dict[str, str] = {}
24
+ if endpoint.service.auth_required:
25
+ token = get_or_refresh_service_token(endpoint.service, force_refresh=force_token_refresh)
26
+ headers["Authorization"] = f"Bearer {token}"
27
+
28
+ timeout = endpoint.timeout_seconds or endpoint.service.default_timeout_seconds or 10
29
+ return ResolvedEndpoint(
30
+ endpoint=endpoint,
31
+ url=build_url(endpoint.service.base_url, endpoint.path),
32
+ timeout=timeout,
33
+ headers=headers,
34
+ )
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from django.conf import settings
6
+
7
+ from django_api_utility.exceptions import SchemaValidationError
8
+ from django_api_utility.models import ExternalEndpointSchema
9
+
10
+ try:
11
+ from jsonschema import ValidationError as JsonSchemaValidationError
12
+ from jsonschema import validate as jsonschema_validate
13
+ except Exception: # pragma: no cover - import failure path
14
+ JsonSchemaValidationError = Exception
15
+ jsonschema_validate = None
16
+
17
+
18
+ def schema_validation_enabled() -> bool:
19
+ return bool(getattr(settings, "API_UTILITY_VALIDATE_SCHEMAS", True))
20
+
21
+
22
+ def _get_active_schema(endpoint, schema_kind: str) -> Optional[ExternalEndpointSchema]:
23
+ # Supports tests that pass mocked/non-model endpoints.
24
+ endpoint_pk = getattr(endpoint, "pk", None)
25
+ if not endpoint_pk:
26
+ return None
27
+
28
+ return (
29
+ ExternalEndpointSchema.objects.filter(
30
+ endpoint=endpoint,
31
+ schema_kind=schema_kind,
32
+ is_active=True,
33
+ )
34
+ .order_by("-updated_at")
35
+ .first()
36
+ )
37
+
38
+
39
+ def validate_payload_schema(*, definition_key: str, endpoint, schema_kind: str, payload: Any) -> None:
40
+ if not schema_validation_enabled():
41
+ return
42
+
43
+ schema_row = _get_active_schema(endpoint, schema_kind)
44
+ if not schema_row:
45
+ return
46
+
47
+ if jsonschema_validate is None:
48
+ raise SchemaValidationError(
49
+ f"Schema validation requested for '{definition_key}' but jsonschema dependency is unavailable"
50
+ )
51
+
52
+ schema_json = schema_row.schema_json or {}
53
+ try:
54
+ jsonschema_validate(instance=payload, schema=schema_json)
55
+ except JsonSchemaValidationError as exc:
56
+ raise SchemaValidationError(
57
+ f"{schema_kind.title()} schema validation failed for '{definition_key}': {exc.message}"
58
+ ) from exc
@@ -0,0 +1,125 @@
1
+ from datetime import timedelta
2
+
3
+ from django.conf import settings
4
+ from django.utils import timezone
5
+
6
+ from django_api_utility.exceptions import HttpClientError, TokenConfigError, TokenRequestError
7
+ from django_api_utility.models import ExternalServiceEndpoint, IntegrationServiceTokenConfig
8
+ from django_api_utility.transport.outbound_service import execute_outbound_request
9
+
10
+
11
+ def _get_token_config_for_service(service):
12
+ return (
13
+ IntegrationServiceTokenConfig.objects.filter(service=service, is_active=True)
14
+ .order_by("-updated_at")
15
+ .first()
16
+ )
17
+
18
+
19
+ def _resolve_token_url(service) -> str:
20
+ configured_key = str(getattr(settings, "API_UTILITY_TOKEN_DEFINITION_KEY", "request_service_token") or "").strip()
21
+ if not configured_key:
22
+ configured_key = "request_service_token"
23
+
24
+ service_key = str(service.name or "").strip().lower().replace(" ", "_")
25
+ candidate_keys = [
26
+ configured_key,
27
+ "request_service_token",
28
+ "request_token",
29
+ f"{service_key}_{configured_key}",
30
+ f"{service_key}_request_service_token",
31
+ f"{service_key}_request_token",
32
+ ]
33
+
34
+ token_endpoint = None
35
+ for key in candidate_keys:
36
+ token_endpoint = (
37
+ ExternalServiceEndpoint.objects.filter(
38
+ service=service,
39
+ is_active=True,
40
+ method__iexact="POST",
41
+ definition_key__iexact=key,
42
+ )
43
+ .order_by("id")
44
+ .first()
45
+ )
46
+ if token_endpoint:
47
+ break
48
+
49
+ if not token_endpoint:
50
+ raise TokenConfigError(
51
+ f"Token endpoint not configured in ExternalServiceEndpoint for service '{service.name}'. "
52
+ f"Expected definition_key one of: {candidate_keys}"
53
+ )
54
+
55
+ return f"{service.base_url.rstrip('/')}/{token_endpoint.path.lstrip('/')}"
56
+
57
+
58
+ def _request_new_service_token(config, service) -> str:
59
+ token_service = config.service or service
60
+ if config.service_id and config.service_id != service.id:
61
+ raise TokenConfigError(
62
+ f"Token config service mismatch. Config is for '{config.service.name}', "
63
+ f"requested service is '{service.name}'."
64
+ )
65
+
66
+ token_url = _resolve_token_url(token_service)
67
+ grant_type = str(getattr(settings, "API_UTILITY_TOKEN_GRANT_TYPE", "client_credentials") or "client_credentials")
68
+ scope = str(getattr(settings, "API_UTILITY_TOKEN_SCOPE", "") or "").strip()
69
+
70
+ payload = {
71
+ "grant_type": grant_type,
72
+ "client_id": config.client_id,
73
+ "client_secret": config.get_client_secret(),
74
+ }
75
+ if scope:
76
+ payload["scope"] = scope
77
+
78
+ timeout = int(getattr(settings, "API_UTILITY_TOKEN_TIMEOUT_SECONDS", 10) or 10)
79
+ try:
80
+ response = execute_outbound_request(
81
+ method="POST",
82
+ url=token_url,
83
+ form_data=payload,
84
+ timeout=timeout,
85
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
86
+ )
87
+ except HttpClientError as exc:
88
+ raise TokenRequestError(f"Token request failed for service '{token_service.name}': {exc}") from exc
89
+
90
+ if response.status_code >= 400:
91
+ raise TokenRequestError(
92
+ f"Token request failed for service '{token_service.name}' "
93
+ f"with status {response.status_code}: {response.text[:500]}"
94
+ )
95
+
96
+ try:
97
+ body = response.json() or {}
98
+ except ValueError as exc:
99
+ raise TokenRequestError(f"Token response for service '{token_service.name}' is not valid JSON") from exc
100
+
101
+ access_token = str(body.get("access_token") or "").strip()
102
+ if not access_token:
103
+ raise TokenRequestError(f"Token response for service '{token_service.name}' missing access_token")
104
+
105
+ expires_in = int(body.get("expires_in") or getattr(settings, "API_UTILITY_DEFAULT_TOKEN_EXPIRES_IN", 3600))
106
+ config.set_service_token(access_token)
107
+ config.token_expiry = timezone.now() + timedelta(seconds=max(expires_in, 1))
108
+ config.save(update_fields=["service_token", "token_expiry", "updated_at"])
109
+ return access_token
110
+
111
+
112
+ def get_or_refresh_service_token(service, force_refresh: bool = False) -> str:
113
+ """Return valid token for service and refresh it when required."""
114
+ config = _get_token_config_for_service(service)
115
+ if not config:
116
+ raise TokenConfigError(
117
+ f"No active IntegrationServiceTokenConfig found for service '{service.name}'. "
118
+ "Save token config with matching service first."
119
+ )
120
+
121
+ buffer_seconds = int(getattr(settings, "API_UTILITY_TOKEN_EXPIRY_BUFFER_SECONDS", 60) or 60)
122
+ if not force_refresh and config.has_valid_service_token(buffer_seconds=buffer_seconds):
123
+ return config.get_service_token()
124
+
125
+ return _request_new_service_token(config=config, service=service)
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict
3
+
4
+ from django_api_utility.models import ExternalServiceEndpoint
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ResolvedEndpoint:
9
+ """Resolved endpoint metadata required to execute a request."""
10
+
11
+ endpoint: ExternalServiceEndpoint
12
+ url: str
13
+ timeout: int
14
+ headers: Dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,34 @@
1
+ class ApiUtilityError(Exception):
2
+ """Base exception for the utility."""
3
+
4
+
5
+ class ConfigurationError(ApiUtilityError):
6
+ """Raised when static utility configuration is invalid."""
7
+
8
+
9
+ class DefinitionKeyError(ConfigurationError):
10
+ """Raised when a definition key cannot be loaded/resolved."""
11
+
12
+
13
+ class EndpointConfigError(ConfigurationError):
14
+ """Raised when endpoint config is missing or invalid."""
15
+
16
+
17
+ class TokenConfigError(ConfigurationError):
18
+ """Raised when token config is missing or invalid."""
19
+
20
+
21
+ class HttpClientError(ApiUtilityError):
22
+ """Raised when an outbound HTTP call fails at transport layer."""
23
+
24
+
25
+ class TokenRequestError(ApiUtilityError):
26
+ """Raised when token endpoint request/response is invalid."""
27
+
28
+
29
+ class EndpointRequestError(ApiUtilityError):
30
+ """Raised when endpoint request execution fails."""
31
+
32
+
33
+ class SchemaValidationError(EndpointRequestError):
34
+ """Raised when request/response schema validation fails."""