openadr3-client-gac-compliance 2.0.0__tar.gz → 3.0.0a2__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.
Files changed (14) hide show
  1. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/PKG-INFO +9 -5
  2. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/README.md +7 -2
  3. openadr3_client_gac_compliance-3.0.0a2/openadr3_client_gac_compliance/__init__.py +22 -0
  4. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/openadr3_client_gac_compliance/gac20/event_gac_compliant.py +41 -46
  5. openadr3_client_gac_compliance-3.0.0a2/openadr3_client_gac_compliance/gac20/plugin.py +43 -0
  6. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/openadr3_client_gac_compliance/gac20/program_gac_compliant.py +18 -25
  7. openadr3_client_gac_compliance-3.0.0a2/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +49 -0
  8. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/pyproject.toml +3 -4
  9. openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/__init__.py +0 -6
  10. openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/config.py +0 -29
  11. openadr3_client_gac_compliance-2.0.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +0 -55
  12. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/LICENSE.md +0 -0
  13. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/openadr3_client_gac_compliance/gac20/__init__.py +0 -0
  14. {openadr3_client_gac_compliance-2.0.0 → openadr3_client_gac_compliance-3.0.0a2}/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: 2.0.0
3
+ Version: 3.0.0a2
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 (>=0.0.11,<1.0.0)
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
- To use this plugin, the package must be imported once globally. We recommend doing this in your root directories `__init__.py` file.
22
+ Registering the plugin is done using the global ValidatorPluginRegistry class:
24
23
 
25
24
  ```python
26
- import openadr3_client_gac_compliance # noqa: F401 (in case you use ruff)
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
- To use this plugin, the package must be imported once globally. We recommend doing this in your root directories `__init__.py` file.
5
+ Registering the plugin is done using the global ValidatorPluginRegistry class:
6
6
 
7
7
  ```python
8
- import openadr3_client_gac_compliance # noqa: F401 (in case you use ruff)
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 seperately in the VTN.
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
- def _continuous_or_seperated(self: Event) -> tuple[Event, list[InitErrorDetails]]:
24
+
25
+ def _continuous_or_separated(self: Event) -> list[InitErrorDetails]:
27
26
  """
28
- Enforces that events either have consistent interval definitions compliant with GAC.
27
+ Validates that events have consistent interval definitions GAC compliant.
29
28
 
30
- the Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
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 can be used, and the intervalPeriods
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 intervalDefinition approach, the intervalPeriods must be set on each individual intervals,
41
- and the top-level intervalPeriod of the event must be None. This separated approach is used when events have differing
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
- "Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.", # noqa: E501
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
- "Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.", # noqa: E501
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 self, validation_errors
80
+ return validation_errors
82
81
 
83
82
 
84
- def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
83
+ def _targets_compliant(self: Event) -> list[InitErrorDetails]:
85
84
  """
86
- Enforces that the targets of the event are compliant with GAC.
85
+ Validates that the targets of the event are GAC compliant.
87
86
 
88
- GAC enforces the following constraints for targets:
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 'ven object name' values (between 1 and 128 characters).
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 only one VEN_NAME target.",
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 cannot be empty.",
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 cannot be empty.",
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 'ven object name' values (between 1 and 128 characters).", # noqa: E501
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 self, validation_errors
208
+ return validation_errors
210
209
 
211
210
 
212
211
  def _payload_descriptors_gac_compliant(
213
212
  self: Event,
214
- ) -> tuple[Event, list[InitErrorDetails]]:
213
+ ) -> list[InitErrorDetails]:
215
214
  """
216
- Enforces that the payload descriptor is GAC compliant.
215
+ Validates that the payload descriptor is GAC compliant.
217
216
 
218
- GAC enforces the following constraints for payload descriptors:
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 self, validation_errors
280
+ return validation_errors
282
281
 
283
282
 
284
- def _event_interval_gac_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
283
+ def _event_interval_gac_compliant(self: Event) -> list[InitErrorDetails]:
285
284
  """
286
- Enforces that the event interval is GAC compliant.
285
+ Validates that the event interval is GAC compliant.
287
286
 
288
- GAC enforces the following constraints for event intervals:
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.
@@ -361,22 +360,21 @@ def _event_interval_gac_compliant(self: Event) -> tuple[Event, list[InitErrorDet
361
360
  )
362
361
  )
363
362
 
364
- return self, validation_errors
363
+ return validation_errors
365
364
 
366
365
 
367
- @ValidatorRegistry.register(Event, ValidatorModel())
368
- def event_gac_compliant(self: Event) -> Event:
366
+ def validate_event_gac_compliant(event: Event) -> list[InitErrorDetails] | None:
369
367
  """
370
- Enforces that events are GAC compliant.
368
+ Validates that events are GAC compliant.
371
369
 
372
- GAC enforces the following constraints for events:
370
+ The following constraints are enforced for events:
373
371
 
374
372
  - The event must not have a priority set.
375
- - The event must have either a continuous or seperated interval definition.
373
+ - The event must have either a continuous or separated interval definition.
376
374
  """
377
375
  validation_errors: list[InitErrorDetails] = []
378
376
 
379
- if self.priority is not None:
377
+ if event.priority is not None:
380
378
  validation_errors.append(
381
379
  InitErrorDetails(
382
380
  type=PydanticCustomError(
@@ -384,24 +382,21 @@ def event_gac_compliant(self: Event) -> Event:
384
382
  "The event must not have a priority set for GAC 2.0 compliance",
385
383
  ),
386
384
  loc=("priority",),
387
- input=self.priority,
385
+ input=event.priority,
388
386
  ctx={},
389
387
  )
390
388
  )
391
389
 
392
- interval_periods_validated, errors = _continuous_or_seperated(self)
390
+ errors = _continuous_or_separated(event)
393
391
  validation_errors.extend(errors)
394
392
 
395
- targets_validated, errors = _targets_compliant(interval_periods_validated)
393
+ errors = _targets_compliant(event)
396
394
  validation_errors.extend(errors)
397
395
 
398
- payload_descriptor_validated, errors = _payload_descriptors_gac_compliant(targets_validated)
396
+ errors = _payload_descriptors_gac_compliant(event)
399
397
  validation_errors.extend(errors)
400
398
 
401
- event_interval_validated, errors = _event_interval_gac_compliant(payload_descriptor_validated)
399
+ errors = _event_interval_gac_compliant(event)
402
400
  validation_errors.extend(errors)
403
401
 
404
- if validation_errors:
405
- raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
406
-
407
- return event_interval_validated
402
+ 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
- @ValidatorRegistry.register(Program, ValidatorModel())
13
- def program_gac_compliant(self: Program) -> Program:
9
+ def validate_program_gac_compliant(program: Program) -> list[InitErrorDetails] | None:
14
10
  """
15
- Enforces that the program is GAC compliant.
11
+ Validates that the program is GAC compliant.
16
12
 
17
- GAC enforces the following constraints for programs:
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 True.
23
- are allowed there.
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 self.retailer_name is None:
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=self.retailer_name,
42
+ input=program.retailer_name,
47
43
  ctx={},
48
44
  )
49
45
  )
50
46
 
51
- if self.retailer_name is not None and (
52
- len(self.retailer_name) < 2 or len(self.retailer_name) > 128 # noqa: PLR2004
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=self.retailer_name,
57
+ input=program.retailer_name,
62
58
  ctx={},
63
59
  )
64
60
  )
65
61
 
66
- if self.program_type is None:
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=self.program_type,
70
+ input=program.program_type,
75
71
  ctx={},
76
72
  )
77
73
  )
78
- if self.program_type is not None and not re.fullmatch(program_type_regex, self.program_type):
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=self.program_type,
82
+ input=program.program_type,
87
83
  ctx={},
88
84
  )
89
85
  )
90
86
 
91
- if self.binding_events is False:
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 True.",
92
+ "The program must have bindingEvents set to true.",
97
93
  ),
98
94
  loc=("binding_events",),
99
- input=self.binding_events,
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openadr3-client-gac-compliance"
3
- version = "2.0.0"
3
+ version = "3.0.0a2"
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 (>=0.0.11,<1.0.0)",
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", "decouple"]
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)
@@ -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