dpp-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dpp_sdk/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """Python SDK for Digital Product Passports (DPP).
2
+
3
+ Idiomatic Pydantic v2 port of the Java DPP SDK (see ../dpp-sdk-platform).
4
+
5
+ Sub-packages:
6
+ dpp_sdk.core -- reusable core DPP model, validation, identifiers
7
+ dpp_sdk.dpp4fun -- furniture-specific DPP aggregate, validation, JSON transport
8
+ dpp_sdk.clients -- HTTP clients for the DPP repository and registry APIs
9
+ """
10
+
11
+ from .core import (
12
+ Address,
13
+ Contact,
14
+ Documentation,
15
+ Dpp,
16
+ DppCore,
17
+ DppError,
18
+ DppMappingError,
19
+ DppValidationError,
20
+ Email,
21
+ Nameplate,
22
+ Organization,
23
+ OrganizationRole,
24
+ PassportMetadata,
25
+ Telephone,
26
+ validate_dpp_core,
27
+ )
28
+ from .dpp4fun import (
29
+ BillOfMaterials,
30
+ Characteristics,
31
+ Component,
32
+ Dimensions,
33
+ Dpp4Fun,
34
+ Dpp4FunJsonCodec,
35
+ Material,
36
+ Part,
37
+ ProductClassification,
38
+ from_json,
39
+ from_json_and_validate,
40
+ to_json,
41
+ validate_dpp4fun,
42
+ )
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ "Address",
48
+ "BillOfMaterials",
49
+ "Characteristics",
50
+ "Component",
51
+ "Contact",
52
+ "Dimensions",
53
+ "Documentation",
54
+ "Dpp",
55
+ "Dpp4Fun",
56
+ "Dpp4FunJsonCodec",
57
+ "DppCore",
58
+ "DppError",
59
+ "DppMappingError",
60
+ "DppValidationError",
61
+ "Email",
62
+ "Material",
63
+ "Nameplate",
64
+ "Organization",
65
+ "OrganizationRole",
66
+ "PassportMetadata",
67
+ "Part",
68
+ "ProductClassification",
69
+ "Telephone",
70
+ "__version__",
71
+ "from_json",
72
+ "from_json_and_validate",
73
+ "to_json",
74
+ "validate_dpp4fun",
75
+ "validate_dpp_core",
76
+ ]
@@ -0,0 +1,54 @@
1
+ """HTTP clients for the DPP repository and registry APIs (prEN-18222-aligned).
2
+
3
+ Port of `dpp.repo.client.*` / `dpp.registry.client.*` from
4
+ ../dpp-sdk-platform/dpp-sdk-clients.
5
+ """
6
+
7
+ from ._http import DppCodec, DppValidator
8
+ from .errors import (
9
+ DppApiClientError,
10
+ DppClientError,
11
+ DppHttpClientError,
12
+ DppMappingClientError,
13
+ DppNetworkClientError,
14
+ DppValidationClientError,
15
+ )
16
+ from .payloads import (
17
+ CreateDppResponse,
18
+ DeleteDppResponse,
19
+ DppApiMessage,
20
+ DppApiResponse,
21
+ DppStatusCode,
22
+ MessageType,
23
+ ReadDppIdsRequest,
24
+ ReadDppIdsResponse,
25
+ RegisterDppRequest,
26
+ RegisterDppResponse,
27
+ UpdateDataElementRequest,
28
+ )
29
+ from .registry import DppRegistryClient
30
+ from .repo import DppRepoClient
31
+
32
+ __all__ = [
33
+ "CreateDppResponse",
34
+ "DeleteDppResponse",
35
+ "DppApiClientError",
36
+ "DppApiMessage",
37
+ "DppApiResponse",
38
+ "DppClientError",
39
+ "DppCodec",
40
+ "DppHttpClientError",
41
+ "DppMappingClientError",
42
+ "DppNetworkClientError",
43
+ "DppRegistryClient",
44
+ "DppRepoClient",
45
+ "DppStatusCode",
46
+ "DppValidationClientError",
47
+ "DppValidator",
48
+ "MessageType",
49
+ "ReadDppIdsRequest",
50
+ "ReadDppIdsResponse",
51
+ "RegisterDppRequest",
52
+ "RegisterDppResponse",
53
+ "UpdateDataElementRequest",
54
+ ]
@@ -0,0 +1,143 @@
1
+ """Shared HTTP transport and JSON helpers (port of ``HttpSupport``).
2
+
3
+ Provides base-URL handling, URL encoding, the request/parse/validate pipeline for
4
+ the API wrapper envelope, and the ``DppCodec`` / ``DppValidator`` typing contracts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Protocol, TypeVar
10
+ from urllib.parse import quote
11
+
12
+ import httpx
13
+ from pydantic import ValidationError
14
+
15
+ from .errors import (
16
+ DppApiClientError,
17
+ DppHttpClientError,
18
+ DppMappingClientError,
19
+ DppNetworkClientError,
20
+ )
21
+ from .payloads import DppApiResponse
22
+
23
+ T = TypeVar("T")
24
+ T_contra = TypeVar("T_contra", contravariant=True)
25
+
26
+ DEFAULT_CONNECT_TIMEOUT = 5.0
27
+ DEFAULT_REQUEST_TIMEOUT = 15.0
28
+
29
+
30
+ class DppCodec(Protocol[T]):
31
+ """Converts a caller-owned DPP type to and from full JSON payloads."""
32
+
33
+ def to_json(self, dpp: T) -> str: ...
34
+
35
+ def from_json(self, raw: str) -> T: ...
36
+
37
+
38
+ class DppValidator(Protocol[T_contra]):
39
+ """Validates a caller-owned DPP type before a create request is sent."""
40
+
41
+ def __call__(self, dpp: T_contra) -> None: ...
42
+
43
+
44
+ def build_client() -> httpx.Client:
45
+ return httpx.Client(
46
+ timeout=httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
47
+ )
48
+
49
+
50
+ def normalize_base_url(base_url: str) -> str:
51
+ trimmed = base_url.strip()
52
+ if not trimmed:
53
+ raise ValueError("baseUrl must not be blank")
54
+ return trimmed.rstrip("/")
55
+
56
+
57
+ def resolve(base_url: str, path_and_query: str) -> str:
58
+ normalized = path_and_query if path_and_query else "/"
59
+ if not normalized.startswith("/"):
60
+ normalized = "/" + normalized
61
+ return base_url + normalized
62
+
63
+
64
+ def encode_segment(value: str) -> str:
65
+ return quote(value, safe="")
66
+
67
+
68
+ def request_api(
69
+ client: httpx.Client,
70
+ method: str,
71
+ url: str,
72
+ body: str | None,
73
+ ) -> DppApiResponse:
74
+ """Run the full request pipeline and return a validated success envelope."""
75
+ headers = {"Accept": "application/json"}
76
+ if body is not None:
77
+ headers["Content-Type"] = "application/json"
78
+ try:
79
+ response = client.request(method, url, content=body, headers=headers)
80
+ except httpx.TimeoutException as exc:
81
+ raise DppNetworkClientError("DPP HTTP request timed out") from exc
82
+ except httpx.TransportError as exc:
83
+ raise DppNetworkClientError("DPP HTTP request could not complete") from exc
84
+
85
+ _require_http_success(response)
86
+ api_response = _parse_api_response(response.text)
87
+ _require_api_success(api_response, response.text)
88
+ return api_response
89
+
90
+
91
+ def _require_http_success(response: httpx.Response) -> None:
92
+ if not 200 <= response.status_code < 300:
93
+ raise DppHttpClientError(
94
+ f"DPP endpoint returned non-success HTTP status {response.status_code}",
95
+ response.status_code,
96
+ response.text,
97
+ )
98
+
99
+
100
+ def _parse_api_response(raw_body: str) -> DppApiResponse:
101
+ try:
102
+ return DppApiResponse.model_validate_json(raw_body)
103
+ except ValidationError as exc:
104
+ raise DppMappingClientError("DPP API response could not be mapped") from exc
105
+
106
+
107
+ def _require_api_success(response: DppApiResponse, raw_body: str) -> None:
108
+ status_code = response.statusCode
109
+ if status_code is None:
110
+ raise DppMappingClientError("DPP API response could not be mapped: missing statusCode")
111
+ if not status_code.is_success:
112
+ raise DppApiClientError(
113
+ f"DPP API returned error status {status_code.value}",
114
+ status_code,
115
+ response.messages,
116
+ raw_body,
117
+ )
118
+
119
+
120
+ def require_payload(response: DppApiResponse) -> Any:
121
+ payload = response.payload
122
+ if payload is None:
123
+ raise _missing_field("payload")
124
+ return payload
125
+
126
+
127
+ def require_text_field(payload: Any, field_name: str, field_path: str) -> str:
128
+ if not isinstance(payload, dict):
129
+ raise _missing_field(field_path)
130
+ value = payload.get(field_name)
131
+ if not isinstance(value, str) or not value.strip():
132
+ raise _missing_field(field_path)
133
+ return value
134
+
135
+
136
+ def require_field(payload: Any, field_name: str, field_path: str) -> Any:
137
+ if not isinstance(payload, dict) or payload.get(field_name) is None:
138
+ raise _missing_field(field_path)
139
+ return payload[field_name]
140
+
141
+
142
+ def _missing_field(field_path: str) -> DppMappingClientError:
143
+ return DppMappingClientError(f"Missing required response field: {field_path}")
@@ -0,0 +1,49 @@
1
+ """Client exception hierarchy (port of ``dpp.*.client.exception``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from .payloads import DppApiMessage, DppStatusCode
9
+
10
+
11
+ class DppClientError(Exception):
12
+ """Base class for all DPP HTTP client errors."""
13
+
14
+
15
+ class DppValidationClientError(DppClientError):
16
+ """Raised when a DPP fails validation before a request is sent."""
17
+
18
+
19
+ class DppMappingClientError(DppClientError):
20
+ """Raised when request/response JSON cannot be serialized or parsed."""
21
+
22
+
23
+ class DppNetworkClientError(DppClientError):
24
+ """Raised when the HTTP request cannot complete (connection/timeout)."""
25
+
26
+
27
+ class DppHttpClientError(DppClientError):
28
+ """Raised when an endpoint returns a non-2xx HTTP status."""
29
+
30
+ def __init__(self, message: str, status_code: int, response_body: str) -> None:
31
+ super().__init__(message)
32
+ self.status_code = status_code
33
+ self.response_body = response_body
34
+
35
+
36
+ class DppApiClientError(DppClientError):
37
+ """Raised when a 2xx response carries an error status in the API wrapper."""
38
+
39
+ def __init__(
40
+ self,
41
+ message: str,
42
+ status_code: DppStatusCode | None,
43
+ messages: list[DppApiMessage] | None,
44
+ raw_response_body: str,
45
+ ) -> None:
46
+ super().__init__(message)
47
+ self.status_code = status_code
48
+ self.messages = messages
49
+ self.raw_response_body = raw_response_body
@@ -0,0 +1,118 @@
1
+ """API wrapper envelope and request/response DTOs for the DPP HTTP clients.
2
+
3
+ Ports ``dpp.repo.payloads.*`` and ``dpp.registry.payloads.*`` (which are
4
+ identical for the shared envelope) into a single module. These models use
5
+ ``extra='ignore'`` to mirror the clients' Jackson config, which disables
6
+ ``FAIL_ON_UNKNOWN_PROPERTIES``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime
12
+ from enum import StrEnum
13
+ from typing import Any
14
+
15
+ from pydantic import BaseModel, ConfigDict
16
+
17
+
18
+ class DppStatusCode(StrEnum):
19
+ """Application-level status in the API wrapper ``statusCode`` field."""
20
+
21
+ Success = "Success"
22
+ SuccessCreated = "SuccessCreated"
23
+ SuccessAccepted = "SuccessAccepted"
24
+ SuccessNoContent = "SuccessNoContent"
25
+ ClientErrorBadRequest = "ClientErrorBadRequest"
26
+ ClientNotAuthorized = "ClientNotAuthorized"
27
+ ClientForbidden = "ClientForbidden"
28
+ ClientMethodNotAllowed = "ClientMethodNotAllowed"
29
+ ClientErrorResourceNotFound = "ClientErrorResourceNotFound"
30
+ ClientResourceConflict = "ClientResourceConflict"
31
+ ServerInternalError = "ServerInternalError"
32
+ ServerErrorBadGateway = "ServerErrorBadGateway"
33
+
34
+ @classmethod
35
+ def _missing_(cls, value: object) -> DppStatusCode | None:
36
+ # Mirror the Java @JsonCreator aliases.
37
+ aliases = {
38
+ "ClientErrorNotAuthorized": cls.ClientNotAuthorized,
39
+ "ClientErrorForbidden": cls.ClientForbidden,
40
+ }
41
+ if isinstance(value, str):
42
+ return aliases.get(value)
43
+ return None
44
+
45
+ @property
46
+ def is_success(self) -> bool:
47
+ return self in {
48
+ DppStatusCode.Success,
49
+ DppStatusCode.SuccessCreated,
50
+ DppStatusCode.SuccessAccepted,
51
+ DppStatusCode.SuccessNoContent,
52
+ }
53
+
54
+
55
+ class MessageType(StrEnum):
56
+ """Severity/category for API wrapper messages."""
57
+
58
+ Info = "Info"
59
+ Warning = "Warning"
60
+ Error = "Error"
61
+ Exception = "Exception"
62
+
63
+
64
+ class _ClientBase(BaseModel):
65
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
66
+
67
+
68
+ class DppApiMessage(_ClientBase):
69
+ messageType: MessageType | None = None
70
+ text: str | None = None
71
+ code: str | None = None
72
+ correlationId: str | None = None
73
+ timestamp: datetime | None = None
74
+
75
+
76
+ class DppApiResponse(_ClientBase):
77
+ """Standard response envelope; ``payload`` is left as raw JSON (``Any``)."""
78
+
79
+ statusCode: DppStatusCode | None = None
80
+ payload: Any = None
81
+ messages: list[DppApiMessage] | None = None
82
+
83
+
84
+ # --- repository DTOs ------------------------------------------------------------
85
+ class CreateDppResponse(_ClientBase):
86
+ dppId: str | None = None
87
+
88
+
89
+ class DeleteDppResponse(_ClientBase):
90
+ statusCode: DppStatusCode | None = None
91
+ messages: list[DppApiMessage] | None = None
92
+
93
+
94
+ class ReadDppIdsRequest(_ClientBase):
95
+ productIdentifiers: list[str] | None = None
96
+ limit: int | None = None
97
+ cursor: str | None = None
98
+
99
+
100
+ class ReadDppIdsResponse(_ClientBase):
101
+ dppIdentifiers: list[str] | None = None
102
+ nextCursor: str | None = None
103
+
104
+
105
+ class UpdateDataElementRequest(_ClientBase):
106
+ payload: Any = None
107
+
108
+
109
+ # --- registry DTOs --------------------------------------------------------------
110
+ class RegisterDppRequest(_ClientBase):
111
+ productIdentifier: str | None = None
112
+ dppIdentifier: str | None = None
113
+ operatorIdentifier: str | None = None
114
+ repoUrl: str | None = None
115
+
116
+
117
+ class RegisterDppResponse(_ClientBase):
118
+ registryIdentifier: str | None = None
@@ -0,0 +1,25 @@
1
+ """HTTP client for the DPP registry API (port of ``HttpDppRegistryClient``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from . import _http
8
+ from .payloads import RegisterDppRequest, RegisterDppResponse
9
+
10
+ _REGISTER_PATH = "/registerDPP"
11
+
12
+
13
+ class DppRegistryClient:
14
+ def __init__(self, base_url: str, *, client: httpx.Client | None = None) -> None:
15
+ self._base_url = _http.normalize_base_url(base_url)
16
+ self._client = client if client is not None else _http.build_client()
17
+
18
+ def post_new_dpp_to_registry(self, request: RegisterDppRequest) -> RegisterDppResponse:
19
+ body = request.model_dump_json()
20
+ response = _http.request_api(
21
+ self._client, "POST", _http.resolve(self._base_url, _REGISTER_PATH), body
22
+ )
23
+ payload = _http.require_payload(response)
24
+ _http.require_text_field(payload, "registryIdentifier", "payload.registryIdentifier")
25
+ return RegisterDppResponse.model_validate(payload)
@@ -0,0 +1,130 @@
1
+ """HTTP client for the DPP repository API (port of ``HttpDppRepoClient``).
2
+
3
+ Generic over a caller-owned DPP model type ``T``, mediated by a :class:`DppCodec`
4
+ (full-payload JSON) and a validator callable. Endpoint paths and the request
5
+ pipeline match the Java client exactly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from datetime import datetime
12
+ from typing import Any, Generic, TypeVar
13
+
14
+ import httpx
15
+
16
+ from . import _http
17
+ from .errors import DppMappingClientError, DppValidationClientError
18
+ from .payloads import CreateDppResponse, DeleteDppResponse, ReadDppIdsRequest, ReadDppIdsResponse
19
+
20
+ T = TypeVar("T")
21
+
22
+ _DPPS_PATH = "/dpps"
23
+ _DPPS_BY_PRODUCT_ID_PATH = "/dppsByProductId/"
24
+ _DPPS_BY_PRODUCT_ID_AND_DATE_PATH = "/dppsByProductIdAndDate/"
25
+ _DPPS_BY_PRODUCT_IDS_PATH = "/dppsByProductIds"
26
+
27
+
28
+ class DppRepoClient(Generic[T]):
29
+ def __init__(
30
+ self,
31
+ base_url: str,
32
+ codec: _http.DppCodec[T],
33
+ validator: _http.DppValidator[T],
34
+ *,
35
+ client: httpx.Client | None = None,
36
+ ) -> None:
37
+ self._base_url = _http.normalize_base_url(base_url)
38
+ self._codec = codec
39
+ self._validator = validator
40
+ self._client = client if client is not None else _http.build_client()
41
+
42
+ # --- lifecycle --------------------------------------------------------------
43
+ def create_dpp(self, dpp: T) -> CreateDppResponse:
44
+ try:
45
+ self._validator(dpp)
46
+ except Exception as exc: # noqa: BLE001 - re-raised as a client error
47
+ raise DppValidationClientError("DPP validation failed before request") from exc
48
+ body = self._serialize(dpp)
49
+ response = self._send("POST", _DPPS_PATH, body)
50
+ payload = _http.require_payload(response)
51
+ _http.require_text_field(payload, "dppId", "payload.dppId")
52
+ return CreateDppResponse.model_validate(payload)
53
+
54
+ def read_dpp_by_id(self, dpp_id: str) -> T:
55
+ response = self._send("GET", self._dpp_path(dpp_id), None)
56
+ return self._decode_dpp_payload(response)
57
+
58
+ def read_dpp_by_product_id(self, product_id: str) -> T:
59
+ path = _DPPS_BY_PRODUCT_ID_PATH + _http.encode_segment(product_id)
60
+ response = self._send("GET", path, None)
61
+ return self._decode_dpp_payload(response)
62
+
63
+ def read_dpp_version_by_product_id_and_date(self, product_id: str, date: datetime) -> T:
64
+ path = (
65
+ _DPPS_BY_PRODUCT_ID_AND_DATE_PATH
66
+ + _http.encode_segment(product_id)
67
+ + "?date="
68
+ + _http.encode_segment(date.isoformat())
69
+ )
70
+ response = self._send("GET", path, None)
71
+ return self._decode_dpp_payload(response)
72
+
73
+ def read_dpp_ids_by_product_ids(
74
+ self, product_ids: list[str], limit: int | None = None, cursor: str | None = None
75
+ ) -> ReadDppIdsResponse:
76
+ request_body = ReadDppIdsRequest(
77
+ productIdentifiers=product_ids, limit=limit, cursor=cursor
78
+ )
79
+ response = self._send("POST", _DPPS_BY_PRODUCT_IDS_PATH, request_body.model_dump_json())
80
+ payload = _http.require_payload(response)
81
+ _http.require_field(payload, "dppIdentifiers", "payload.dppIdentifiers")
82
+ return ReadDppIdsResponse.model_validate(payload)
83
+
84
+ def update_dpp_by_id(self, dpp_id: str, partial_dpp: Any) -> T:
85
+ body = json.dumps(partial_dpp)
86
+ response = self._send("PATCH", self._dpp_path(dpp_id), body)
87
+ return self._decode_dpp_payload(response)
88
+
89
+ def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse:
90
+ response = self._send("DELETE", self._dpp_path(dpp_id), None)
91
+ return DeleteDppResponse(statusCode=response.statusCode, messages=response.messages)
92
+
93
+ # --- fine-granular elements -------------------------------------------------
94
+ def read_data_element(self, dpp_id: str, element_path: str) -> Any:
95
+ response = self._send("GET", self._data_element_path(dpp_id, element_path), None)
96
+ return _http.require_payload(response)
97
+
98
+ def update_data_element(self, dpp_id: str, element_path: str, payload: Any) -> Any:
99
+ body = json.dumps({"payload": payload})
100
+ response = self._send("PATCH", self._data_element_path(dpp_id, element_path), body)
101
+ return _http.require_payload(response)
102
+
103
+ # --- internals --------------------------------------------------------------
104
+ def _send(self, method: str, path: str, body: str | None) -> Any:
105
+ return _http.request_api(self._client, method, _http.resolve(self._base_url, path), body)
106
+
107
+ def _serialize(self, dpp: T) -> str:
108
+ try:
109
+ json_str = self._codec.to_json(dpp)
110
+ except Exception as exc: # noqa: BLE001 - re-raised as a client error
111
+ raise DppMappingClientError("DPP serialization failed before request") from exc
112
+ if json_str is None:
113
+ raise DppMappingClientError("codec returned null JSON")
114
+ return json_str
115
+
116
+ def _decode_dpp_payload(self, response: Any) -> T:
117
+ payload = _http.require_payload(response)
118
+ payload_json = json.dumps(payload)
119
+ try:
120
+ return self._codec.from_json(payload_json)
121
+ except Exception as exc: # noqa: BLE001 - re-raised as a client error
122
+ raise DppMappingClientError("DPP deserialization failed after response") from exc
123
+
124
+ @staticmethod
125
+ def _dpp_path(dpp_id: str) -> str:
126
+ return _DPPS_PATH + "/" + _http.encode_segment(dpp_id)
127
+
128
+ @classmethod
129
+ def _data_element_path(cls, dpp_id: str, element_path: str) -> str:
130
+ return cls._dpp_path(dpp_id) + "/elements/" + _http.encode_segment(element_path)
@@ -0,0 +1,38 @@
1
+ """Reusable core DPP model, validation, and identifiers.
2
+
3
+ Port of `dppsdk.core.*` from ../dpp-sdk-platform/dpp-datamodel/dpp-core.
4
+ """
5
+
6
+ from .errors import DppError, DppMappingError, DppValidationError
7
+ from .model import (
8
+ Address,
9
+ Contact,
10
+ Documentation,
11
+ Dpp,
12
+ DppCore,
13
+ Email,
14
+ Nameplate,
15
+ Organization,
16
+ OrganizationRole,
17
+ PassportMetadata,
18
+ Telephone,
19
+ )
20
+ from .validation import validate_dpp_core
21
+
22
+ __all__ = [
23
+ "Address",
24
+ "Contact",
25
+ "Documentation",
26
+ "Dpp",
27
+ "DppCore",
28
+ "DppError",
29
+ "DppMappingError",
30
+ "DppValidationError",
31
+ "Email",
32
+ "Nameplate",
33
+ "Organization",
34
+ "OrganizationRole",
35
+ "PassportMetadata",
36
+ "Telephone",
37
+ "validate_dpp_core",
38
+ ]
dpp_sdk/core/errors.py ADDED
@@ -0,0 +1,21 @@
1
+ """Exception hierarchy for the DPP datamodel.
2
+
3
+ Mirrors the Java SDK's split between mapping/transport failures and semantic
4
+ validation failures. Construction-time (structural) failures surface as
5
+ ``pydantic.ValidationError``; the semantic validators in
6
+ :mod:`dpp_sdk.core.validation` raise :class:`DppValidationError`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ class DppError(Exception):
13
+ """Base class for all datamodel errors raised by the SDK."""
14
+
15
+
16
+ class DppValidationError(DppError):
17
+ """Raised when a DPP fails a semantic validation rule (fail-fast)."""
18
+
19
+
20
+ class DppMappingError(DppError):
21
+ """Raised when a DPP cannot be serialized to or parsed from JSON."""