openadr3-client-gac-compliance 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
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/gac20/event_gac_compliant.py +278 -62
- openadr3_client_gac_compliance/gac20/program_gac_compliant.py +70 -8
- openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +31 -3
- {openadr3_client_gac_compliance-1.1.0.dist-info → openadr3_client_gac_compliance-1.2.1.dist-info}/METADATA +2 -2
- {openadr3_client_gac_compliance-1.1.0.dist-info → openadr3_client_gac_compliance-1.2.1.dist-info}/RECORD +7 -7
- {openadr3_client_gac_compliance-1.1.0.dist-info → openadr3_client_gac_compliance-1.2.1.dist-info}/LICENSE.md +0 -0
- {openadr3_client_gac_compliance-1.1.0.dist-info → openadr3_client_gac_compliance-1.2.1.dist-info}/WHEEL +0 -0
|
@@ -13,12 +13,16 @@ as the program object does not contain the events, these are stored seperately i
|
|
|
13
13
|
|
|
14
14
|
from itertools import pairwise
|
|
15
15
|
import re
|
|
16
|
+
from typing import Tuple
|
|
16
17
|
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
17
18
|
from openadr3_client.models.event.event import Event
|
|
18
19
|
from openadr3_client.models.event.event_payload import EventPayloadType
|
|
19
20
|
|
|
21
|
+
from pydantic import ValidationError
|
|
22
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
|
|
25
|
+
def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
22
26
|
"""Enforces that events either have consistent interval definitions compliant with GAC.
|
|
23
27
|
|
|
24
28
|
the Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
|
|
@@ -35,6 +39,8 @@ def _continuous_or_seperated(self: Event) -> Event:
|
|
|
35
39
|
and the top-level intervalPeriod of the event must be None. This seperated approach is used when events have differing
|
|
36
40
|
durations.
|
|
37
41
|
"""
|
|
42
|
+
validation_errors: list[InitErrorDetails] = []
|
|
43
|
+
|
|
38
44
|
intervals = self.intervals or ()
|
|
39
45
|
|
|
40
46
|
if self.interval_period is None:
|
|
@@ -42,8 +48,16 @@ def _continuous_or_seperated(self: Event) -> Event:
|
|
|
42
48
|
# Ensure that all intervals have the interval_period defined, to comply with the GAC specification.
|
|
43
49
|
undefined_intervals_period = [i for i in intervals if i.interval_period is None]
|
|
44
50
|
if undefined_intervals_period:
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
validation_errors.append(
|
|
52
|
+
InitErrorDetails(
|
|
53
|
+
type=PydanticCustomError(
|
|
54
|
+
"value_error",
|
|
55
|
+
"Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.",
|
|
56
|
+
),
|
|
57
|
+
loc=("intervals",),
|
|
58
|
+
input=self.intervals,
|
|
59
|
+
ctx={},
|
|
60
|
+
)
|
|
47
61
|
)
|
|
48
62
|
else:
|
|
49
63
|
# interval period set at top level of the event.
|
|
@@ -52,14 +66,22 @@ def _continuous_or_seperated(self: Event) -> Event:
|
|
|
52
66
|
i for i in intervals if i.interval_period is not None
|
|
53
67
|
]
|
|
54
68
|
if duplicate_interval_period:
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
validation_errors.append(
|
|
70
|
+
InitErrorDetails(
|
|
71
|
+
type=PydanticCustomError(
|
|
72
|
+
"value_error",
|
|
73
|
+
"Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.",
|
|
74
|
+
),
|
|
75
|
+
loc=("intervals",),
|
|
76
|
+
input=self.intervals,
|
|
77
|
+
ctx={},
|
|
78
|
+
)
|
|
57
79
|
)
|
|
58
80
|
|
|
59
|
-
return self
|
|
81
|
+
return self, validation_errors
|
|
60
82
|
|
|
61
83
|
|
|
62
|
-
def _targets_compliant(self: Event) -> Event:
|
|
84
|
+
def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
63
85
|
"""Enforces that the targets of the event are compliant with GAC.
|
|
64
86
|
|
|
65
87
|
GAC enforces the following constraints for targets:
|
|
@@ -69,6 +91,7 @@ def _targets_compliant(self: Event) -> Event:
|
|
|
69
91
|
- The event must contain a VEN_NAME target.
|
|
70
92
|
- The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).
|
|
71
93
|
"""
|
|
94
|
+
validation_errors: list[InitErrorDetails] = []
|
|
72
95
|
targets = self.targets or ()
|
|
73
96
|
|
|
74
97
|
power_service_locations = [
|
|
@@ -77,42 +100,124 @@ def _targets_compliant(self: Event) -> Event:
|
|
|
77
100
|
ven_names = [t for t in targets if t.type == "VEN_NAME"]
|
|
78
101
|
|
|
79
102
|
if not power_service_locations:
|
|
80
|
-
|
|
103
|
+
validation_errors.append(
|
|
104
|
+
InitErrorDetails(
|
|
105
|
+
type=PydanticCustomError(
|
|
106
|
+
"value_error",
|
|
107
|
+
"The event must contain a POWER_SERVICE_LOCATIONS target.",
|
|
108
|
+
),
|
|
109
|
+
loc=("targets",),
|
|
110
|
+
input=self.targets,
|
|
111
|
+
ctx={},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
81
114
|
|
|
82
115
|
if not ven_names:
|
|
83
|
-
|
|
116
|
+
validation_errors.append(
|
|
117
|
+
InitErrorDetails(
|
|
118
|
+
type=PydanticCustomError(
|
|
119
|
+
"value_error",
|
|
120
|
+
"The event must contain a VEN_NAME target.",
|
|
121
|
+
),
|
|
122
|
+
loc=("targets",),
|
|
123
|
+
input=self.targets,
|
|
124
|
+
ctx={},
|
|
125
|
+
)
|
|
126
|
+
)
|
|
84
127
|
|
|
85
128
|
if len(power_service_locations) > 1:
|
|
86
|
-
|
|
87
|
-
|
|
129
|
+
validation_errors.append(
|
|
130
|
+
InitErrorDetails(
|
|
131
|
+
type=PydanticCustomError(
|
|
132
|
+
"value_error",
|
|
133
|
+
"The event must contain exactly one POWER_SERVICE_LOCATIONS target.",
|
|
134
|
+
),
|
|
135
|
+
loc=("targets",),
|
|
136
|
+
input=self.targets,
|
|
137
|
+
ctx={},
|
|
138
|
+
)
|
|
88
139
|
)
|
|
89
140
|
|
|
90
141
|
if len(ven_names) > 1:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
validation_errors.append(
|
|
143
|
+
InitErrorDetails(
|
|
144
|
+
type=PydanticCustomError(
|
|
145
|
+
"value_error",
|
|
146
|
+
"The event must contain only one VEN_NAME target.",
|
|
147
|
+
),
|
|
148
|
+
loc=("targets",),
|
|
149
|
+
input=self.targets,
|
|
150
|
+
ctx={},
|
|
151
|
+
)
|
|
152
|
+
)
|
|
95
153
|
|
|
96
|
-
if
|
|
97
|
-
|
|
154
|
+
if (
|
|
155
|
+
power_service_locations
|
|
156
|
+
and ven_names
|
|
157
|
+
and len(power_service_locations) == 1
|
|
158
|
+
and len(ven_names) == 1
|
|
159
|
+
):
|
|
160
|
+
power_service_location = power_service_locations[0]
|
|
161
|
+
ven_name = ven_names[0]
|
|
162
|
+
|
|
163
|
+
if len(power_service_location.values) == 0:
|
|
164
|
+
validation_errors.append(
|
|
165
|
+
InitErrorDetails(
|
|
166
|
+
type=PydanticCustomError(
|
|
167
|
+
"value_error",
|
|
168
|
+
"The POWER_SERVICE_LOCATIONS target value cannot be empty.",
|
|
169
|
+
),
|
|
170
|
+
loc=("targets",),
|
|
171
|
+
input=self.targets,
|
|
172
|
+
ctx={},
|
|
173
|
+
)
|
|
174
|
+
)
|
|
98
175
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
if not all(re.fullmatch(r"\d{18}", v) for v in power_service_location.values):
|
|
177
|
+
validation_errors.append(
|
|
178
|
+
InitErrorDetails(
|
|
179
|
+
type=PydanticCustomError(
|
|
180
|
+
"value_error",
|
|
181
|
+
"The POWER_SERVICE_LOCATIONS target value must be a list of 'EAN18' values.",
|
|
182
|
+
),
|
|
183
|
+
loc=("targets",),
|
|
184
|
+
input=self.targets,
|
|
185
|
+
ctx={},
|
|
186
|
+
)
|
|
187
|
+
)
|
|
103
188
|
|
|
104
|
-
|
|
105
|
-
|
|
189
|
+
if len(ven_name.values) == 0:
|
|
190
|
+
validation_errors.append(
|
|
191
|
+
InitErrorDetails(
|
|
192
|
+
type=PydanticCustomError(
|
|
193
|
+
"value_error",
|
|
194
|
+
"The VEN_NAME target value cannot be empty.",
|
|
195
|
+
),
|
|
196
|
+
loc=("targets",),
|
|
197
|
+
input=self.targets,
|
|
198
|
+
ctx={},
|
|
199
|
+
)
|
|
200
|
+
)
|
|
106
201
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
202
|
+
if not all(1 <= len(v) <= 128 for v in ven_name.values):
|
|
203
|
+
validation_errors.append(
|
|
204
|
+
InitErrorDetails(
|
|
205
|
+
type=PydanticCustomError(
|
|
206
|
+
"value_error",
|
|
207
|
+
"The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).",
|
|
208
|
+
),
|
|
209
|
+
loc=("targets",),
|
|
210
|
+
input=self.targets,
|
|
211
|
+
ctx={},
|
|
212
|
+
)
|
|
213
|
+
)
|
|
111
214
|
|
|
112
|
-
return self
|
|
215
|
+
return self, validation_errors
|
|
113
216
|
|
|
114
217
|
|
|
115
|
-
def _payload_descriptor_gac_compliant(
|
|
218
|
+
def _payload_descriptor_gac_compliant(
|
|
219
|
+
self: Event,
|
|
220
|
+
) -> Tuple[Event, list[InitErrorDetails]]:
|
|
116
221
|
"""Enforces that the payload descriptor is GAC compliant.
|
|
117
222
|
|
|
118
223
|
GAC enforces the following constraints for payload descriptors:
|
|
@@ -121,28 +226,67 @@ def _payload_descriptor_gac_compliant(self: Event) -> Event:
|
|
|
121
226
|
- The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'
|
|
122
227
|
- The payload descriptor must have a units of 'KW' (case sensitive).
|
|
123
228
|
"""
|
|
124
|
-
|
|
125
|
-
raise ValueError("The event must have a payload descriptor.")
|
|
229
|
+
validation_errors: list[InitErrorDetails] = []
|
|
126
230
|
|
|
127
|
-
if
|
|
128
|
-
|
|
231
|
+
if self.payload_descriptor is None:
|
|
232
|
+
validation_errors.append(
|
|
233
|
+
InitErrorDetails(
|
|
234
|
+
type=PydanticCustomError(
|
|
235
|
+
"value_error",
|
|
236
|
+
"The event must have a payload descriptor.",
|
|
237
|
+
),
|
|
238
|
+
loc=("payload_descriptor",),
|
|
239
|
+
input=self.payload_descriptor,
|
|
240
|
+
ctx={},
|
|
241
|
+
)
|
|
242
|
+
)
|
|
129
243
|
|
|
130
|
-
|
|
244
|
+
if self.payload_descriptor is not None:
|
|
245
|
+
if len(self.payload_descriptor) != 1:
|
|
246
|
+
validation_errors.append(
|
|
247
|
+
InitErrorDetails(
|
|
248
|
+
type=PydanticCustomError(
|
|
249
|
+
"value_error",
|
|
250
|
+
"The event must have exactly one payload descriptor.",
|
|
251
|
+
),
|
|
252
|
+
loc=("payload_descriptor",),
|
|
253
|
+
input=self.payload_descriptor,
|
|
254
|
+
ctx={},
|
|
255
|
+
)
|
|
256
|
+
)
|
|
131
257
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
258
|
+
payload_descriptor = self.payload_descriptor[0]
|
|
259
|
+
|
|
260
|
+
if payload_descriptor.payload_type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
261
|
+
validation_errors.append(
|
|
262
|
+
InitErrorDetails(
|
|
263
|
+
type=PydanticCustomError(
|
|
264
|
+
"value_error",
|
|
265
|
+
"The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'.",
|
|
266
|
+
),
|
|
267
|
+
loc=("payload_descriptor",),
|
|
268
|
+
input=self.payload_descriptor,
|
|
269
|
+
ctx={},
|
|
270
|
+
)
|
|
271
|
+
)
|
|
136
272
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
273
|
+
if payload_descriptor.units != "KW":
|
|
274
|
+
validation_errors.append(
|
|
275
|
+
InitErrorDetails(
|
|
276
|
+
type=PydanticCustomError(
|
|
277
|
+
"value_error",
|
|
278
|
+
"The payload descriptor must have a units of 'KW' (case sensitive).",
|
|
279
|
+
),
|
|
280
|
+
loc=("payload_descriptor",),
|
|
281
|
+
input=self.payload_descriptor,
|
|
282
|
+
ctx={},
|
|
283
|
+
)
|
|
284
|
+
)
|
|
141
285
|
|
|
142
|
-
return self
|
|
286
|
+
return self, validation_errors
|
|
143
287
|
|
|
144
288
|
|
|
145
|
-
def _event_interval_gac_compliant(self: Event) -> Event:
|
|
289
|
+
def _event_interval_gac_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
146
290
|
"""Enforces that the event interval is GAC compliant.
|
|
147
291
|
|
|
148
292
|
GAC enforces the following constraints for event intervals:
|
|
@@ -151,29 +295,77 @@ def _event_interval_gac_compliant(self: Event) -> Event:
|
|
|
151
295
|
- The event interval must have exactly one payload.
|
|
152
296
|
- The payload of the event interval must have a type of 'IMPORT_CAPACITY_LIMIT'
|
|
153
297
|
"""
|
|
298
|
+
validation_errors: list[InitErrorDetails] = []
|
|
299
|
+
|
|
154
300
|
if not self.intervals:
|
|
155
|
-
|
|
301
|
+
validation_errors.append(
|
|
302
|
+
InitErrorDetails(
|
|
303
|
+
type=PydanticCustomError(
|
|
304
|
+
"value_error",
|
|
305
|
+
"The event must have at least one interval.",
|
|
306
|
+
),
|
|
307
|
+
loc=("intervals",),
|
|
308
|
+
input=self.intervals,
|
|
309
|
+
ctx={},
|
|
310
|
+
)
|
|
311
|
+
)
|
|
156
312
|
|
|
157
313
|
if not all(curr.id > prev.id for prev, curr in pairwise(self.intervals)):
|
|
158
|
-
|
|
159
|
-
|
|
314
|
+
validation_errors.append(
|
|
315
|
+
InitErrorDetails(
|
|
316
|
+
type=PydanticCustomError(
|
|
317
|
+
"value_error",
|
|
318
|
+
"The event interval must have an id value that is strictly increasing.",
|
|
319
|
+
),
|
|
320
|
+
loc=("intervals",),
|
|
321
|
+
input=self.intervals,
|
|
322
|
+
ctx={},
|
|
323
|
+
)
|
|
160
324
|
)
|
|
161
325
|
|
|
162
326
|
for interval in self.intervals:
|
|
163
327
|
if interval.payloads is None:
|
|
164
|
-
|
|
328
|
+
validation_errors.append(
|
|
329
|
+
InitErrorDetails(
|
|
330
|
+
type=PydanticCustomError(
|
|
331
|
+
"value_error",
|
|
332
|
+
"The event interval must have a payload.",
|
|
333
|
+
),
|
|
334
|
+
loc=("intervals",),
|
|
335
|
+
input=self.intervals,
|
|
336
|
+
ctx={},
|
|
337
|
+
)
|
|
338
|
+
)
|
|
165
339
|
|
|
166
340
|
if len(interval.payloads) != 1:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
341
|
+
validation_errors.append(
|
|
342
|
+
InitErrorDetails(
|
|
343
|
+
type=PydanticCustomError(
|
|
344
|
+
"value_error",
|
|
345
|
+
"The event interval must have exactly one payload.",
|
|
346
|
+
),
|
|
347
|
+
loc=("intervals",),
|
|
348
|
+
input=self.intervals,
|
|
349
|
+
ctx={},
|
|
350
|
+
)
|
|
174
351
|
)
|
|
352
|
+
else:
|
|
353
|
+
payload = interval.payloads[0]
|
|
354
|
+
|
|
355
|
+
if payload.type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
356
|
+
validation_errors.append(
|
|
357
|
+
InitErrorDetails(
|
|
358
|
+
type=PydanticCustomError(
|
|
359
|
+
"value_error",
|
|
360
|
+
"The event interval payload must have a payload type of 'IMPORT_CAPACITY_LIMIT'.",
|
|
361
|
+
),
|
|
362
|
+
loc=("intervals",),
|
|
363
|
+
input=self.intervals,
|
|
364
|
+
ctx={},
|
|
365
|
+
)
|
|
366
|
+
)
|
|
175
367
|
|
|
176
|
-
return self
|
|
368
|
+
return self, validation_errors
|
|
177
369
|
|
|
178
370
|
|
|
179
371
|
@ValidatorRegistry.register(Event, ValidatorModel())
|
|
@@ -185,16 +377,40 @@ def event_gac_compliant(self: Event) -> Event:
|
|
|
185
377
|
- The event must not have a priority set.
|
|
186
378
|
- The event must have either a continuous or seperated interval definition.
|
|
187
379
|
"""
|
|
380
|
+
validation_errors: list[InitErrorDetails] = []
|
|
381
|
+
|
|
188
382
|
if self.priority is not None:
|
|
189
|
-
|
|
190
|
-
|
|
383
|
+
validation_errors.append(
|
|
384
|
+
InitErrorDetails(
|
|
385
|
+
type=PydanticCustomError(
|
|
386
|
+
"value_error",
|
|
387
|
+
"The event must not have a priority set for GAC 2.0 compliance",
|
|
388
|
+
),
|
|
389
|
+
loc=("priority",),
|
|
390
|
+
input=self.priority,
|
|
391
|
+
ctx={},
|
|
392
|
+
)
|
|
191
393
|
)
|
|
192
394
|
|
|
193
|
-
interval_periods_validated = _continuous_or_seperated(self)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
395
|
+
interval_periods_validated, errors = _continuous_or_seperated(self)
|
|
396
|
+
validation_errors.extend(errors)
|
|
397
|
+
|
|
398
|
+
targets_validated, errors = _targets_compliant(interval_periods_validated)
|
|
399
|
+
validation_errors.extend(errors)
|
|
400
|
+
|
|
401
|
+
payload_descriptor_validated, errors = _payload_descriptor_gac_compliant(
|
|
402
|
+
targets_validated
|
|
403
|
+
)
|
|
404
|
+
validation_errors.extend(errors)
|
|
405
|
+
|
|
406
|
+
event_interval_validated, errors = _event_interval_gac_compliant(
|
|
197
407
|
payload_descriptor_validated
|
|
198
408
|
)
|
|
409
|
+
validation_errors.extend(errors)
|
|
410
|
+
|
|
411
|
+
if validation_errors:
|
|
412
|
+
raise ValidationError.from_exception_data(
|
|
413
|
+
title=self.__class__.__name__, line_errors=validation_errors
|
|
414
|
+
)
|
|
199
415
|
|
|
200
416
|
return event_interval_validated
|
|
@@ -5,6 +5,9 @@ from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorMo
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
@ValidatorRegistry.register(Program, ValidatorModel())
|
|
10
13
|
def program_gac_compliant(self: Program) -> Program:
|
|
@@ -18,22 +21,81 @@ def program_gac_compliant(self: Program) -> Program:
|
|
|
18
21
|
- The program MUST have bindingEvents set to True.
|
|
19
22
|
are allowed there.
|
|
20
23
|
"""
|
|
24
|
+
validation_errors: list[InitErrorDetails] = []
|
|
25
|
+
|
|
21
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-]+)*))?$"
|
|
22
27
|
|
|
23
28
|
if self.retailer_name is None:
|
|
24
|
-
|
|
29
|
+
validation_errors.append(
|
|
30
|
+
InitErrorDetails(
|
|
31
|
+
type=PydanticCustomError(
|
|
32
|
+
"value_error",
|
|
33
|
+
"The program must have a retailer name.",
|
|
34
|
+
),
|
|
35
|
+
loc=("retailer_name",),
|
|
36
|
+
input=self.retailer_name,
|
|
37
|
+
ctx={},
|
|
38
|
+
)
|
|
39
|
+
)
|
|
25
40
|
|
|
26
|
-
if
|
|
27
|
-
|
|
41
|
+
if self.retailer_name is not None and (
|
|
42
|
+
len(self.retailer_name) < 2 or len(self.retailer_name) > 128
|
|
43
|
+
):
|
|
44
|
+
validation_errors.append(
|
|
45
|
+
InitErrorDetails(
|
|
46
|
+
type=PydanticCustomError(
|
|
47
|
+
"value_error",
|
|
48
|
+
"The retailer name must be between 2 and 128 characters long.",
|
|
49
|
+
),
|
|
50
|
+
loc=("retailer_name",),
|
|
51
|
+
input=self.retailer_name,
|
|
52
|
+
ctx={},
|
|
53
|
+
)
|
|
54
|
+
)
|
|
28
55
|
|
|
29
56
|
if self.program_type is None:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
validation_errors.append(
|
|
58
|
+
InitErrorDetails(
|
|
59
|
+
type=PydanticCustomError(
|
|
60
|
+
"value_error",
|
|
61
|
+
"The program must have a program type.",
|
|
62
|
+
),
|
|
63
|
+
loc=("program_type",),
|
|
64
|
+
input=self.program_type,
|
|
65
|
+
ctx={},
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
if self.program_type is not None and not re.fullmatch(
|
|
69
|
+
program_type_regex, self.program_type
|
|
70
|
+
):
|
|
71
|
+
validation_errors.append(
|
|
72
|
+
InitErrorDetails(
|
|
73
|
+
type=PydanticCustomError(
|
|
74
|
+
"value_error",
|
|
75
|
+
"The program type must follow the format DSO_CPO_INTERFACE-x.x.x.",
|
|
76
|
+
),
|
|
77
|
+
loc=("program_type",),
|
|
78
|
+
input=self.program_type,
|
|
79
|
+
ctx={},
|
|
80
|
+
)
|
|
34
81
|
)
|
|
35
82
|
|
|
36
83
|
if self.binding_events is False:
|
|
37
|
-
|
|
84
|
+
validation_errors.append(
|
|
85
|
+
InitErrorDetails(
|
|
86
|
+
type=PydanticCustomError(
|
|
87
|
+
"value_error",
|
|
88
|
+
"The program must have bindingEvents set to True.",
|
|
89
|
+
),
|
|
90
|
+
loc=("binding_events",),
|
|
91
|
+
input=self.binding_events,
|
|
92
|
+
ctx={},
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if validation_errors:
|
|
97
|
+
raise ValidationError.from_exception_data(
|
|
98
|
+
title=self.__class__.__name__, line_errors=validation_errors
|
|
99
|
+
)
|
|
38
100
|
|
|
39
101
|
return self
|
|
@@ -3,6 +3,9 @@ from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorMo
|
|
|
3
3
|
from openadr3_client.models.ven.ven import Ven
|
|
4
4
|
import pycountry
|
|
5
5
|
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
@ValidatorRegistry.register(Ven, ValidatorModel())
|
|
8
11
|
def ven_gac_compliant(self: Ven) -> Ven:
|
|
@@ -12,16 +15,41 @@ def ven_gac_compliant(self: Ven) -> Ven:
|
|
|
12
15
|
- The ven must have a ven name
|
|
13
16
|
- The ven name must be an eMI3 identifier.
|
|
14
17
|
"""
|
|
18
|
+
validation_errors: list[InitErrorDetails] = []
|
|
19
|
+
|
|
15
20
|
emi3_identifier_regex = r"^[A-Z]{2}-?[A-Z0-9]{3}$"
|
|
16
21
|
|
|
17
22
|
if not re.fullmatch(emi3_identifier_regex, self.ven_name):
|
|
18
|
-
|
|
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
|
+
)
|
|
19
34
|
|
|
20
35
|
alpha_2_country = pycountry.countries.get(alpha_2=self.ven_name[:2])
|
|
21
36
|
|
|
22
37
|
if alpha_2_country is None:
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
53
|
)
|
|
26
54
|
|
|
27
55
|
return self
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: openadr3-client-gac-compliance
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Nick van der Burgt
|
|
6
6
|
Author-email: nick.van.der.burgt@elaad.nl
|
|
@@ -8,7 +8,7 @@ Requires-Python: >=3.12, <4
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
-
Requires-Dist: openadr3-client (>=0.0.1,<0.0
|
|
11
|
+
Requires-Dist: openadr3-client (>=0.0.1,<1.0.0)
|
|
12
12
|
Requires-Dist: pycountry (>=24.6.1,<25.0.0)
|
|
13
13
|
Requires-Dist: pydantic (>=2.11.2,<3.0.0)
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
openadr3_client_gac_compliance/__init__.py,sha256=dzf9YdOlssEb9GSIoThaMzEJ956G-hpLi7HIXRC3TR8,334
|
|
2
2
|
openadr3_client_gac_compliance/config.py,sha256=X_KEl99bUm05rH0IOKLyeR4Zn2utdC8U3vLkdG8MYXU,675
|
|
3
3
|
openadr3_client_gac_compliance/gac20/__init__.py,sha256=YPjRHMl-uR6ZwrDGjY_EboScHe_VhTrovso_zDOSd6o,220
|
|
4
|
-
openadr3_client_gac_compliance/gac20/event_gac_compliant.py,sha256=
|
|
5
|
-
openadr3_client_gac_compliance/gac20/program_gac_compliant.py,sha256=
|
|
6
|
-
openadr3_client_gac_compliance/gac20/ven_gac_compliant.py,sha256=
|
|
4
|
+
openadr3_client_gac_compliance/gac20/event_gac_compliant.py,sha256=dZIVRRgpRajkceu8IDLNaz9b0lNmlFfWjdlAvlxymTo,15060
|
|
5
|
+
openadr3_client_gac_compliance/gac20/program_gac_compliant.py,sha256=i7l9oboA31_HMHZ-b8GbVTXGfeNCL3kF-o5e7--riCg,3540
|
|
6
|
+
openadr3_client_gac_compliance/gac20/ven_gac_compliant.py,sha256=57MH73JorFSNyeB6Tep4XX_WMhtKls2DXoSoBaMhob8,1751
|
|
7
7
|
openadr3_client_gac_compliance/gac21/__init__.py,sha256=GTu1EyBj2T72zT3MKPettcvP-DNCJ0p6oAMj3Z-08N0,61
|
|
8
|
-
openadr3_client_gac_compliance-1.1.
|
|
9
|
-
openadr3_client_gac_compliance-1.1.
|
|
10
|
-
openadr3_client_gac_compliance-1.1.
|
|
11
|
-
openadr3_client_gac_compliance-1.1.
|
|
8
|
+
openadr3_client_gac_compliance-1.2.1.dist-info/LICENSE.md,sha256=NNNxKzhSK6afX-UaN8WZIVtMi5Tu8dVr6q3ciTSnH-g,10250
|
|
9
|
+
openadr3_client_gac_compliance-1.2.1.dist-info/METADATA,sha256=n5GMDEvlXrEreakDfUWYiVOTZF7mgT1W8JzzhhsGK6g,1096
|
|
10
|
+
openadr3_client_gac_compliance-1.2.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
11
|
+
openadr3_client_gac_compliance-1.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|