Improve DeliveryArea validation and error handling#241
Conversation
9e9a1c6 to
8e6ce9e
Compare
8e6ce9e to
8a24201
Compare
63d7fc3 to
26372c3
Compare
This class will be used as the base for both valid and invalid delivery area classes. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
`DeliveryArea`, the existing type, is retroactively made a subclass of `BaseDeliveryArea`. Field shape and construction API are unchanged (`code: str | None`, `code_type: EnergyMarketCodeType | int`) for backwards compatibility, but a `__post_init__` is added with invariant checks: a well-formed delivery area must have a non-empty `code` and a specified `code_type`. Constructing one without that doesn't pass those invariants will now emit a `DeprecationWarning` (and it will raise `ValueError` in v0.5.0). Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
A `__post_init__` is added with invariant checks: a well-formed delivery area must have a non-empty `code`. Constructing one that doesn't pass the invariant will now emit a `DeprecationWarning` (and it will raise `ValueError` in v0.5.0). Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
a9e633b to
edf04b2
Compare
|
Ready for review again. @cwasicki Please confirm this makes sense to you. I was wondering if we should bake the "default" delivery area code directly here if it is unspecified, so we can already make it logically as if it was required, instead of having downstream users having to care about this transitional phase where a missing delivery area code is OK (and some default should be assumed when this happens). What do you think? |
Introduce `delivery_area_from_proto2`, sibling of the existing `delivery_area_from_proto`, returning `DeliveryArea | InvalidDeliveryArea` instead of a plain `DeliveryArea`. The new converter enforces at the boundary the invariant that `DeliveryArea.__post_init__` now warns about `code` must be non-empty. A wire message that fails either check becomes an `InvalidDeliveryArea` carrying the raw data verbatim, so callers can inspect or report it via `InvalidDeliveryAreaError`. Unknown non-zero `int` `code_type` values are still treated as valid to preserve forward compatibility with new protobuf enum values. For practical reasons, a `code_type` of `0` (which normally should invalidate a delivery area, is considered valid, as it is not always set but we can fall back to a well-known default currently. Because the valid path constructs a `DeliveryArea` only when the invariants already hold, `delivery_area_from_proto2` never triggers the inner `DeprecationWarning` — and, unlike the old converter, it doesn't log any "found issues" warning either: the return type itself now signals whether the wire data was well-formed. The existing `delivery_area_from_proto` remains unchanged and will be marked `@deprecated` in a follow-up commit. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Loosen `Microgrid.delivery_area` from `DeliveryArea | None` to `DeliveryArea | InvalidDeliveryArea | None` to reflect the three possible wire states: absent, present-but-malformed, and well-formed. `microgrid_from_proto` is switched to `delivery_area_from_proto2`, so proto-loaded microgrids already carry the right variant on the union. Add the semantic accessors `Microgrid.get_delivery_area() -> DeliveryArea` so callers that need a valid delivery area don't have to check for either failure mode by hand: * `None` -> `MissingFieldError` (the field wasn't set on the wire). * `InvalidDeliveryArea` -> `InvalidDeliveryAreaError`, with the offending instance available on `.delivery_area` for inspection. * `DeliveryArea` -> returned unchanged. Also add `Microgrid.get_delivery_area_or_none() -> DeliveryArea | None` for cases where users want to handle the `None` case gracefully, only raising on an invalid delivery area. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
edf04b2 to
5a28455
Compare
Mark `delivery_area_from_proto` as `@deprecated`, pointing callers at `delivery_area_from_proto2`, which returns `DeliveryArea | InvalidDeliveryArea` and surfaces malformed wire data at the type level rather than as an ad-hoc log warning. The internal `DeliveryArea(...)` construction inside `delivery_area_from_proto` was already wrapped in `warnings.catch_warnings()` when `DeliveryArea.__post_init__` gained its soft invariant check, so the two suppressions compose correctly: callers of the deprecated entry point see exactly one `DeprecationWarning`, identifying `delivery_area_from_proto2` as the replacement. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
I guess so, whatever you are using if you get no delivery area code. |
`src/.../grid/_delivery_area.py` exposes five public symbols
(`EnergyMarketCodeType`, `BaseDeliveryArea`, `DeliveryArea`,
`InvalidDeliveryArea`, `InvalidDeliveryAreaError`), so the test file
grew quite big.
This commit splits the test file following the pattern:
tests/grid/test_delivery_area.py
-> tests/grid/_delivery_area/test_<thing>.py
Test bodies and assertions are unchanged; only their location and their
names change. Test function names are stripped of the redundant
module-name prefix per the same guideline (e.g.
`test_invalid_delivery_area_error_default_message` -> `test_default_message`
inside `test_invalid_delivery_area_error.py`).
Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
5a28455 to
9048b55
Compare
For the foreseeable future this would indeed be convenient. But not sure if we should really silently default to it. |
I feel the same, but if we stay on the practical side, actually having this behaviour in a common library means updating should be much easier when we make sure not having a delivery code type is really an error, as we only need to update one place instead of tracking all places where this ad-hoc default was added. But I guess you are the main user of this, so I leave the decision to you. |
Drop the ad-hoc placeholders (the `<NO CODE>` sentinel and the `❌`
fallbacks). `InvalidDeliveryArea` to now wraps the fields that violate
invariants in an explicit `<invalid:VALUE>` marker.
Concretely, `str(area)` now renders as:
For `DeliveryArea` (invariants trusted, no annotation):
* `"10Y1001A1001A450[EUROPE_EIC]"` well-formed
* `"DE[type=999]"` unknown `int` `code_type`
* `"DE[type=0]"` raw `int(0)` `code_type`
* `"DE[UNSPECIFIED]"` deprecated `UNSPECIFIED` member
* `"None[EUROPE_EIC]"` (deprecated) `None` `code`
* `"[EUROPE_EIC]"` (deprecated) empty `code`
For `InvalidDeliveryArea` (invariant violations flagged):
* `"<invalid:''>[EUROPE_EIC]"` empty `code`
* `"<invalid:None>[EUROPE_EIC]"` (deprecated) `None` `code`
* `"DE[type=<invalid:0>]"` `code_type` is raw `int(0)` or
deprecated `UNSPECIFIED` member
* `"DE[type=999]"` unknown `int` `code_type`
* `"10Y1001A1001A450[EUROPE_EIC]"` otherwise-well-formed data
The two `__str__` methods are separate on purpose: `DeliveryArea` is
the "trust the values" contract, while `InvalidDeliveryArea` is
explicitly for malformed wire data — so only the latter should call
out what actually violated an invariant.
Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Extend the `DeliveryArea.__post_init__` invariant so an unspecified `code_type` (the raw `int` `0` or the deprecated `UNSPECIFIED` member) is treated the same as a missing `code`: a `DeprecationWarning` now, a hard `ValueError` in v0.5.0. The class docstring is updated to reflect the extended invariant. To let callers opt into the upcoming behavior right away, add a private `_raise_on_invalid: InitVar[bool] = False` init-only parameter that switches the checks from warning to raising. The `_`-prefix signals that this is a transitional lever, not part of the stable public surface: it will be removed together with the warning path once the invariants become hard errors. Adapt `delivery_area_from_proto2` to the new invariant. It now calls `DeliveryArea(..., _raise_on_invalid=True)` and falls back to `InvalidDeliveryArea` when the construction raises, instead of only checking `message.code` by hand — this keeps the validation logic in a single place. To preserve the current shape of well-formed conversions while the service still omits `code_type` in many messages, the converter grows a `replace_unspecified_code_type_with` keyword argument (default `EnergyMarketCodeType.EUROPE_EIC`) that fills in the missing value before invariant checking. Once the service starts populating `code_type` consistently, the argument (and this accommodation) can be removed and unspecified values will simply produce an `InvalidDeliveryArea`. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
|
Update with:
@cwasicki can you have a look and approve if it looks good to you? |
cwasicki
left a comment
There was a problem hiding this comment.
LGTM, I would do the default to EIC differently but not a blocker.
| def delivery_area_from_proto2( | ||
| message: delivery_area_pb2.DeliveryArea, | ||
| *, | ||
| replace_unspecified_code_type_with: EnergyMarketCodeType = EnergyMarketCodeType.EUROPE_EIC, |
There was a problem hiding this comment.
Users have to update their code anyway to use this new function. Maybe we use required boolean instead to enforce an explicit decision for the fallback, i.e.
def delivery_area_from_proto2(
message: delivery_area_pb2.DeliveryArea,
replace_unspecified_code_type_with_eic: bool,
)
There was a problem hiding this comment.
If everybody will use the european code type, is it really useful to make the argument required? If we make it required the code needs to be updated twice. Now and then again when all delivery area messages provide a proper code type and we remove the replace_unspecified_code_type_with_eic. IMHO it looks like it only adds more work and doesn't buy us anything.
There was a problem hiding this comment.
then again when all delivery area messages provide a proper code type and we remove the replace_unspecified_code_type_with_eic
Why would you want to remove it in future? Are you planning to remove the existing argument with its default to EIC too?
This new keyword-only parameter was added to `delivery_area_from_proto2()` because the system was not setting `code_type` properly always. This has been fixed so this hack is not needed anymore. Refs frequenz-floss#241.
Adds a new delivery-area class hierarchy:
frequenz.client.common.grid.BaseDeliveryArea— abstract common supertype of the two concrete leaves; not directly instantiable.frequenz.client.common.grid.DeliveryArea— well-formed delivery area (retroactively made a subclass ofBaseDeliveryArea.frequenz.client.common.grid.InvalidDeliveryArea— malformed wire data; same fields asDeliveryAreawith no invariants enforced, so callers can inspect whatever the server actually sent.Also adds
frequenz.client.common.grid.proto.v1alpha8.delivery_area_from_proto2returningDeliveryArea | InvalidDeliveryArea. This is the replacement fordelivery_area_from_proto(which is deprecated now) and is whatmicrogrid_from_protouses internally.This deprecates construction of
frequenz.client.common.grid.DeliveryAreawith invalid data. A well-formedDeliveryAreahas a non-emptycodeand a non-UNSPECIFIEDcode_type. Constructing one with invalid data currently emits aDeprecationWarning; a future release will replace the warning with a hardValueError. Preferdelivery_area_from_proto2to load delivery areas from the wire — malformed messages becomeInvalidDeliveryAreainstances instead.Part of #239 and #251.