openadr3-client-gac-compliance 2.0.0__tar.gz → 3.0.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.
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/PKG-INFO +9 -5
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/README.md +7 -2
- openadr3_client_gac_compliance-3.0.0/openadr3_client_gac_compliance/__init__.py +22 -0
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac20/event_gac_compliant.py +53 -46
- openadr3_client_gac_compliance-3.0.0/openadr3_client_gac_compliance/gac20/plugin.py +43 -0
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac20/program_gac_compliant.py +18 -25
- openadr3_client_gac_compliance-3.0.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +49 -0
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/pyproject.toml +3 -4
- openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/__init__.py +0 -6
- openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/config.py +0 -29
- openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +0 -55
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/LICENSE.md +0 -0
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac20/__init__.py +0 -0
- {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac21/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openadr3-client-gac-compliance
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary:
|
|
5
5
|
License-File: LICENSE.md
|
|
6
6
|
Author: Nick van der Burgt
|
|
@@ -10,18 +10,22 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
-
Requires-Dist: openadr3-client (>=
|
|
13
|
+
Requires-Dist: openadr3-client (>=1.0.0a1,<2.0.0)
|
|
14
14
|
Requires-Dist: pycountry (>=24.6.1,<25.0.0)
|
|
15
15
|
Requires-Dist: pydantic (>=2.11.2,<3.0.0)
|
|
16
|
-
Requires-Dist: python-decouple (>=3.8,<4.0)
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
17
|
|
|
19
18
|
# OpenADR3 client
|
|
20
19
|
|
|
21
20
|
This repository contains a plugin for the [OpenADR3-client](https://github.com/ElaadNL/openadr3-client) library that adds additional pydantic validators to the OpenADR3 domain models to ensure GAC compliance. Since GAC compliance is a superset of OpenADR3, adding validation rules on top of the OpenADR3 models is sufficient to ensure compliance.
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
Registering the plugin is done using the global ValidatorPluginRegistry class:
|
|
24
23
|
|
|
25
24
|
```python
|
|
26
|
-
|
|
25
|
+
from openadr3_client.plugin import ValidatorPluginRegistry, ValidatorPlugin
|
|
26
|
+
from openadr3_client_gac_compliance.gac20.plugin import Gac20ValidatorPlugin
|
|
27
|
+
|
|
28
|
+
ValidatorPluginRegistry.register_plugin(
|
|
29
|
+
Gac20ValidatorPlugin().setup()
|
|
30
|
+
)
|
|
27
31
|
```
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
This repository contains a plugin for the [OpenADR3-client](https://github.com/ElaadNL/openadr3-client) library that adds additional pydantic validators to the OpenADR3 domain models to ensure GAC compliance. Since GAC compliance is a superset of OpenADR3, adding validation rules on top of the OpenADR3 models is sufficient to ensure compliance.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Registering the plugin is done using the global ValidatorPluginRegistry class:
|
|
6
6
|
|
|
7
7
|
```python
|
|
8
|
-
|
|
8
|
+
from openadr3_client.plugin import ValidatorPluginRegistry, ValidatorPlugin
|
|
9
|
+
from openadr3_client_gac_compliance.gac20.plugin import Gac20ValidatorPlugin
|
|
10
|
+
|
|
11
|
+
ValidatorPluginRegistry.register_plugin(
|
|
12
|
+
Gac20ValidatorPlugin().setup()
|
|
13
|
+
)
|
|
9
14
|
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenADR3 GAC Compliance Plugin.
|
|
3
|
+
|
|
4
|
+
This package provides validation plugins for OpenADR3 models to ensure compliance
|
|
5
|
+
with the Grid Aware Charging (GAC) specification.
|
|
6
|
+
|
|
7
|
+
The main entry point is the Gac20ValidatorPlugin which can be registered
|
|
8
|
+
with the OpenADR3 client's validator plugin registry.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python
|
|
12
|
+
from openadr3_client.plugin import ValidatorPluginRegistry
|
|
13
|
+
from openadr3_client_gac_compliance.plugin import Gac20ValidatorPlugin
|
|
14
|
+
from openadr3_client_gac_compliance.gac20.gac_plugin import GacVersion
|
|
15
|
+
|
|
16
|
+
# Register the GAC validation plugin
|
|
17
|
+
ValidatorPluginRegistry.register_plugin(
|
|
18
|
+
Gac20ValidatorPlugin.setup(gac_version=GacVersion.VERSION_2_0)
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
"""
|
|
@@ -9,7 +9,7 @@ pydantic validators. Namely, the requirement that a safe mode event MUST be pres
|
|
|
9
9
|
|
|
10
10
|
As the pydantic validator works on the scope of a single Event Object, it is not possible to validate
|
|
11
11
|
that a safe mode event is present in a program. And it cannot be validated on the Program object,
|
|
12
|
-
as the program object does not contain the events, these are stored
|
|
12
|
+
as the program object does not contain the events, these are stored separately in the VTN.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import re
|
|
@@ -17,28 +17,27 @@ from itertools import pairwise
|
|
|
17
17
|
|
|
18
18
|
from openadr3_client.models.event.event import Event
|
|
19
19
|
from openadr3_client.models.event.event_payload import EventPayloadType
|
|
20
|
-
from openadr3_client.models.model import Model as ValidatorModel
|
|
21
|
-
from openadr3_client.models.model import ValidatorRegistry
|
|
22
|
-
from pydantic import ValidationError
|
|
23
20
|
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
24
21
|
|
|
22
|
+
INTERVAL_PERIOD_ERROR_MESSAGE = "'interval_period' must either be set on the event-level, or for each interval."
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
|
|
25
|
+
def _continuous_or_separated(self: Event) -> list[InitErrorDetails]:
|
|
27
26
|
"""
|
|
28
|
-
|
|
27
|
+
Validates that events have consistent interval definitions GAC compliant.
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
The Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
|
|
31
30
|
interval definitions:
|
|
32
31
|
|
|
33
32
|
1. Continuous
|
|
34
33
|
2. Separated
|
|
35
34
|
|
|
36
35
|
The continuous implementation can be used when all intervals have the same duration.
|
|
37
|
-
In this case, only the top-level intervalPeriod of the event
|
|
36
|
+
In this case, only the top-level intervalPeriod of the event may be used, and the intervalPeriods
|
|
38
37
|
of the individual intervals must be None.
|
|
39
38
|
|
|
40
|
-
In the separated
|
|
41
|
-
and the top-level intervalPeriod of the event must be None. This separated
|
|
39
|
+
In the separated implementation, the intervalPeriods must be set on each individual intervals,
|
|
40
|
+
and the top-level intervalPeriod of the event must be None. This separated implementation allows events to have differing
|
|
42
41
|
durations.
|
|
43
42
|
""" # noqa: E501
|
|
44
43
|
validation_errors: list[InitErrorDetails] = []
|
|
@@ -54,7 +53,7 @@ def _continuous_or_seperated(self: Event) -> tuple[Event, list[InitErrorDetails]
|
|
|
54
53
|
InitErrorDetails(
|
|
55
54
|
type=PydanticCustomError(
|
|
56
55
|
"value_error",
|
|
57
|
-
|
|
56
|
+
INTERVAL_PERIOD_ERROR_MESSAGE,
|
|
58
57
|
),
|
|
59
58
|
loc=("intervals",),
|
|
60
59
|
input=self.intervals,
|
|
@@ -70,7 +69,7 @@ def _continuous_or_seperated(self: Event) -> tuple[Event, list[InitErrorDetails]
|
|
|
70
69
|
InitErrorDetails(
|
|
71
70
|
type=PydanticCustomError(
|
|
72
71
|
"value_error",
|
|
73
|
-
|
|
72
|
+
INTERVAL_PERIOD_ERROR_MESSAGE,
|
|
74
73
|
),
|
|
75
74
|
loc=("intervals",),
|
|
76
75
|
input=self.intervals,
|
|
@@ -78,19 +77,19 @@ def _continuous_or_seperated(self: Event) -> tuple[Event, list[InitErrorDetails]
|
|
|
78
77
|
)
|
|
79
78
|
)
|
|
80
79
|
|
|
81
|
-
return
|
|
80
|
+
return validation_errors
|
|
82
81
|
|
|
83
82
|
|
|
84
|
-
def _targets_compliant(self: Event) ->
|
|
83
|
+
def _targets_compliant(self: Event) -> list[InitErrorDetails]:
|
|
85
84
|
"""
|
|
86
|
-
|
|
85
|
+
Validates that the targets of the event are GAC compliant.
|
|
87
86
|
|
|
88
|
-
|
|
87
|
+
The following constraints are enforced for targets:
|
|
89
88
|
|
|
90
89
|
- The event must contain a POWER_SERVICE_LOCATION target.
|
|
91
90
|
- The POWER_SERVICE_LOCATION target value must be a list of 'EAN18' values.
|
|
92
91
|
- The event must contain a VEN_NAME target.
|
|
93
|
-
- The VEN_NAME target value must be a list of '
|
|
92
|
+
- The VEN_NAME target value must be a list of 'VEN name' values (between 1 and 128 characters).
|
|
94
93
|
"""
|
|
95
94
|
validation_errors: list[InitErrorDetails] = []
|
|
96
95
|
targets = self.targets or ()
|
|
@@ -142,7 +141,7 @@ def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
|
|
|
142
141
|
InitErrorDetails(
|
|
143
142
|
type=PydanticCustomError(
|
|
144
143
|
"value_error",
|
|
145
|
-
"The event must contain
|
|
144
|
+
"The event must contain exactly one VEN_NAME target.",
|
|
146
145
|
),
|
|
147
146
|
loc=("targets",),
|
|
148
147
|
input=self.targets,
|
|
@@ -159,7 +158,7 @@ def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
|
|
|
159
158
|
InitErrorDetails(
|
|
160
159
|
type=PydanticCustomError(
|
|
161
160
|
"value_error",
|
|
162
|
-
"The POWER_SERVICE_LOCATION target value
|
|
161
|
+
"The POWER_SERVICE_LOCATION target value may not be empty.",
|
|
163
162
|
),
|
|
164
163
|
loc=("targets",),
|
|
165
164
|
input=self.targets,
|
|
@@ -185,7 +184,7 @@ def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
|
|
|
185
184
|
InitErrorDetails(
|
|
186
185
|
type=PydanticCustomError(
|
|
187
186
|
"value_error",
|
|
188
|
-
"The VEN_NAME target value
|
|
187
|
+
"The VEN_NAME target value may not be empty.",
|
|
189
188
|
),
|
|
190
189
|
loc=("targets",),
|
|
191
190
|
input=self.targets,
|
|
@@ -198,7 +197,7 @@ def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
|
|
|
198
197
|
InitErrorDetails(
|
|
199
198
|
type=PydanticCustomError(
|
|
200
199
|
"value_error",
|
|
201
|
-
"The VEN_NAME target value must be a list of '
|
|
200
|
+
"The VEN_NAME target value must be a list of 'VEN name' values (between 1 and 128 characters).",
|
|
202
201
|
),
|
|
203
202
|
loc=("targets",),
|
|
204
203
|
input=self.targets,
|
|
@@ -206,18 +205,18 @@ def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
|
|
|
206
205
|
)
|
|
207
206
|
)
|
|
208
207
|
|
|
209
|
-
return
|
|
208
|
+
return validation_errors
|
|
210
209
|
|
|
211
210
|
|
|
212
211
|
def _payload_descriptors_gac_compliant(
|
|
213
212
|
self: Event,
|
|
214
|
-
) ->
|
|
213
|
+
) -> list[InitErrorDetails]:
|
|
215
214
|
"""
|
|
216
|
-
|
|
215
|
+
Validates that the payload descriptor is GAC compliant.
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
The following constraints are enforced for payload descriptors:
|
|
219
218
|
|
|
220
|
-
- The event interval must exactly one payload descriptor.
|
|
219
|
+
- The event interval must contain exactly one payload descriptor.
|
|
221
220
|
- The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'
|
|
222
221
|
- The payload descriptor must have a units of 'KW' (case sensitive).
|
|
223
222
|
"""
|
|
@@ -278,14 +277,14 @@ def _payload_descriptors_gac_compliant(
|
|
|
278
277
|
)
|
|
279
278
|
)
|
|
280
279
|
|
|
281
|
-
return
|
|
280
|
+
return validation_errors
|
|
282
281
|
|
|
283
282
|
|
|
284
|
-
def _event_interval_gac_compliant(self: Event) ->
|
|
283
|
+
def _event_interval_gac_compliant(self: Event) -> list[InitErrorDetails]:
|
|
285
284
|
"""
|
|
286
|
-
|
|
285
|
+
Validates that the event interval is GAC compliant.
|
|
287
286
|
|
|
288
|
-
|
|
287
|
+
The following constraints are enforced for event intervals:
|
|
289
288
|
|
|
290
289
|
- The event interval must have an id value that is strictly increasing.
|
|
291
290
|
- The event interval must have exactly one payload.
|
|
@@ -360,23 +359,34 @@ def _event_interval_gac_compliant(self: Event) -> tuple[Event, list[InitErrorDet
|
|
|
360
359
|
ctx={},
|
|
361
360
|
)
|
|
362
361
|
)
|
|
362
|
+
if len(payload.values) > 1:
|
|
363
|
+
validation_errors.append(
|
|
364
|
+
InitErrorDetails(
|
|
365
|
+
type=PydanticCustomError(
|
|
366
|
+
"value_error",
|
|
367
|
+
"The event interval payload must have exactly one value per payload.",
|
|
368
|
+
),
|
|
369
|
+
loc=("intervals",),
|
|
370
|
+
input=self.intervals,
|
|
371
|
+
ctx={},
|
|
372
|
+
)
|
|
373
|
+
)
|
|
363
374
|
|
|
364
|
-
return
|
|
375
|
+
return validation_errors
|
|
365
376
|
|
|
366
377
|
|
|
367
|
-
|
|
368
|
-
def event_gac_compliant(self: Event) -> Event:
|
|
378
|
+
def validate_event_gac_compliant(event: Event) -> list[InitErrorDetails] | None:
|
|
369
379
|
"""
|
|
370
|
-
|
|
380
|
+
Validates that events are GAC compliant.
|
|
371
381
|
|
|
372
|
-
|
|
382
|
+
The following constraints are enforced for events:
|
|
373
383
|
|
|
374
384
|
- The event must not have a priority set.
|
|
375
|
-
- The event must have either a continuous or
|
|
385
|
+
- The event must have either a continuous or separated interval definition.
|
|
376
386
|
"""
|
|
377
387
|
validation_errors: list[InitErrorDetails] = []
|
|
378
388
|
|
|
379
|
-
if
|
|
389
|
+
if event.priority is not None:
|
|
380
390
|
validation_errors.append(
|
|
381
391
|
InitErrorDetails(
|
|
382
392
|
type=PydanticCustomError(
|
|
@@ -384,24 +394,21 @@ def event_gac_compliant(self: Event) -> Event:
|
|
|
384
394
|
"The event must not have a priority set for GAC 2.0 compliance",
|
|
385
395
|
),
|
|
386
396
|
loc=("priority",),
|
|
387
|
-
input=
|
|
397
|
+
input=event.priority,
|
|
388
398
|
ctx={},
|
|
389
399
|
)
|
|
390
400
|
)
|
|
391
401
|
|
|
392
|
-
|
|
402
|
+
errors = _continuous_or_separated(event)
|
|
393
403
|
validation_errors.extend(errors)
|
|
394
404
|
|
|
395
|
-
|
|
405
|
+
errors = _targets_compliant(event)
|
|
396
406
|
validation_errors.extend(errors)
|
|
397
407
|
|
|
398
|
-
|
|
408
|
+
errors = _payload_descriptors_gac_compliant(event)
|
|
399
409
|
validation_errors.extend(errors)
|
|
400
410
|
|
|
401
|
-
|
|
411
|
+
errors = _event_interval_gac_compliant(event)
|
|
402
412
|
validation_errors.extend(errors)
|
|
403
413
|
|
|
404
|
-
if validation_errors
|
|
405
|
-
raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
|
|
406
|
-
|
|
407
|
-
return event_interval_validated
|
|
414
|
+
return validation_errors if validation_errors else None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""GAC compliance plugin for OpenADR3 client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from openadr3_client.models.event.event import Event
|
|
6
|
+
from openadr3_client.models.program.program import Program
|
|
7
|
+
from openadr3_client.models.ven.ven import Ven
|
|
8
|
+
from openadr3_client.plugin import ValidatorPlugin
|
|
9
|
+
|
|
10
|
+
from openadr3_client_gac_compliance.gac20.event_gac_compliant import validate_event_gac_compliant
|
|
11
|
+
from openadr3_client_gac_compliance.gac20.program_gac_compliant import validate_program_gac_compliant
|
|
12
|
+
from openadr3_client_gac_compliance.gac20.ven_gac_compliant import validate_ven_gac_compliant
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Gac20ValidatorPlugin(ValidatorPlugin):
|
|
16
|
+
"""Plugin that validates OpenADR3 models for GAC compliance."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize the GAC validator plugin."""
|
|
20
|
+
super().__init__()
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def setup(*_args: Any, **_kwargs: Any) -> "Gac20ValidatorPlugin": # noqa: ANN401
|
|
24
|
+
"""
|
|
25
|
+
Set up the GAC validator plugin.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
*args: Positional arguments (unused).
|
|
29
|
+
**kwargs: Keyword arguments containing configuration.
|
|
30
|
+
Expected keys:
|
|
31
|
+
- gac_version: The GAC version to validate against.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
GacValidatorPlugin: Configured plugin instance.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
plugin = Gac20ValidatorPlugin()
|
|
38
|
+
|
|
39
|
+
plugin.register_model_validator(Event, validate_event_gac_compliant)
|
|
40
|
+
plugin.register_model_validator(Program, validate_program_gac_compliant)
|
|
41
|
+
plugin.register_model_validator(Ven, validate_ven_gac_compliant)
|
|
42
|
+
|
|
43
|
+
return plugin
|
|
@@ -2,25 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from openadr3_client.models.model import Model as ValidatorModel
|
|
6
|
-
from openadr3_client.models.model import ValidatorRegistry
|
|
7
5
|
from openadr3_client.models.program.program import Program
|
|
8
|
-
from pydantic import ValidationError
|
|
9
6
|
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
10
7
|
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
def program_gac_compliant(self: Program) -> Program:
|
|
9
|
+
def validate_program_gac_compliant(program: Program) -> list[InitErrorDetails] | None:
|
|
14
10
|
"""
|
|
15
|
-
|
|
11
|
+
Validates that the program is GAC compliant.
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
The following constraints are enforced for programs:
|
|
18
14
|
- The program must have a retailer name
|
|
19
15
|
- The retailer name must be between 2 and 128 characters long.
|
|
20
16
|
- The program MUST have a programType.
|
|
21
17
|
- The programType MUST equal "DSO_CPO_INTERFACE-x.x.x, where x.x.x is the version as defined in the GAC specification.
|
|
22
|
-
- The program MUST have bindingEvents set to
|
|
23
|
-
|
|
18
|
+
- The program MUST have bindingEvents set to true.
|
|
19
|
+
|
|
24
20
|
""" # noqa: E501
|
|
25
21
|
validation_errors: list[InitErrorDetails] = []
|
|
26
22
|
|
|
@@ -35,7 +31,7 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
35
31
|
r"$"
|
|
36
32
|
)
|
|
37
33
|
|
|
38
|
-
if
|
|
34
|
+
if program.retailer_name is None:
|
|
39
35
|
validation_errors.append(
|
|
40
36
|
InitErrorDetails(
|
|
41
37
|
type=PydanticCustomError(
|
|
@@ -43,13 +39,13 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
43
39
|
"The program must have a retailer name.",
|
|
44
40
|
),
|
|
45
41
|
loc=("retailer_name",),
|
|
46
|
-
input=
|
|
42
|
+
input=program.retailer_name,
|
|
47
43
|
ctx={},
|
|
48
44
|
)
|
|
49
45
|
)
|
|
50
46
|
|
|
51
|
-
if
|
|
52
|
-
len(
|
|
47
|
+
if program.retailer_name is not None and (
|
|
48
|
+
len(program.retailer_name) < 2 or len(program.retailer_name) > 128 # noqa: PLR2004
|
|
53
49
|
):
|
|
54
50
|
validation_errors.append(
|
|
55
51
|
InitErrorDetails(
|
|
@@ -58,12 +54,12 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
58
54
|
"The retailer name must be between 2 and 128 characters long.",
|
|
59
55
|
),
|
|
60
56
|
loc=("retailer_name",),
|
|
61
|
-
input=
|
|
57
|
+
input=program.retailer_name,
|
|
62
58
|
ctx={},
|
|
63
59
|
)
|
|
64
60
|
)
|
|
65
61
|
|
|
66
|
-
if
|
|
62
|
+
if program.program_type is None:
|
|
67
63
|
validation_errors.append(
|
|
68
64
|
InitErrorDetails(
|
|
69
65
|
type=PydanticCustomError(
|
|
@@ -71,11 +67,11 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
71
67
|
"The program must have a program type.",
|
|
72
68
|
),
|
|
73
69
|
loc=("program_type",),
|
|
74
|
-
input=
|
|
70
|
+
input=program.program_type,
|
|
75
71
|
ctx={},
|
|
76
72
|
)
|
|
77
73
|
)
|
|
78
|
-
if
|
|
74
|
+
if program.program_type is not None and not re.fullmatch(program_type_regex, program.program_type):
|
|
79
75
|
validation_errors.append(
|
|
80
76
|
InitErrorDetails(
|
|
81
77
|
type=PydanticCustomError(
|
|
@@ -83,25 +79,22 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
83
79
|
"The program type must follow the format DSO_CPO_INTERFACE-x.x.x.",
|
|
84
80
|
),
|
|
85
81
|
loc=("program_type",),
|
|
86
|
-
input=
|
|
82
|
+
input=program.program_type,
|
|
87
83
|
ctx={},
|
|
88
84
|
)
|
|
89
85
|
)
|
|
90
86
|
|
|
91
|
-
if
|
|
87
|
+
if program.binding_events is False:
|
|
92
88
|
validation_errors.append(
|
|
93
89
|
InitErrorDetails(
|
|
94
90
|
type=PydanticCustomError(
|
|
95
91
|
"value_error",
|
|
96
|
-
"The program must have bindingEvents set to
|
|
92
|
+
"The program must have bindingEvents set to true.",
|
|
97
93
|
),
|
|
98
94
|
loc=("binding_events",),
|
|
99
|
-
input=
|
|
95
|
+
input=program.binding_events,
|
|
100
96
|
ctx={},
|
|
101
97
|
)
|
|
102
98
|
)
|
|
103
99
|
|
|
104
|
-
if validation_errors
|
|
105
|
-
raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
|
|
106
|
-
|
|
107
|
-
return self
|
|
100
|
+
return validation_errors if validation_errors else None
|
openadr3_client_gac_compliance-3.0.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import pycountry
|
|
4
|
+
from openadr3_client.models.ven.ven import Ven
|
|
5
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_ven_gac_compliant(ven: Ven) -> list[InitErrorDetails] | None:
|
|
9
|
+
"""
|
|
10
|
+
Validates that the VEN is GAC compliant.
|
|
11
|
+
|
|
12
|
+
The following constraints are enforced for VENs:
|
|
13
|
+
- The VEN must have a VEN name
|
|
14
|
+
- The VEN name must be an eMI3 identifier.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
validation_errors: list[InitErrorDetails] = []
|
|
18
|
+
|
|
19
|
+
emi3_identifier_regex = r"^[A-Z]{2}-?[A-Z0-9]{3}$"
|
|
20
|
+
|
|
21
|
+
if not re.fullmatch(emi3_identifier_regex, ven.ven_name):
|
|
22
|
+
validation_errors.append(
|
|
23
|
+
InitErrorDetails(
|
|
24
|
+
type=PydanticCustomError(
|
|
25
|
+
"value_error",
|
|
26
|
+
"The VEN name must be formatted as an eMI3 identifier.",
|
|
27
|
+
),
|
|
28
|
+
loc=("ven_name",),
|
|
29
|
+
input=ven.ven_name,
|
|
30
|
+
ctx={},
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
alpha_2_country = pycountry.countries.get(alpha_2=ven.ven_name[:2])
|
|
35
|
+
|
|
36
|
+
if alpha_2_country is None:
|
|
37
|
+
validation_errors.append(
|
|
38
|
+
InitErrorDetails(
|
|
39
|
+
type=PydanticCustomError(
|
|
40
|
+
"value_error",
|
|
41
|
+
"The first two characters of the VEN name must be a valid ISO 3166-1 alpha-2 country code.",
|
|
42
|
+
),
|
|
43
|
+
loc=("ven_name",),
|
|
44
|
+
input=ven.ven_name,
|
|
45
|
+
ctx={},
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return validation_errors if validation_errors else None
|
{openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openadr3-client-gac-compliance"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Nick van der Burgt", email = "nick.van.der.burgt@elaad.nl"}
|
|
@@ -9,9 +9,8 @@ readme = "README.md"
|
|
|
9
9
|
requires-python = ">=3.12, <4"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"pydantic (>=2.11.2,<3.0.0)",
|
|
12
|
-
"openadr3-client (>=
|
|
12
|
+
"openadr3-client (>=1.0.0a1,<2.0.0)",
|
|
13
13
|
"pycountry (>=24.6.1,<25.0.0)",
|
|
14
|
-
"python-decouple (>=3.8,<4.0)",
|
|
15
14
|
]
|
|
16
15
|
|
|
17
16
|
[build-system]
|
|
@@ -26,7 +25,7 @@ pytest-cov = "^6.1.1"
|
|
|
26
25
|
taskipy = "^1.14.1"
|
|
27
26
|
|
|
28
27
|
[[tool.mypy.overrides]]
|
|
29
|
-
module = ["openadr3_client"
|
|
28
|
+
module = ["openadr3_client"]
|
|
30
29
|
ignore_missing_imports = true
|
|
31
30
|
|
|
32
31
|
[tool.taskipy.tasks]
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
from openadr3_client_gac_compliance.config import GAC_VERSION
|
|
2
|
-
|
|
3
|
-
if GAC_VERSION == "2.0":
|
|
4
|
-
import openadr3_client_gac_compliance.gac20.event_gac_compliant
|
|
5
|
-
import openadr3_client_gac_compliance.gac20.program_gac_compliant
|
|
6
|
-
import openadr3_client_gac_compliance.gac20.ven_gac_compliant # noqa: F401
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""Contains configuration variables used by the OpenADR3 GAC compliance plugin."""
|
|
2
|
-
|
|
3
|
-
from decouple import config
|
|
4
|
-
|
|
5
|
-
VALID_GAC_VERSIONS: list[str] = ["2.0"]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _gac_version_cast(value: str) -> str:
|
|
9
|
-
"""
|
|
10
|
-
Cast the GAC version to a string.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
value (str): The GAC version to cast.
|
|
14
|
-
|
|
15
|
-
Raises:
|
|
16
|
-
ValueError: If the GAC version is not a valid GAC version.
|
|
17
|
-
|
|
18
|
-
Returns:
|
|
19
|
-
str: The GAC version.
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
if value not in VALID_GAC_VERSIONS:
|
|
23
|
-
msg = f"Invalid GAC version: {value}"
|
|
24
|
-
raise ValueError(msg)
|
|
25
|
-
return value
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# The GAC version to use for the compliance validators.
|
|
29
|
-
GAC_VERSION = config("GAC_VERSION", default="2.0", cast=_gac_version_cast)
|
openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
import pycountry
|
|
4
|
-
from openadr3_client.models.model import Model as ValidatorModel
|
|
5
|
-
from openadr3_client.models.model import ValidatorRegistry
|
|
6
|
-
from openadr3_client.models.ven.ven import Ven
|
|
7
|
-
from pydantic import ValidationError
|
|
8
|
-
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@ValidatorRegistry.register(Ven, ValidatorModel())
|
|
12
|
-
def ven_gac_compliant(self: Ven) -> Ven:
|
|
13
|
-
"""
|
|
14
|
-
Enforces that the ven is GAC compliant.
|
|
15
|
-
|
|
16
|
-
GAC enforces the following constraints for vens:
|
|
17
|
-
- The ven must have a ven name
|
|
18
|
-
- The ven name must be an eMI3 identifier.
|
|
19
|
-
"""
|
|
20
|
-
validation_errors: list[InitErrorDetails] = []
|
|
21
|
-
|
|
22
|
-
emi3_identifier_regex = r"^[A-Z]{2}-?[A-Z0-9]{3}$"
|
|
23
|
-
|
|
24
|
-
if not re.fullmatch(emi3_identifier_regex, self.ven_name):
|
|
25
|
-
validation_errors.append(
|
|
26
|
-
InitErrorDetails(
|
|
27
|
-
type=PydanticCustomError(
|
|
28
|
-
"value_error",
|
|
29
|
-
"The ven name must be formatted as an eMI3 identifier.",
|
|
30
|
-
),
|
|
31
|
-
loc=("ven_name",),
|
|
32
|
-
input=self.ven_name,
|
|
33
|
-
ctx={},
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
alpha_2_country = pycountry.countries.get(alpha_2=self.ven_name[:2])
|
|
38
|
-
|
|
39
|
-
if alpha_2_country is None:
|
|
40
|
-
validation_errors.append(
|
|
41
|
-
InitErrorDetails(
|
|
42
|
-
type=PydanticCustomError(
|
|
43
|
-
"value_error",
|
|
44
|
-
"The first two characters of the ven name must be a valid ISO 3166-1 alpha-2 country code.",
|
|
45
|
-
),
|
|
46
|
-
loc=("ven_name",),
|
|
47
|
-
input=self.ven_name,
|
|
48
|
-
ctx={},
|
|
49
|
-
)
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
if validation_errors:
|
|
53
|
-
raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
|
|
54
|
-
|
|
55
|
-
return self
|
|
File without changes
|
|
File without changes
|