openadr3-client-gac-compliance 1.4.0__tar.gz → 2.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.
@@ -1,16 +1,19 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: openadr3-client-gac-compliance
3
- Version: 1.4.0
3
+ Version: 2.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
- Requires-Dist: openadr3-client (>=0.0.7,<1.0.0)
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: openadr3-client (>=0.0.11,<1.0.0)
12
14
  Requires-Dist: pycountry (>=24.6.1,<25.0.0)
13
15
  Requires-Dist: pydantic (>=2.11.2,<3.0.0)
16
+ Requires-Dist: python-decouple (>=3.8,<4.0)
14
17
  Description-Content-Type: text/markdown
15
18
 
16
19
  # OpenADR3 client
@@ -1,6 +1,6 @@
1
1
  from openadr3_client_gac_compliance.config import GAC_VERSION
2
2
 
3
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
4
+ import openadr3_client_gac_compliance.gac20.event_gac_compliant
5
+ import openadr3_client_gac_compliance.gac20.program_gac_compliant
6
6
  import openadr3_client_gac_compliance.gac20.ven_gac_compliant # noqa: F401
@@ -6,7 +6,8 @@ VALID_GAC_VERSIONS: list[str] = ["2.0"]
6
6
 
7
7
 
8
8
  def _gac_version_cast(value: str) -> str:
9
- """Cast the GAC version to a string.
9
+ """
10
+ Cast the GAC version to a string.
10
11
 
11
12
  Args:
12
13
  value (str): The GAC version to cast.
@@ -16,9 +17,11 @@ def _gac_version_cast(value: str) -> str:
16
17
 
17
18
  Returns:
18
19
  str: The GAC version.
20
+
19
21
  """
20
22
  if value not in VALID_GAC_VERSIONS:
21
- raise ValueError(f"Invalid GAC version: {value}")
23
+ msg = f"Invalid GAC version: {value}"
24
+ raise ValueError(msg)
22
25
  return value
23
26
 
24
27
 
@@ -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
- """Module which implements GAC compliance validators for the event OpenADR3 types.
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.
@@ -11,34 +12,35 @@ that a safe mode event is present in a program. And it cannot be validated on th
11
12
  as the program object does not contain the events, these are stored seperately in the VTN.
12
13
  """
13
14
 
14
- from itertools import pairwise
15
15
  import re
16
- from typing import Tuple
17
- from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
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
-
20
+ from openadr3_client.models.model import Model as ValidatorModel
21
+ from openadr3_client.models.model import ValidatorRegistry
21
22
  from pydantic import ValidationError
22
23
  from pydantic_core import InitErrorDetails, PydanticCustomError
23
24
 
24
25
 
25
- def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
26
- """Enforces that events either have consistent interval definitions compliant with GAC.
26
+ def _continuous_or_seperated(self: Event) -> tuple[Event, list[InitErrorDetails]]:
27
+ """
28
+ Enforces that events either have consistent interval definitions compliant with GAC.
27
29
 
28
30
  the Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
29
31
  interval definitions:
30
32
 
31
33
  1. Continuous
32
- 2. Seperated
34
+ 2. Separated
33
35
 
34
- The continious implementation can be used when all intervals have the same duration.
36
+ The continuous implementation can be used when all intervals have the same duration.
35
37
  In this case, only the top-level intervalPeriod of the event can be used, and the intervalPeriods
36
38
  of the individual intervals must be None.
37
39
 
38
- In the seperated intervalDefinition approach, the intervalPeriods must be set on each individual intervals,
39
- and the top-level intervalPeriod of the event must be None. This seperated approach is used when events have differing
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
40
42
  durations.
41
- """
43
+ """ # noqa: E501
42
44
  validation_errors: list[InitErrorDetails] = []
43
45
 
44
46
  intervals = self.intervals or ()
@@ -52,7 +54,7 @@ def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]
52
54
  InitErrorDetails(
53
55
  type=PydanticCustomError(
54
56
  "value_error",
55
- "Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.",
57
+ "Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.", # noqa: E501
56
58
  ),
57
59
  loc=("intervals",),
58
60
  input=self.intervals,
@@ -62,15 +64,13 @@ def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]
62
64
  else:
63
65
  # interval period set at top level of the event.
64
66
  # 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
- ]
67
+ duplicate_interval_period = [i for i in intervals if i.interval_period is not None]
68
68
  if duplicate_interval_period:
69
69
  validation_errors.append(
70
70
  InitErrorDetails(
71
71
  type=PydanticCustomError(
72
72
  "value_error",
73
- "Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.",
73
+ "Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.", # noqa: E501
74
74
  ),
75
75
  loc=("intervals",),
76
76
  input=self.intervals,
@@ -81,8 +81,9 @@ def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]
81
81
  return self, validation_errors
82
82
 
83
83
 
84
- def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
85
- """Enforces that the targets of the event are compliant with GAC.
84
+ def _targets_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
85
+ """
86
+ Enforces that the targets of the event are compliant with GAC.
86
87
 
87
88
  GAC enforces the following constraints for targets:
88
89
 
@@ -149,12 +150,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
149
150
  )
150
151
  )
151
152
 
152
- if (
153
- power_service_locations
154
- and ven_names
155
- and len(power_service_locations) == 1
156
- and len(ven_names) == 1
157
- ):
153
+ if power_service_locations and ven_names and len(power_service_locations) == 1 and len(ven_names) == 1:
158
154
  power_service_location = power_service_locations[0]
159
155
  ven_name = ven_names[0]
160
156
 
@@ -171,9 +167,7 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
171
167
  )
172
168
  )
173
169
 
174
- if not all(
175
- re.fullmatch(r"^EAN\d{15}$", v) for v in power_service_location.values
176
- ):
170
+ if not all(re.fullmatch(r"^\d{18}$", v) for v in power_service_location.values):
177
171
  validation_errors.append(
178
172
  InitErrorDetails(
179
173
  type=PydanticCustomError(
@@ -199,12 +193,12 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
199
193
  )
200
194
  )
201
195
 
202
- if not all(1 <= len(v) <= 128 for v in ven_name.values):
196
+ if not all(1 <= len(v) <= 128 for v in ven_name.values): # noqa: PLR2004
203
197
  validation_errors.append(
204
198
  InitErrorDetails(
205
199
  type=PydanticCustomError(
206
200
  "value_error",
207
- "The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).",
201
+ "The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).", # noqa: E501
208
202
  ),
209
203
  loc=("targets",),
210
204
  input=self.targets,
@@ -215,10 +209,11 @@ def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
215
209
  return self, validation_errors
216
210
 
217
211
 
218
- def _payload_descriptor_gac_compliant(
212
+ def _payload_descriptors_gac_compliant(
219
213
  self: Event,
220
- ) -> Tuple[Event, list[InitErrorDetails]]:
221
- """Enforces that the payload descriptor is GAC compliant.
214
+ ) -> tuple[Event, list[InitErrorDetails]]:
215
+ """
216
+ Enforces that the payload descriptor is GAC compliant.
222
217
 
223
218
  GAC enforces the following constraints for payload descriptors:
224
219
 
@@ -255,9 +250,9 @@ def _payload_descriptor_gac_compliant(
255
250
  )
256
251
  )
257
252
 
258
- payload_descriptor = self.payload_descriptors[0]
253
+ payload_descriptors = self.payload_descriptors[0]
259
254
 
260
- if payload_descriptor.payload_type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
255
+ if payload_descriptors.payload_type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
261
256
  validation_errors.append(
262
257
  InitErrorDetails(
263
258
  type=PydanticCustomError(
@@ -270,7 +265,7 @@ def _payload_descriptor_gac_compliant(
270
265
  )
271
266
  )
272
267
 
273
- if payload_descriptor.units != "KW":
268
+ if payload_descriptors.units != "KW":
274
269
  validation_errors.append(
275
270
  InitErrorDetails(
276
271
  type=PydanticCustomError(
@@ -286,8 +281,9 @@ def _payload_descriptor_gac_compliant(
286
281
  return self, validation_errors
287
282
 
288
283
 
289
- def _event_interval_gac_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
290
- """Enforces that the event interval is GAC compliant.
284
+ def _event_interval_gac_compliant(self: Event) -> tuple[Event, list[InitErrorDetails]]:
285
+ """
286
+ Enforces that the event interval is GAC compliant.
291
287
 
292
288
  GAC enforces the following constraints for event intervals:
293
289
 
@@ -370,7 +366,8 @@ def _event_interval_gac_compliant(self: Event) -> Tuple[Event, list[InitErrorDet
370
366
 
371
367
  @ValidatorRegistry.register(Event, ValidatorModel())
372
368
  def event_gac_compliant(self: Event) -> Event:
373
- """Enforces that events are GAC compliant.
369
+ """
370
+ Enforces that events are GAC compliant.
374
371
 
375
372
  GAC enforces the following constraints for events:
376
373
 
@@ -398,19 +395,13 @@ def event_gac_compliant(self: Event) -> Event:
398
395
  targets_validated, errors = _targets_compliant(interval_periods_validated)
399
396
  validation_errors.extend(errors)
400
397
 
401
- payload_descriptor_validated, errors = _payload_descriptor_gac_compliant(
402
- targets_validated
403
- )
398
+ payload_descriptor_validated, errors = _payload_descriptors_gac_compliant(targets_validated)
404
399
  validation_errors.extend(errors)
405
400
 
406
- event_interval_validated, errors = _event_interval_gac_compliant(
407
- payload_descriptor_validated
408
- )
401
+ event_interval_validated, errors = _event_interval_gac_compliant(payload_descriptor_validated)
409
402
  validation_errors.extend(errors)
410
403
 
411
404
  if validation_errors:
412
- raise ValidationError.from_exception_data(
413
- title=self.__class__.__name__, line_errors=validation_errors
414
- )
405
+ raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
415
406
 
416
407
  return event_interval_validated
@@ -1,17 +1,18 @@
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
 
5
+ from openadr3_client.models.model import Model as ValidatorModel
6
+ from openadr3_client.models.model import ValidatorRegistry
7
+ from openadr3_client.models.program.program import Program
8
8
  from pydantic import ValidationError
9
9
  from pydantic_core import InitErrorDetails, PydanticCustomError
10
10
 
11
11
 
12
12
  @ValidatorRegistry.register(Program, ValidatorModel())
13
13
  def program_gac_compliant(self: Program) -> Program:
14
- """Enforces that the program is GAC compliant.
14
+ """
15
+ Enforces that the program is GAC compliant.
15
16
 
16
17
  GAC enforces the following constraints for programs:
17
18
  - The program must have a retailer name
@@ -20,10 +21,19 @@ def program_gac_compliant(self: Program) -> Program:
20
21
  - The programType MUST equal "DSO_CPO_INTERFACE-x.x.x, where x.x.x is the version as defined in the GAC specification.
21
22
  - The program MUST have bindingEvents set to True.
22
23
  are allowed there.
23
- """
24
+ """ # noqa: E501
24
25
  validation_errors: list[InitErrorDetails] = []
25
26
 
26
- program_type_regex = r"^DSO_CPO_INTERFACE-(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
27
+ program_type_regex = (
28
+ r"^DSO_CPO_INTERFACE-"
29
+ r"(0|[1-9]\d*)\."
30
+ r"(0|[1-9]\d*)\."
31
+ r"(0|[1-9]\d*)"
32
+ r"(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
33
+ r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))"
34
+ r"?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"
35
+ r"$"
36
+ )
27
37
 
28
38
  if self.retailer_name is None:
29
39
  validation_errors.append(
@@ -39,7 +49,7 @@ def program_gac_compliant(self: Program) -> Program:
39
49
  )
40
50
 
41
51
  if self.retailer_name is not None and (
42
- len(self.retailer_name) < 2 or len(self.retailer_name) > 128
52
+ len(self.retailer_name) < 2 or len(self.retailer_name) > 128 # noqa: PLR2004
43
53
  ):
44
54
  validation_errors.append(
45
55
  InitErrorDetails(
@@ -65,9 +75,7 @@ def program_gac_compliant(self: Program) -> Program:
65
75
  ctx={},
66
76
  )
67
77
  )
68
- if self.program_type is not None and not re.fullmatch(
69
- program_type_regex, self.program_type
70
- ):
78
+ if self.program_type is not None and not re.fullmatch(program_type_regex, self.program_type):
71
79
  validation_errors.append(
72
80
  InitErrorDetails(
73
81
  type=PydanticCustomError(
@@ -94,8 +102,6 @@ def program_gac_compliant(self: Program) -> Program:
94
102
  )
95
103
 
96
104
  if validation_errors:
97
- raise ValidationError.from_exception_data(
98
- title=self.__class__.__name__, line_errors=validation_errors
99
- )
105
+ raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
100
106
 
101
107
  return self
@@ -1,15 +1,17 @@
1
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
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
6
7
  from pydantic import ValidationError
7
8
  from pydantic_core import InitErrorDetails, PydanticCustomError
8
9
 
9
10
 
10
11
  @ValidatorRegistry.register(Ven, ValidatorModel())
11
12
  def ven_gac_compliant(self: Ven) -> Ven:
12
- """Enforces that the ven is GAC compliant.
13
+ """
14
+ Enforces that the ven is GAC compliant.
13
15
 
14
16
  GAC enforces the following constraints for vens:
15
17
  - The ven must have a ven name
@@ -48,8 +50,6 @@ def ven_gac_compliant(self: Ven) -> Ven:
48
50
  )
49
51
 
50
52
  if validation_errors:
51
- raise ValidationError.from_exception_data(
52
- title=self.__class__.__name__, line_errors=validation_errors
53
- )
53
+ raise ValidationError.from_exception_data(title=self.__class__.__name__, line_errors=validation_errors)
54
54
 
55
55
  return self
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "openadr3-client-gac-compliance"
3
+ version = "2.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 (>=0.0.11,<1.0.0)",
13
+ "pycountry (>=24.6.1,<25.0.0)",
14
+ "python-decouple (>=3.8,<4.0)",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
19
+ build-backend = "poetry.core.masonry.api"
20
+
21
+ [tool.poetry.group.dev.dependencies]
22
+ mypy = "^1.15.0"
23
+ ruff = "^0.11.4"
24
+ pytest = "^8.3.5"
25
+ pytest-cov = "^6.1.1"
26
+ taskipy = "^1.14.1"
27
+
28
+ [[tool.mypy.overrides]]
29
+ module = ["openadr3_client", "decouple"]
30
+ ignore_missing_imports = true
31
+
32
+ [tool.taskipy.tasks]
33
+ local-ci = "task check && task format && task mypy && task test"
34
+ fix = "task check-fix && task format-fix && task mypy"
35
+ check = "ruff check ."
36
+ check-fix = "ruff check --fix ."
37
+ check-ci = "ruff check --output-format=github --no-cache ."
38
+ mypy = "mypy . --check-untyped-defs"
39
+ format = "ruff format . --diff"
40
+ format-fix = "ruff format ."
41
+ format-ci = "ruff format --diff --no-cache ."
42
+ test = "pytest --cov"
43
+
44
+ [tool.ruff]
45
+ line-length = 120
46
+
47
+ [tool.ruff.lint]
48
+ select = ["ALL"]
49
+
50
+ # Do not require docstrings for functions, methods and classes
51
+ ignore = [
52
+ # From:https://github.com/wemake-services/wemake-django-template/blob/master/pyproject.toml
53
+ "A005", # allow to shadow stdlib and builtin module names
54
+ "COM812", # trailing comma, conflicts with `ruff format`
55
+ # Different doc rules that we don't really care about:
56
+ "D100",
57
+ "D104",
58
+ "D106",
59
+ "D203",
60
+ "D212",
61
+ "D401",
62
+ "D404",
63
+ "D405",
64
+ "ISC001", # implicit string concat conflicts with `ruff format`
65
+ "ISC003", # prefer explicit string concat over implicit concat
66
+ "PLR09", # we have our own complexity rules
67
+ # "PLR2004", # do not report magic numbers -> I do want to report magic numbers
68
+ "PLR6301", # do not require classmethod / staticmethod when self not used
69
+ "TRY003", # long exception messages from `tryceratops`
70
+ # Start of own error codes
71
+ "ANN002", # Ignore unused *args
72
+ "ANN003", # Ignore unused **kwargs
73
+ "D107", # Allow undocumented __init__
74
+ "TC001", # Move to type checking block (does not play nice with pydantic models)
75
+ "TC002", # Move to type checking block (does not play nice with pydantic models)
76
+ ]
77
+
78
+ # Ignores S101 ("Use of 'assert' detected") for the tests.
79
+ [tool.ruff.lint.per-file-ignores]
80
+ "tests/*" = ["S101", "ANN201", "SLF001", "S106", "S105", "S311", "PLR2004", "PT018"]
@@ -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