|
4 | 4 | import re
|
5 | 5 | import time
|
6 | 6 | import uuid
|
7 |
| -from dataclasses import asdict |
8 |
| -from typing import Dict, List, Union |
| 7 | +from dataclasses import Field, asdict, is_dataclass |
| 8 | +from typing import Any, Dict, List, Union, get_args, get_origin |
9 | 9 |
|
10 | 10 | from ocpp.exceptions import NotImplementedError, NotSupportedError, OCPPError
|
11 | 11 | from ocpp.messages import Call, MessageType, unpack, validate_payload
|
@@ -73,6 +73,71 @@ def snake_to_camel_case(data):
|
73 | 73 | return data
|
74 | 74 |
|
75 | 75 |
|
| 76 | +def _is_dataclass_instance(input: Any) -> bool: |
| 77 | + """Verify if given `input` is a dataclass.""" |
| 78 | + return is_dataclass(input) and not isinstance(input, type) |
| 79 | + |
| 80 | + |
| 81 | +def _is_optional_field(field: Field) -> bool: |
| 82 | + """Verify if given `field` allows `None` as value. |
| 83 | +
|
| 84 | + The fields `schema` and `host` on the following class would return `False`. |
| 85 | + While the fields `post` and `query` return `True`. |
| 86 | +
|
| 87 | + @dataclass |
| 88 | + class URL: |
| 89 | + schema: str, |
| 90 | + host: str, |
| 91 | + post: Optional[str], |
| 92 | + query: Union[None, str] |
| 93 | +
|
| 94 | + """ |
| 95 | + return get_origin(field.type) is Union and type(None) in get_args(field.type) |
| 96 | + |
| 97 | + |
| 98 | +def serialize_as_dict(dataclass): |
| 99 | + """Serialize the given `dataclass` as a `dict` recursively. |
| 100 | +
|
| 101 | + @dataclass |
| 102 | + class StatusInfoType: |
| 103 | + reason_code: str |
| 104 | + additional_info: Optional[str] = None |
| 105 | +
|
| 106 | + with_additional_info = StatusInfoType( |
| 107 | + reason="Unknown", |
| 108 | + additional_info="More details" |
| 109 | + ) |
| 110 | +
|
| 111 | + assert serialize_as_dict(with_additional_info) == { |
| 112 | + 'reason': 'Unknown', |
| 113 | + 'additional_info': 'More details', |
| 114 | + } |
| 115 | +
|
| 116 | + without_additional_info = StatusInfoType(reason="Unknown") |
| 117 | +
|
| 118 | + assert serialize_as_dict(with_additional_info) == { |
| 119 | + 'reason': 'Unknown', |
| 120 | + 'additional_info': None, |
| 121 | + } |
| 122 | +
|
| 123 | + """ |
| 124 | + serialized = asdict(dataclass) |
| 125 | + |
| 126 | + for field in dataclass.__dataclass_fields__.values(): |
| 127 | + |
| 128 | + value = getattr(dataclass, field.name) |
| 129 | + if _is_dataclass_instance(value): |
| 130 | + serialized[field.name] = serialize_as_dict(value) |
| 131 | + continue |
| 132 | + |
| 133 | + if isinstance(value, list): |
| 134 | + for item in value: |
| 135 | + if _is_dataclass_instance(item): |
| 136 | + serialized[field.name] = [serialize_as_dict(item)] |
| 137 | + |
| 138 | + return serialized |
| 139 | + |
| 140 | + |
76 | 141 | def remove_nones(data: Union[List, Dict]) -> Union[List, Dict]:
|
77 | 142 | if isinstance(data, dict):
|
78 | 143 | return {k: remove_nones(v) for k, v in data.items() if v is not None}
|
@@ -246,7 +311,7 @@ async def _handle_call(self, msg):
|
246 | 311 |
|
247 | 312 | return
|
248 | 313 |
|
249 |
| - temp_response_payload = asdict(response) |
| 314 | + temp_response_payload = serialize_as_dict(response) |
250 | 315 |
|
251 | 316 | # Remove nones ensures that we strip out optional arguments
|
252 | 317 | # which were not set and have a default value of None
|
@@ -308,7 +373,7 @@ async def call(self, payload, suppress=True, unique_id=None):
|
308 | 373 | CallError.
|
309 | 374 |
|
310 | 375 | """
|
311 |
| - camel_case_payload = snake_to_camel_case(asdict(payload)) |
| 376 | + camel_case_payload = snake_to_camel_case(serialize_as_dict(payload)) |
312 | 377 |
|
313 | 378 | unique_id = (
|
314 | 379 | unique_id if unique_id is not None else str(self._unique_id_generator())
|
|
0 commit comments