openadr3-client-gac-compliance 1.4.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-1.4.0 → openadr3_client_gac_compliance-3.0.0}/PKG-INFO +12 -5
- {openadr3_client_gac_compliance-1.4.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-1.4.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac20/__init__.py +1 -1
- {openadr3_client_gac_compliance-1.4.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac20/event_gac_compliant.py +73 -75
- openadr3_client_gac_compliance-3.0.0/openadr3_client_gac_compliance/gac20/plugin.py +43 -0
- {openadr3_client_gac_compliance-1.4.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac20/program_gac_compliant.py +31 -32
- openadr3_client_gac_compliance-3.0.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +49 -0
- openadr3_client_gac_compliance-3.0.0/pyproject.toml +79 -0
- openadr3_client_gac_compliance-1.4.0/openadr3_client_gac_compliance/__init__.py +0 -6
- openadr3_client_gac_compliance-1.4.0/openadr3_client_gac_compliance/config.py +0 -26
- openadr3_client_gac_compliance-1.4.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +0 -55
- openadr3_client_gac_compliance-1.4.0/pyproject.toml +0 -28
- {openadr3_client_gac_compliance-1.4.0 → openadr3_client_gac_compliance-3.0.0}/LICENSE.md +0 -0
- {openadr3_client_gac_compliance-1.4.0 → openadr3_client_gac_compliance-3.0.0}/openadr3_client_gac_compliance/gac21/__init__.py +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: openadr3-client-gac-compliance
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary:
|
|
5
|
+
License-File: LICENSE.md
|
|
5
6
|
Author: Nick van der Burgt
|
|
6
7
|
Author-email: nick.van.der.burgt@elaad.nl
|
|
7
8
|
Requires-Python: >=3.12, <4
|
|
8
9
|
Classifier: Programming Language :: Python :: 3
|
|
9
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
-
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: openadr3-client (>=1.0.0a1,<2.0.0)
|
|
12
14
|
Requires-Dist: pycountry (>=24.6.1,<25.0.0)
|
|
13
15
|
Requires-Dist: pydantic (>=2.11.2,<3.0.0)
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
@@ -17,8 +19,13 @@ Description-Content-Type: text/markdown
|
|
|
17
19
|
|
|
18
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.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
Registering the plugin is done using the global ValidatorPluginRegistry class:
|
|
21
23
|
|
|
22
24
|
```python
|
|
23
|
-
|
|
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
|
+
)
|
|
24
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
|
+
"""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# This module imports all the GAC 3.0 compliance validators.
|
|
2
2
|
|
|
3
|
+
import openadr3_client_gac_compliance.gac20.event_gac_compliant
|
|
3
4
|
import openadr3_client_gac_compliance.gac20.program_gac_compliant # noqa: F401
|
|
4
|
-
import openadr3_client_gac_compliance.gac20.event_gac_compliant # noqa: F401
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Module which implements GAC compliance validators for the event OpenADR3 types.
|
|
2
3
|
|
|
3
4
|
This module validates all the object constraints and requirements on the OpenADR3 events resource
|
|
4
5
|
as specified in the Grid aware charging (GAC) specification.
|
|
@@ -8,37 +9,37 @@ pydantic validators. Namely, the requirement that a safe mode event MUST be pres
|
|
|
8
9
|
|
|
9
10
|
As the pydantic validator works on the scope of a single Event Object, it is not possible to validate
|
|
10
11
|
that a safe mode event is present in a program. And it cannot be validated on the Program object,
|
|
11
|
-
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.
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
|
-
from itertools import pairwise
|
|
15
15
|
import re
|
|
16
|
-
from
|
|
17
|
-
|
|
16
|
+
from itertools import pairwise
|
|
17
|
+
|
|
18
18
|
from openadr3_client.models.event.event import Event
|
|
19
19
|
from openadr3_client.models.event.event_payload import EventPayloadType
|
|
20
|
-
|
|
21
|
-
from pydantic import ValidationError
|
|
22
20
|
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
23
21
|
|
|
22
|
+
INTERVAL_PERIOD_ERROR_MESSAGE = "'interval_period' must either be set on the event-level, or for each interval."
|
|
23
|
+
|
|
24
24
|
|
|
25
|
-
def
|
|
26
|
-
"""
|
|
25
|
+
def _continuous_or_separated(self: Event) -> list[InitErrorDetails]:
|
|
26
|
+
"""
|
|
27
|
+
Validates that events have consistent interval definitions GAC compliant.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
The Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
|
|
29
30
|
interval definitions:
|
|
30
31
|
|
|
31
32
|
1. Continuous
|
|
32
|
-
2.
|
|
33
|
+
2. Separated
|
|
33
34
|
|
|
34
|
-
The
|
|
35
|
-
In this case, only the top-level intervalPeriod of the event
|
|
35
|
+
The continuous implementation can be used when all intervals have the same duration.
|
|
36
|
+
In this case, only the top-level intervalPeriod of the event may be used, and the intervalPeriods
|
|
36
37
|
of the individual intervals must be None.
|
|
37
38
|
|
|
38
|
-
In the
|
|
39
|
-
and the top-level intervalPeriod of the event must be None. This
|
|
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
|
|
40
41
|
durations.
|
|
41
|
-
"""
|
|
42
|
+
""" # noqa: E501
|
|
42
43
|
validation_errors: list[InitErrorDetails] = []
|
|
43
44
|
|
|
44
45
|
intervals = self.intervals or ()
|
|
@@ -52,7 +53,7 @@ def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]
|
|
|
52
53
|
InitErrorDetails(
|
|
53
54
|
type=PydanticCustomError(
|
|
54
55
|
"value_error",
|
|
55
|
-
|
|
56
|
+
INTERVAL_PERIOD_ERROR_MESSAGE,
|
|
56
57
|
),
|
|
57
58
|
loc=("intervals",),
|
|
58
59
|
input=self.intervals,
|
|
@@ -62,15 +63,13 @@ def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]
|
|
|
62
63
|
else:
|
|
63
64
|
# interval period set at top level of the event.
|
|
64
65
|
# Ensure that all intervals do not have the interval_period defined, to comply with the GAC specification.
|
|
65
|
-
duplicate_interval_period = [
|
|
66
|
-
i for i in intervals if i.interval_period is not None
|
|
67
|
-
]
|
|
66
|
+
duplicate_interval_period = [i for i in intervals if i.interval_period is not None]
|
|
68
67
|
if duplicate_interval_period:
|
|
69
68
|
validation_errors.append(
|
|
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,18 +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) ->
|
|
85
|
-
"""
|
|
83
|
+
def _targets_compliant(self: Event) -> list[InitErrorDetails]:
|
|
84
|
+
"""
|
|
85
|
+
Validates that the targets of the event are GAC compliant.
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
The following constraints are enforced for targets:
|
|
88
88
|
|
|
89
89
|
- The event must contain a POWER_SERVICE_LOCATION target.
|
|
90
90
|
- The POWER_SERVICE_LOCATION target value must be a list of 'EAN18' values.
|
|
91
91
|
- The event must contain a VEN_NAME target.
|
|
92
|
-
- 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).
|
|
93
93
|
"""
|
|
94
94
|
validation_errors: list[InitErrorDetails] = []
|
|
95
95
|
targets = self.targets or ()
|
|
@@ -141,7 +141,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
141
141
|
InitErrorDetails(
|
|
142
142
|
type=PydanticCustomError(
|
|
143
143
|
"value_error",
|
|
144
|
-
"The event must contain
|
|
144
|
+
"The event must contain exactly one VEN_NAME target.",
|
|
145
145
|
),
|
|
146
146
|
loc=("targets",),
|
|
147
147
|
input=self.targets,
|
|
@@ -149,12 +149,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
149
149
|
)
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
-
if (
|
|
153
|
-
power_service_locations
|
|
154
|
-
and ven_names
|
|
155
|
-
and len(power_service_locations) == 1
|
|
156
|
-
and len(ven_names) == 1
|
|
157
|
-
):
|
|
152
|
+
if power_service_locations and ven_names and len(power_service_locations) == 1 and len(ven_names) == 1:
|
|
158
153
|
power_service_location = power_service_locations[0]
|
|
159
154
|
ven_name = ven_names[0]
|
|
160
155
|
|
|
@@ -163,7 +158,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
163
158
|
InitErrorDetails(
|
|
164
159
|
type=PydanticCustomError(
|
|
165
160
|
"value_error",
|
|
166
|
-
"The POWER_SERVICE_LOCATION target value
|
|
161
|
+
"The POWER_SERVICE_LOCATION target value may not be empty.",
|
|
167
162
|
),
|
|
168
163
|
loc=("targets",),
|
|
169
164
|
input=self.targets,
|
|
@@ -171,9 +166,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
171
166
|
)
|
|
172
167
|
)
|
|
173
168
|
|
|
174
|
-
if not all(
|
|
175
|
-
re.fullmatch(r"^EAN\d{15}$", v) for v in power_service_location.values
|
|
176
|
-
):
|
|
169
|
+
if not all(re.fullmatch(r"^\d{18}$", v) for v in power_service_location.values):
|
|
177
170
|
validation_errors.append(
|
|
178
171
|
InitErrorDetails(
|
|
179
172
|
type=PydanticCustomError(
|
|
@@ -191,7 +184,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
191
184
|
InitErrorDetails(
|
|
192
185
|
type=PydanticCustomError(
|
|
193
186
|
"value_error",
|
|
194
|
-
"The VEN_NAME target value
|
|
187
|
+
"The VEN_NAME target value may not be empty.",
|
|
195
188
|
),
|
|
196
189
|
loc=("targets",),
|
|
197
190
|
input=self.targets,
|
|
@@ -199,12 +192,12 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
199
192
|
)
|
|
200
193
|
)
|
|
201
194
|
|
|
202
|
-
if not all(1 <= len(v) <= 128 for v in ven_name.values):
|
|
195
|
+
if not all(1 <= len(v) <= 128 for v in ven_name.values): # noqa: PLR2004
|
|
203
196
|
validation_errors.append(
|
|
204
197
|
InitErrorDetails(
|
|
205
198
|
type=PydanticCustomError(
|
|
206
199
|
"value_error",
|
|
207
|
-
"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).",
|
|
208
201
|
),
|
|
209
202
|
loc=("targets",),
|
|
210
203
|
input=self.targets,
|
|
@@ -212,17 +205,18 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
|
212
205
|
)
|
|
213
206
|
)
|
|
214
207
|
|
|
215
|
-
return
|
|
208
|
+
return validation_errors
|
|
216
209
|
|
|
217
210
|
|
|
218
|
-
def
|
|
211
|
+
def _payload_descriptors_gac_compliant(
|
|
219
212
|
self: Event,
|
|
220
|
-
) ->
|
|
221
|
-
"""
|
|
213
|
+
) -> list[InitErrorDetails]:
|
|
214
|
+
"""
|
|
215
|
+
Validates that the payload descriptor is GAC compliant.
|
|
222
216
|
|
|
223
|
-
|
|
217
|
+
The following constraints are enforced for payload descriptors:
|
|
224
218
|
|
|
225
|
-
- The event interval must exactly one payload descriptor.
|
|
219
|
+
- The event interval must contain exactly one payload descriptor.
|
|
226
220
|
- The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'
|
|
227
221
|
- The payload descriptor must have a units of 'KW' (case sensitive).
|
|
228
222
|
"""
|
|
@@ -255,9 +249,9 @@ def _payload_descriptor_gac_compliant(
|
|
|
255
249
|
)
|
|
256
250
|
)
|
|
257
251
|
|
|
258
|
-
|
|
252
|
+
payload_descriptors = self.payload_descriptors[0]
|
|
259
253
|
|
|
260
|
-
if
|
|
254
|
+
if payload_descriptors.payload_type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
261
255
|
validation_errors.append(
|
|
262
256
|
InitErrorDetails(
|
|
263
257
|
type=PydanticCustomError(
|
|
@@ -270,7 +264,7 @@ def _payload_descriptor_gac_compliant(
|
|
|
270
264
|
)
|
|
271
265
|
)
|
|
272
266
|
|
|
273
|
-
if
|
|
267
|
+
if payload_descriptors.units != "KW":
|
|
274
268
|
validation_errors.append(
|
|
275
269
|
InitErrorDetails(
|
|
276
270
|
type=PydanticCustomError(
|
|
@@ -283,13 +277,14 @@ def _payload_descriptor_gac_compliant(
|
|
|
283
277
|
)
|
|
284
278
|
)
|
|
285
279
|
|
|
286
|
-
return
|
|
280
|
+
return validation_errors
|
|
287
281
|
|
|
288
282
|
|
|
289
|
-
def _event_interval_gac_compliant(self: Event) ->
|
|
290
|
-
"""
|
|
283
|
+
def _event_interval_gac_compliant(self: Event) -> list[InitErrorDetails]:
|
|
284
|
+
"""
|
|
285
|
+
Validates that the event interval is GAC compliant.
|
|
291
286
|
|
|
292
|
-
|
|
287
|
+
The following constraints are enforced for event intervals:
|
|
293
288
|
|
|
294
289
|
- The event interval must have an id value that is strictly increasing.
|
|
295
290
|
- The event interval must have exactly one payload.
|
|
@@ -364,22 +359,34 @@ def _event_interval_gac_compliant(self: Event) -> Tuple[Event, list[InitErrorDet
|
|
|
364
359
|
ctx={},
|
|
365
360
|
)
|
|
366
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
|
+
)
|
|
367
374
|
|
|
368
|
-
return
|
|
375
|
+
return validation_errors
|
|
369
376
|
|
|
370
377
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
def validate_event_gac_compliant(event: Event) -> list[InitErrorDetails] | None:
|
|
379
|
+
"""
|
|
380
|
+
Validates that events are GAC compliant.
|
|
374
381
|
|
|
375
|
-
|
|
382
|
+
The following constraints are enforced for events:
|
|
376
383
|
|
|
377
384
|
- The event must not have a priority set.
|
|
378
|
-
- The event must have either a continuous or
|
|
385
|
+
- The event must have either a continuous or separated interval definition.
|
|
379
386
|
"""
|
|
380
387
|
validation_errors: list[InitErrorDetails] = []
|
|
381
388
|
|
|
382
|
-
if
|
|
389
|
+
if event.priority is not None:
|
|
383
390
|
validation_errors.append(
|
|
384
391
|
InitErrorDetails(
|
|
385
392
|
type=PydanticCustomError(
|
|
@@ -387,30 +394,21 @@ def event_gac_compliant(self: Event) -> Event:
|
|
|
387
394
|
"The event must not have a priority set for GAC 2.0 compliance",
|
|
388
395
|
),
|
|
389
396
|
loc=("priority",),
|
|
390
|
-
input=
|
|
397
|
+
input=event.priority,
|
|
391
398
|
ctx={},
|
|
392
399
|
)
|
|
393
400
|
)
|
|
394
401
|
|
|
395
|
-
|
|
402
|
+
errors = _continuous_or_separated(event)
|
|
396
403
|
validation_errors.extend(errors)
|
|
397
404
|
|
|
398
|
-
|
|
405
|
+
errors = _targets_compliant(event)
|
|
399
406
|
validation_errors.extend(errors)
|
|
400
407
|
|
|
401
|
-
|
|
402
|
-
targets_validated
|
|
403
|
-
)
|
|
408
|
+
errors = _payload_descriptors_gac_compliant(event)
|
|
404
409
|
validation_errors.extend(errors)
|
|
405
410
|
|
|
406
|
-
|
|
407
|
-
payload_descriptor_validated
|
|
408
|
-
)
|
|
411
|
+
errors = _event_interval_gac_compliant(event)
|
|
409
412
|
validation_errors.extend(errors)
|
|
410
413
|
|
|
411
|
-
if validation_errors
|
|
412
|
-
raise ValidationError.from_exception_data(
|
|
413
|
-
title=self.__class__.__name__, line_errors=validation_errors
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
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
|
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
"""Module which implements GAC compliance validators for the program OpenADR3 types."""
|
|
2
2
|
|
|
3
|
-
from openadr3_client.models.program.program import Program
|
|
4
|
-
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
5
|
-
|
|
6
3
|
import re
|
|
7
4
|
|
|
8
|
-
from
|
|
5
|
+
from openadr3_client.models.program.program import Program
|
|
9
6
|
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
10
7
|
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
def validate_program_gac_compliant(program: Program) -> list[InitErrorDetails] | None:
|
|
10
|
+
"""
|
|
11
|
+
Validates that the program is GAC compliant.
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
The following constraints are enforced for programs:
|
|
17
14
|
- The program must have a retailer name
|
|
18
15
|
- The retailer name must be between 2 and 128 characters long.
|
|
19
16
|
- The program MUST have a programType.
|
|
20
17
|
- The programType MUST equal "DSO_CPO_INTERFACE-x.x.x, where x.x.x is the version as defined in the GAC specification.
|
|
21
|
-
- The program MUST have bindingEvents set to
|
|
22
|
-
|
|
23
|
-
"""
|
|
18
|
+
- The program MUST have bindingEvents set to true.
|
|
19
|
+
|
|
20
|
+
""" # noqa: E501
|
|
24
21
|
validation_errors: list[InitErrorDetails] = []
|
|
25
22
|
|
|
26
|
-
program_type_regex =
|
|
23
|
+
program_type_regex = (
|
|
24
|
+
r"^DSO_CPO_INTERFACE-"
|
|
25
|
+
r"(0|[1-9]\d*)\."
|
|
26
|
+
r"(0|[1-9]\d*)\."
|
|
27
|
+
r"(0|[1-9]\d*)"
|
|
28
|
+
r"(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
|
|
29
|
+
r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))"
|
|
30
|
+
r"?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"
|
|
31
|
+
r"$"
|
|
32
|
+
)
|
|
27
33
|
|
|
28
|
-
if
|
|
34
|
+
if program.retailer_name is None:
|
|
29
35
|
validation_errors.append(
|
|
30
36
|
InitErrorDetails(
|
|
31
37
|
type=PydanticCustomError(
|
|
@@ -33,13 +39,13 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
33
39
|
"The program must have a retailer name.",
|
|
34
40
|
),
|
|
35
41
|
loc=("retailer_name",),
|
|
36
|
-
input=
|
|
42
|
+
input=program.retailer_name,
|
|
37
43
|
ctx={},
|
|
38
44
|
)
|
|
39
45
|
)
|
|
40
46
|
|
|
41
|
-
if
|
|
42
|
-
len(
|
|
47
|
+
if program.retailer_name is not None and (
|
|
48
|
+
len(program.retailer_name) < 2 or len(program.retailer_name) > 128 # noqa: PLR2004
|
|
43
49
|
):
|
|
44
50
|
validation_errors.append(
|
|
45
51
|
InitErrorDetails(
|
|
@@ -48,12 +54,12 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
48
54
|
"The retailer name must be between 2 and 128 characters long.",
|
|
49
55
|
),
|
|
50
56
|
loc=("retailer_name",),
|
|
51
|
-
input=
|
|
57
|
+
input=program.retailer_name,
|
|
52
58
|
ctx={},
|
|
53
59
|
)
|
|
54
60
|
)
|
|
55
61
|
|
|
56
|
-
if
|
|
62
|
+
if program.program_type is None:
|
|
57
63
|
validation_errors.append(
|
|
58
64
|
InitErrorDetails(
|
|
59
65
|
type=PydanticCustomError(
|
|
@@ -61,13 +67,11 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
61
67
|
"The program must have a program type.",
|
|
62
68
|
),
|
|
63
69
|
loc=("program_type",),
|
|
64
|
-
input=
|
|
70
|
+
input=program.program_type,
|
|
65
71
|
ctx={},
|
|
66
72
|
)
|
|
67
73
|
)
|
|
68
|
-
if
|
|
69
|
-
program_type_regex, self.program_type
|
|
70
|
-
):
|
|
74
|
+
if program.program_type is not None and not re.fullmatch(program_type_regex, program.program_type):
|
|
71
75
|
validation_errors.append(
|
|
72
76
|
InitErrorDetails(
|
|
73
77
|
type=PydanticCustomError(
|
|
@@ -75,27 +79,22 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
75
79
|
"The program type must follow the format DSO_CPO_INTERFACE-x.x.x.",
|
|
76
80
|
),
|
|
77
81
|
loc=("program_type",),
|
|
78
|
-
input=
|
|
82
|
+
input=program.program_type,
|
|
79
83
|
ctx={},
|
|
80
84
|
)
|
|
81
85
|
)
|
|
82
86
|
|
|
83
|
-
if
|
|
87
|
+
if program.binding_events is False:
|
|
84
88
|
validation_errors.append(
|
|
85
89
|
InitErrorDetails(
|
|
86
90
|
type=PydanticCustomError(
|
|
87
91
|
"value_error",
|
|
88
|
-
"The program must have bindingEvents set to
|
|
92
|
+
"The program must have bindingEvents set to true.",
|
|
89
93
|
),
|
|
90
94
|
loc=("binding_events",),
|
|
91
|
-
input=
|
|
95
|
+
input=program.binding_events,
|
|
92
96
|
ctx={},
|
|
93
97
|
)
|
|
94
98
|
)
|
|
95
99
|
|
|
96
|
-
if validation_errors
|
|
97
|
-
raise ValidationError.from_exception_data(
|
|
98
|
-
title=self.__class__.__name__, line_errors=validation_errors
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
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
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "openadr3-client-gac-compliance"
|
|
3
|
+
version = "3.0.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Nick van der Burgt", email = "nick.van.der.burgt@elaad.nl"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12, <4"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pydantic (>=2.11.2,<3.0.0)",
|
|
12
|
+
"openadr3-client (>=1.0.0a1,<2.0.0)",
|
|
13
|
+
"pycountry (>=24.6.1,<25.0.0)",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
mypy = "^1.15.0"
|
|
22
|
+
ruff = "^0.11.4"
|
|
23
|
+
pytest = "^8.3.5"
|
|
24
|
+
pytest-cov = "^6.1.1"
|
|
25
|
+
taskipy = "^1.14.1"
|
|
26
|
+
|
|
27
|
+
[[tool.mypy.overrides]]
|
|
28
|
+
module = ["openadr3_client"]
|
|
29
|
+
ignore_missing_imports = true
|
|
30
|
+
|
|
31
|
+
[tool.taskipy.tasks]
|
|
32
|
+
local-ci = "task check && task format && task mypy && task test"
|
|
33
|
+
fix = "task check-fix && task format-fix && task mypy"
|
|
34
|
+
check = "ruff check ."
|
|
35
|
+
check-fix = "ruff check --fix ."
|
|
36
|
+
check-ci = "ruff check --output-format=github --no-cache ."
|
|
37
|
+
mypy = "mypy . --check-untyped-defs"
|
|
38
|
+
format = "ruff format . --diff"
|
|
39
|
+
format-fix = "ruff format ."
|
|
40
|
+
format-ci = "ruff format --diff --no-cache ."
|
|
41
|
+
test = "pytest --cov"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 120
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["ALL"]
|
|
48
|
+
|
|
49
|
+
# Do not require docstrings for functions, methods and classes
|
|
50
|
+
ignore = [
|
|
51
|
+
# From:https://github.com/wemake-services/wemake-django-template/blob/master/pyproject.toml
|
|
52
|
+
"A005", # allow to shadow stdlib and builtin module names
|
|
53
|
+
"COM812", # trailing comma, conflicts with `ruff format`
|
|
54
|
+
# Different doc rules that we don't really care about:
|
|
55
|
+
"D100",
|
|
56
|
+
"D104",
|
|
57
|
+
"D106",
|
|
58
|
+
"D203",
|
|
59
|
+
"D212",
|
|
60
|
+
"D401",
|
|
61
|
+
"D404",
|
|
62
|
+
"D405",
|
|
63
|
+
"ISC001", # implicit string concat conflicts with `ruff format`
|
|
64
|
+
"ISC003", # prefer explicit string concat over implicit concat
|
|
65
|
+
"PLR09", # we have our own complexity rules
|
|
66
|
+
# "PLR2004", # do not report magic numbers -> I do want to report magic numbers
|
|
67
|
+
"PLR6301", # do not require classmethod / staticmethod when self not used
|
|
68
|
+
"TRY003", # long exception messages from `tryceratops`
|
|
69
|
+
# Start of own error codes
|
|
70
|
+
"ANN002", # Ignore unused *args
|
|
71
|
+
"ANN003", # Ignore unused **kwargs
|
|
72
|
+
"D107", # Allow undocumented __init__
|
|
73
|
+
"TC001", # Move to type checking block (does not play nice with pydantic models)
|
|
74
|
+
"TC002", # Move to type checking block (does not play nice with pydantic models)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Ignores S101 ("Use of 'assert' detected") for the tests.
|
|
78
|
+
[tool.ruff.lint.per-file-ignores]
|
|
79
|
+
"tests/*" = ["S101", "ANN201", "SLF001", "S106", "S105", "S311", "PLR2004", "PT018"]
|
|
@@ -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.program_gac_compliant # noqa: F401
|
|
5
|
-
import openadr3_client_gac_compliance.gac20.event_gac_compliant # noqa: F401
|
|
6
|
-
import openadr3_client_gac_compliance.gac20.ven_gac_compliant # noqa: F401
|
|
@@ -1,26 +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
|
-
"""Cast the GAC version to a string.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
value (str): The GAC version to cast.
|
|
13
|
-
|
|
14
|
-
Raises:
|
|
15
|
-
ValueError: If the GAC version is not a valid GAC version.
|
|
16
|
-
|
|
17
|
-
Returns:
|
|
18
|
-
str: The GAC version.
|
|
19
|
-
"""
|
|
20
|
-
if value not in VALID_GAC_VERSIONS:
|
|
21
|
-
raise ValueError(f"Invalid GAC version: {value}")
|
|
22
|
-
return value
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# The GAC version to use for the compliance validators.
|
|
26
|
-
GAC_VERSION = config("GAC_VERSION", default="2.0", cast=_gac_version_cast)
|
openadr3_client_gac_compliance-1.4.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
3
|
-
from openadr3_client.models.ven.ven import Ven
|
|
4
|
-
import pycountry
|
|
5
|
-
|
|
6
|
-
from pydantic import ValidationError
|
|
7
|
-
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@ValidatorRegistry.register(Ven, ValidatorModel())
|
|
11
|
-
def ven_gac_compliant(self: Ven) -> Ven:
|
|
12
|
-
"""Enforces that the ven is GAC compliant.
|
|
13
|
-
|
|
14
|
-
GAC enforces the following constraints for vens:
|
|
15
|
-
- The ven must have a ven name
|
|
16
|
-
- The ven name must be an eMI3 identifier.
|
|
17
|
-
"""
|
|
18
|
-
validation_errors: list[InitErrorDetails] = []
|
|
19
|
-
|
|
20
|
-
emi3_identifier_regex = r"^[A-Z]{2}-?[A-Z0-9]{3}$"
|
|
21
|
-
|
|
22
|
-
if not re.fullmatch(emi3_identifier_regex, self.ven_name):
|
|
23
|
-
validation_errors.append(
|
|
24
|
-
InitErrorDetails(
|
|
25
|
-
type=PydanticCustomError(
|
|
26
|
-
"value_error",
|
|
27
|
-
"The ven name must be formatted as an eMI3 identifier.",
|
|
28
|
-
),
|
|
29
|
-
loc=("ven_name",),
|
|
30
|
-
input=self.ven_name,
|
|
31
|
-
ctx={},
|
|
32
|
-
)
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
alpha_2_country = pycountry.countries.get(alpha_2=self.ven_name[:2])
|
|
36
|
-
|
|
37
|
-
if alpha_2_country is None:
|
|
38
|
-
validation_errors.append(
|
|
39
|
-
InitErrorDetails(
|
|
40
|
-
type=PydanticCustomError(
|
|
41
|
-
"value_error",
|
|
42
|
-
"The first two characters of the ven name must be a valid ISO 3166-1 alpha-2 country code.",
|
|
43
|
-
),
|
|
44
|
-
loc=("ven_name",),
|
|
45
|
-
input=self.ven_name,
|
|
46
|
-
ctx={},
|
|
47
|
-
)
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
if validation_errors:
|
|
51
|
-
raise ValidationError.from_exception_data(
|
|
52
|
-
title=self.__class__.__name__, line_errors=validation_errors
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
return self
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "openadr3-client-gac-compliance"
|
|
3
|
-
version = "1.4.0"
|
|
4
|
-
description = ""
|
|
5
|
-
authors = [
|
|
6
|
-
{name = "Nick van der Burgt", email = "nick.van.der.burgt@elaad.nl"}
|
|
7
|
-
]
|
|
8
|
-
readme = "README.md"
|
|
9
|
-
requires-python = ">=3.12, <4"
|
|
10
|
-
dependencies = [
|
|
11
|
-
"pydantic (>=2.11.2,<3.0.0)",
|
|
12
|
-
"openadr3-client (>=0.0.7,<1.0.0)",
|
|
13
|
-
"pycountry (>=24.6.1,<25.0.0)",
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
[build-system]
|
|
17
|
-
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
18
|
-
build-backend = "poetry.core.masonry.api"
|
|
19
|
-
|
|
20
|
-
[tool.poetry.group.dev.dependencies]
|
|
21
|
-
mypy = "^1.15.0"
|
|
22
|
-
ruff = "^0.11.4"
|
|
23
|
-
pytest = "^8.3.5"
|
|
24
|
-
pytest-cov = "^6.1.1"
|
|
25
|
-
|
|
26
|
-
[[tool.mypy.overrides]]
|
|
27
|
-
module = ["openadr3_client", "decouple"]
|
|
28
|
-
ignore_missing_imports = true
|
|
File without changes
|