pinexq-client 0.2.0.2024.607.8__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.
Files changed (49) hide show
  1. hypermedia_client/core/__init__.py +8 -0
  2. hypermedia_client/core/base_relations.py +8 -0
  3. hypermedia_client/core/enterapi.py +17 -0
  4. hypermedia_client/core/exceptions.py +2 -0
  5. hypermedia_client/core/hco/__init__.py +0 -0
  6. hypermedia_client/core/hco/action_hco.py +70 -0
  7. hypermedia_client/core/hco/action_with_parameters_hco.py +86 -0
  8. hypermedia_client/core/hco/download_link_hco.py +37 -0
  9. hypermedia_client/core/hco/hco_base.py +91 -0
  10. hypermedia_client/core/hco/link_hco.py +57 -0
  11. hypermedia_client/core/hco/upload_action_hco.py +113 -0
  12. hypermedia_client/core/http_headers.py +9 -0
  13. hypermedia_client/core/media_types.py +24 -0
  14. hypermedia_client/core/model/__init__.py +0 -0
  15. hypermedia_client/core/model/error.py +9 -0
  16. hypermedia_client/core/model/sirenmodels.py +155 -0
  17. hypermedia_client/core/polling.py +37 -0
  18. hypermedia_client/core/sirenaccess.py +173 -0
  19. hypermedia_client/job_management/__init__.py +6 -0
  20. hypermedia_client/job_management/enterjma.py +42 -0
  21. hypermedia_client/job_management/hcos/__init__.py +12 -0
  22. hypermedia_client/job_management/hcos/entrypoint_hco.py +57 -0
  23. hypermedia_client/job_management/hcos/info_hco.py +42 -0
  24. hypermedia_client/job_management/hcos/input_dataslot_hco.py +82 -0
  25. hypermedia_client/job_management/hcos/job_hco.py +174 -0
  26. hypermedia_client/job_management/hcos/job_query_result_hco.py +63 -0
  27. hypermedia_client/job_management/hcos/job_used_tags_hco.py +30 -0
  28. hypermedia_client/job_management/hcos/jobsroot_hco.py +80 -0
  29. hypermedia_client/job_management/hcos/output_dataslot_hco.py +44 -0
  30. hypermedia_client/job_management/hcos/processing_step_hco.py +71 -0
  31. hypermedia_client/job_management/hcos/processing_step_used_tags_hco.py +30 -0
  32. hypermedia_client/job_management/hcos/processingstep_query_result_hco.py +68 -0
  33. hypermedia_client/job_management/hcos/processingsteproot_hco.py +72 -0
  34. hypermedia_client/job_management/hcos/user_hco.py +37 -0
  35. hypermedia_client/job_management/hcos/workdata_hco.py +127 -0
  36. hypermedia_client/job_management/hcos/workdata_query_result_hco.py +67 -0
  37. hypermedia_client/job_management/hcos/workdata_used_tags_query_result_hco.py +30 -0
  38. hypermedia_client/job_management/hcos/workdataroot_hco.py +84 -0
  39. hypermedia_client/job_management/ideas.md +28 -0
  40. hypermedia_client/job_management/known_relations.py +29 -0
  41. hypermedia_client/job_management/model/__init__.py +1 -0
  42. hypermedia_client/job_management/model/open_api_generated.py +890 -0
  43. hypermedia_client/job_management/model/sirenentities.py +112 -0
  44. hypermedia_client/job_management/tool/__init__.py +1 -0
  45. hypermedia_client/job_management/tool/job.py +442 -0
  46. pinexq_client-0.2.0.2024.607.8.dist-info/METADATA +105 -0
  47. pinexq_client-0.2.0.2024.607.8.dist-info/RECORD +49 -0
  48. pinexq_client-0.2.0.2024.607.8.dist-info/WHEEL +4 -0
  49. pinexq_client-0.2.0.2024.607.8.dist-info/licenses/LICENSE +19 -0
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: F403, F401
2
+ from .sirenaccess import *
3
+ from .model.sirenmodels import *
4
+ from .model.error import *
5
+ from .media_types import *
6
+ from .http_headers import *
7
+ from .exceptions import *
8
+ from .base_relations import BaseRelations
@@ -0,0 +1,8 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class BaseRelations(StrEnum):
5
+ SELF = "self"
6
+ ITEM = "item"
7
+ DOWNLOAD = "Download"
8
+ CREATED_RESSOURCE = "created-resource"
@@ -0,0 +1,17 @@
1
+ from typing import TypeVar, Type
2
+
3
+ import httpx
4
+
5
+ from hypermedia_client.core import Entity
6
+ from hypermedia_client.core.hco.hco_base import Hco
7
+
8
+ THco = TypeVar('THco', bound=Hco)
9
+
10
+
11
+ def enter_api(client: httpx.Client, entrypoint_hco_type: Type[THco], entrypoint_entity_type: Type[Entity] = Entity,
12
+ entrypoint: str = "api/EntryPoint") -> THco:
13
+ entry_point_response = client.get(url=entrypoint)
14
+ entry_point_response.raise_for_status()
15
+ entrypoint_entity = entrypoint_entity_type.model_validate_json(entry_point_response.read())
16
+
17
+ return entrypoint_hco_type.from_entity(entrypoint_entity, client)
@@ -0,0 +1,2 @@
1
+ class SirenException(Exception):
2
+ pass
File without changes
@@ -0,0 +1,70 @@
1
+ from typing import TypeVar, Self
2
+
3
+ import httpx
4
+ from httpx import Response
5
+ from httpx import URL
6
+
7
+ from hypermedia_client.core import SirenException, Entity, Action, ProblemDetails, execute_action
8
+ from hypermedia_client.core.hco.hco_base import ClientContainer
9
+
10
+ TEntity = TypeVar('TEntity', bound=Entity)
11
+ THcoEntity = TypeVar('THcoEntity', bound=Entity)
12
+
13
+
14
+ class ActionHco(ClientContainer):
15
+ _client: httpx.Client
16
+ _action: Action
17
+
18
+ @classmethod
19
+ def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self | None:
20
+ if action is None:
21
+ return None
22
+
23
+ if action.has_parameters():
24
+ raise SirenException(f"Error while mapping action: expected action no parameters but got some")
25
+
26
+ instance = cls(client)
27
+ instance._action = action
28
+ return instance
29
+
30
+ @classmethod
31
+ def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self | None:
32
+ if entity is None:
33
+ return None
34
+
35
+ action = entity.find_first_action_with_name(name)
36
+ return cls.from_action_optional(client, action)
37
+
38
+ @classmethod
39
+ def from_action(cls, client: httpx.Client, action: Action) -> Self:
40
+ action = cls.from_action_optional(client, action)
41
+ if action is None:
42
+ raise SirenException(
43
+ f"Error while mapping mandatory action: does not exist")
44
+ return action
45
+
46
+ @classmethod
47
+ def from_entity(cls, client: httpx.Client, entity: Entity, name: str) -> Self:
48
+ result = cls.from_entity_optional(client, entity, name)
49
+ if result is None:
50
+ raise SirenException(
51
+ f"Error while mapping mandatory action {name}: does not exist")
52
+ return result
53
+
54
+ def _execute_internal(self) -> None | URL:
55
+ response = execute_action(self._client, self._action)
56
+
57
+ if isinstance(response, ProblemDetails):
58
+ raise SirenException(
59
+ f"Error while executing action: {response}")
60
+ if isinstance(response, Response):
61
+ raise SirenException(
62
+ f"Error while executing action, unexpected response: {response}")
63
+ return response
64
+
65
+ def __repr__(self):
66
+ return f"<{self.__class__.__name__}: '{self._action.name}'>"
67
+
68
+
69
+
70
+
@@ -0,0 +1,86 @@
1
+ from typing import TypeVar, Type, Self, Generic
2
+
3
+ import httpx
4
+ from httpx import Response
5
+ from httpx import URL
6
+ from pydantic import BaseModel
7
+
8
+ from hypermedia_client.core import SirenException, Entity, Action, ProblemDetails, execute_action
9
+ from hypermedia_client.core.hco.hco_base import ClientContainer
10
+
11
+ TParameters = TypeVar('TParameters', bound=BaseModel)
12
+
13
+
14
+ class ActionWithParametersHco(ClientContainer, Generic[TParameters]):
15
+ _client: httpx.Client
16
+ _action: Action
17
+
18
+ @classmethod
19
+ def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self | None:
20
+ if action is None:
21
+ return None
22
+
23
+ if not action.has_parameters():
24
+ raise SirenException(
25
+ f"Error while mapping action: expected action with parameters but got none")
26
+
27
+ instance = cls(client)
28
+ instance._action = action
29
+ return instance
30
+
31
+ @classmethod
32
+ def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self | None:
33
+ if entity is None:
34
+ return None
35
+
36
+ action = entity.find_first_action_with_name(name)
37
+ return cls.from_action_optional(client, action)
38
+
39
+ @classmethod
40
+ def from_action(cls, client: httpx.Client, action: Action) -> Self:
41
+ action = cls.from_action_optional(client, action)
42
+ if action is None:
43
+ raise SirenException(
44
+ f"Error while mapping mandatory action: action does not exist")
45
+ return action
46
+
47
+ @classmethod
48
+ def from_entity(cls, client: httpx.Client, entity: Entity, name: str) -> Self:
49
+ result = cls.from_entity_optional(client, entity, name)
50
+ if result is None:
51
+ raise SirenException(
52
+ f"Error while mapping mandatory action {name}: action does not exist")
53
+ return result
54
+
55
+ def _execute_internal(self, parameters: BaseModel) -> None | URL:
56
+ if parameters is None:
57
+ raise SirenException(f"Error while executing action: action requires parameters")
58
+
59
+ response = execute_action(self._client, self._action, parameters)
60
+
61
+ if isinstance(response, ProblemDetails):
62
+ raise SirenException(
63
+ f"Error while executing action: {response}")
64
+ if isinstance(response, Response):
65
+ raise SirenException(
66
+ f"Error while executing action, unexpected response: {response}")
67
+ return response
68
+
69
+ def _execute(self, parameters: TParameters):
70
+ result = self._execute_internal(parameters)
71
+ if result is not None:
72
+ raise SirenException("Action did respond with unexpected URL")
73
+ return
74
+
75
+ def _execute_returns_url(self, parameters: TParameters) -> URL:
76
+ result = self._execute_internal(parameters)
77
+ if result is None:
78
+ raise SirenException("Action did not respond with URL")
79
+ return result
80
+
81
+ def _get_default_parameters(self, parameter_type: Type[TParameters],
82
+ default_if_none: TParameters) -> TParameters:
83
+ return self._action.get_default_parameters(parameter_type, default_if_none)
84
+
85
+ def __repr__(self):
86
+ return f"<{self.__class__.__name__}: '{self._action.name}'>"
@@ -0,0 +1,37 @@
1
+ from io import BytesIO
2
+ from typing import Self
3
+
4
+ import httpx
5
+ from httpx import Response
6
+
7
+ from hypermedia_client.core import Link, Entity, get_resource, SirenException
8
+ from hypermedia_client.core.hco.link_hco import LinkHco
9
+
10
+
11
+ class DownloadLinkHco(LinkHco):
12
+
13
+ @classmethod
14
+ def from_link_optional(cls, client: httpx.Client, link: Link | None) -> Self | None:
15
+ return super(DownloadLinkHco, cls).from_link_optional(client, link)
16
+
17
+ @classmethod
18
+ def from_entity_optional(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self | None:
19
+ return super(DownloadLinkHco, cls).from_entity_optional(client, entity, link_relation)
20
+
21
+ @classmethod
22
+ def from_link(cls, client: httpx.Client, link: Link) -> Self:
23
+ return super(DownloadLinkHco, cls).from_link(client, link)
24
+
25
+ @classmethod
26
+ def from_entity(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self:
27
+ return super(DownloadLinkHco, cls).from_entity(client, entity, link_relation)
28
+
29
+ def download(self) -> bytes:
30
+ response: Response = get_resource(self._client, self._link.href, self._link.type)
31
+ if not isinstance(response, Response):
32
+ raise SirenException(
33
+ f"Error while downloading resource: did not get response type")
34
+
35
+ return response.content
36
+
37
+ # TODO: download for large files
@@ -0,0 +1,91 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, TypeVar, Generic, Callable, Any
3
+
4
+ import httpx
5
+ from httpx import URL
6
+
7
+ from hypermedia_client.core import SirenException, Entity, BaseRelations, Link
8
+
9
+ TEntity = TypeVar('TEntity', bound=Entity)
10
+ THcoEntity = TypeVar('THcoEntity', bound=Entity)
11
+
12
+
13
+ class ClientContainer:
14
+ _client: httpx.Client
15
+
16
+ def __init__(self, client: httpx.Client):
17
+ self._client = client
18
+
19
+ def __repr__(self):
20
+ variables = [f"{k}: {repr(v)}" for k, v in vars(self).items() if not k.startswith('_')]
21
+ var_repr = f" ({', '.join(variables)})"
22
+ return f"<{self.__class__.__name__}{var_repr if variables else ''}>"
23
+
24
+
25
+ @dataclass
26
+ class Property:
27
+ """
28
+ Sentinel object marking properties in an HCO object to be copied from the entity.
29
+
30
+ Args:
31
+ name: Name of the property in the entity. If not provided, it's assumed to be
32
+ the same as the property name in the Hco. [optional]
33
+ converter: A callable that will be applied to the value in the entity before
34
+ being assigned to the property in the Hco. [optional]
35
+ """
36
+ name: str | None = None
37
+ converter: Callable[[Any], Any] | None = None
38
+
39
+
40
+ class Hco(ClientContainer, Generic[THcoEntity]):
41
+ _entity: THcoEntity
42
+
43
+ def __init__(self, client: httpx.Client, entity: THcoEntity):
44
+ super().__init__(client)
45
+ self._entity = entity
46
+ self._set_properties()
47
+
48
+ def __hash__(self):
49
+ # TODO: write a unit test ;)
50
+ candidate_self_link: Link | None = self._entity.find_first_link_with_relation(BaseRelations.SELF)
51
+ if candidate_self_link is not None:
52
+ return hash(URL(candidate_self_link.href).path)
53
+ else:
54
+ return super().__hash__()
55
+
56
+ def __eq__(self, other):
57
+ # TODO: write a unit test ;)
58
+ if isinstance(other, Hco):
59
+ return hash(self) == hash(other)
60
+ else:
61
+ return False
62
+
63
+ @staticmethod
64
+ def check_classes(existing_classes: List[str], expected_classes: List[str]):
65
+ for expected_class in expected_classes:
66
+ if expected_class not in existing_classes:
67
+ raise SirenException(
68
+ f"Error while mapping entity:expected hco class {expected_class} is not a class of generic entity "
69
+ f"with classes {existing_classes}")
70
+
71
+ def _set_properties(self):
72
+ """Initializes Hco properties from the entity.
73
+
74
+ Iterates over all properties defined in the class annotation. If one is initialized
75
+ with `Property()` the variable gets set from a property in self._entity.properties
76
+ with the same name.
77
+ """
78
+ for var_name, var_type in self.__annotations__.items():
79
+ # Get the initialized object. The annotation contain only the annotated type.
80
+ hco_property = getattr(self, var_name, None)
81
+ # Skip everything that is not initialized as `Property()`
82
+ if not (hco_property and isinstance(hco_property, Property)):
83
+ continue
84
+
85
+ if not hco_property.name:
86
+ hco_property.name = var_name
87
+ property_value = getattr(self._entity.properties, var_name)
88
+ if hco_property.converter:
89
+ property_value = hco_property.converter(property_value)
90
+ setattr(self, var_name, property_value)
91
+
@@ -0,0 +1,57 @@
1
+ from typing import Self, Type
2
+
3
+ import httpx
4
+ from httpx import URL
5
+
6
+ from hypermedia_client.core import Link, SirenException, Entity, navigate, ensure_siren_response
7
+ from hypermedia_client.core.hco.hco_base import ClientContainer, TEntity
8
+
9
+
10
+ class LinkHco(ClientContainer):
11
+ _client: httpx.Client
12
+ _link: Link
13
+
14
+ @classmethod
15
+ def from_link_optional(cls, client: httpx.Client, link: Link | None) -> Self | None:
16
+ if link is None:
17
+ return None
18
+
19
+ instance = cls(client)
20
+ instance._link = link
21
+ return instance
22
+
23
+ @classmethod
24
+ def from_entity_optional(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self | None:
25
+ if entity is None:
26
+ return None
27
+
28
+ link = entity.find_first_link_with_relation(link_relation)
29
+ return cls.from_link_optional(client, link)
30
+
31
+ @classmethod
32
+ def from_link(cls, client: httpx.Client, link: Link) -> Self:
33
+ result = cls.from_link_optional(client, link)
34
+ if result is None:
35
+ raise SirenException(f"Error while mapping mandatory link: link is None")
36
+
37
+ return result
38
+
39
+ @classmethod
40
+ def from_entity(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self:
41
+ result = cls.from_entity_optional(client, entity, link_relation)
42
+ if result is None:
43
+ raise SirenException(
44
+ f"Error while mapping mandatory link: entity contains no link with relation {link_relation}")
45
+
46
+ return result
47
+
48
+ def _navigate_internal(self, parse_type: Type[TEntity] = Entity) -> TEntity:
49
+ response = navigate(self._client, self._link, parse_type)
50
+ return ensure_siren_response(response)
51
+
52
+ def get_url(self) -> URL:
53
+ return URL(self._link.href)
54
+
55
+ def __repr__(self):
56
+ rel_names = ', '.join((f"'{r}'" for r in self._link.rel))
57
+ return f"<{self.__class__.__name__}: {rel_names}>"
@@ -0,0 +1,113 @@
1
+ from io import IOBase
2
+ from typing import BinaryIO, Iterable, AsyncIterable, Any, Self, List
3
+
4
+ import httpx
5
+ from httpx import URL
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
8
+ from hypermedia_client.core import upload_file, upload_binary, upload_json, SirenException, MediaTypes, Action, Entity, \
9
+ SirenClasses
10
+ from hypermedia_client.core.hco.action_with_parameters_hco import ActionWithParametersHco
11
+
12
+
13
+ class UploadParameters(BaseModel):
14
+ # arbitrary_types_allowed
15
+ model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
16
+ filename: str
17
+ mediatype: str = MediaTypes.OCTET_STREAM
18
+
19
+ file: IOBase | None = None
20
+ binary: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None
21
+ json_: Any | None = Field(None, alias='json')
22
+
23
+ @model_validator(mode='after')
24
+ def check_only_one_input_method(self) -> Self:
25
+ list_of_vars = [self.file, self.binary, self.json_]
26
+ initialized_vars_count = [v is not None for v in list_of_vars].count(True)
27
+
28
+ if initialized_vars_count == 0:
29
+ raise ValueError(f'Please provide a upload content: file, json, binary')
30
+ if initialized_vars_count > 1:
31
+ raise ValueError(f'Please provide only one upload content: file, json, binary')
32
+
33
+ return self
34
+
35
+
36
+ class UploadAction(ActionWithParametersHco[UploadParameters]):
37
+ accept: str | None = None
38
+ max_filesize_bytes: int | None = None
39
+ allow_multiple: bool | None = None
40
+
41
+ @classmethod
42
+ def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self | None:
43
+ instance = super(UploadAction, cls).from_action_optional(client, action)
44
+
45
+ if instance:
46
+ cls.validate_file_upload_field(instance)
47
+ cls.assign_upload_constraints(instance)
48
+ return instance
49
+
50
+ @classmethod
51
+ def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self | None:
52
+ instance = super(UploadAction, cls).from_entity_optional(client, entity, name)
53
+
54
+ if instance:
55
+ cls.validate_file_upload_field(instance)
56
+ cls.assign_upload_constraints(instance)
57
+ return instance
58
+
59
+ @classmethod
60
+ def from_action(cls, client: httpx.Client, action: Action) -> Self:
61
+ instance = super(UploadAction, cls).from_action(client, action)
62
+ cls.validate_file_upload_field(instance)
63
+ cls.assign_upload_constraints(instance)
64
+ return instance
65
+
66
+ @classmethod
67
+ def from_entity(cls, client: httpx.Client, entity: Entity, name: str) -> Self:
68
+ instance = super(UploadAction, cls).from_entity(client, entity, name)
69
+ cls.validate_file_upload_field(instance)
70
+ cls.assign_upload_constraints(instance)
71
+ return instance
72
+
73
+ def assign_upload_constraints(self):
74
+ upload_fields = self._action.fields[0]
75
+
76
+ self.accept = upload_fields.accept
77
+ self.max_filesize_bytes = upload_fields.maxFileSizeBytes
78
+ self.allow_multiple = upload_fields.allowMultiple
79
+
80
+ def validate_file_upload_field(self):
81
+ action = self._action
82
+ if SirenClasses.FileUploadAction not in action.class_:
83
+ raise SirenException(
84
+ f"Upload action does not have expected class: {str(SirenClasses.FileUploadAction)}. Got: {action.class_}")
85
+
86
+ if action.type != MediaTypes.MULTIPART_FORM_DATA.value:
87
+ raise SirenException(
88
+ f"Upload action does not have expected type: {str(MediaTypes.MULTIPART_FORM_DATA)}. Got: {action.type}")
89
+
90
+ upload_fields = self._action.fields[0]
91
+
92
+ if upload_fields.type != "file":
93
+ raise SirenException(
94
+ f"Upload action does not have expected field type: 'file'. Got: {upload_fields.type}")
95
+
96
+ def _upload(self, parameters: UploadParameters) -> URL:
97
+
98
+ result = None
99
+ if parameters.file is not None:
100
+ result = upload_file(self._client, self._action, parameters.file, parameters.filename, parameters.mediatype)
101
+
102
+ elif parameters.binary is not None:
103
+ result = upload_binary(self._client, self._action, parameters.binary, parameters.filename,
104
+ parameters.mediatype)
105
+
106
+ elif parameters.json_ is not None:
107
+ result = upload_json(self._client, self._action, parameters.json_, parameters.filename)
108
+ else:
109
+ raise SirenException("Did not execute upload, none selected")
110
+
111
+ if not isinstance(result, URL):
112
+ raise SirenException("Upload did not respond with location")
113
+ return result
@@ -0,0 +1,9 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class Headers(StrEnum):
5
+ API_KEY = "x-api-key"
6
+ CONTENT_TYPE = "Content-Type"
7
+ CONTENT_LENGTH = "Content-Length"
8
+ LOCATION_HEADER = "Location"
9
+
@@ -0,0 +1,24 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class MediaTypes(StrEnum):
5
+ APPLICATION_JSON = "application/json"
6
+ SIREN = "application/vnd.siren+json"
7
+ PROBLEM_DETAILS = "application/problem+json"
8
+ MULTIPART_FORM_DATA = "multipart/form-data"
9
+ OCTET_STREAM = "application/octet-stream"
10
+
11
+ XML = "application/xml"
12
+ ZIP = "application/zip"
13
+ PDF = "application/pdf"
14
+ TEXT = "text/plain"
15
+ HTML = "text/html"
16
+ CSV = "text/csv"
17
+ SVG = "image/svg+xml"
18
+ PNG = "image/png"
19
+ JPEG = "image/jpeg"
20
+ BMP = "image/bmp"
21
+
22
+
23
+ class SirenClasses(StrEnum):
24
+ FileUploadAction = "FileUploadAction"
File without changes
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ProblemDetails(BaseModel):
5
+ type: str | None = None
6
+ title: str | None = None
7
+ status: int | None = None
8
+ detail: str | None = None
9
+ instance: str | None = None