Skip to content

Improve DeliveryArea validation and error handling#241

Merged
llucax merged 13 commits into
frequenz-floss:v0.x.xfrom
llucax:name-delivery-area
Jul 16, 2026
Merged

Improve DeliveryArea validation and error handling#241
llucax merged 13 commits into
frequenz-floss:v0.x.xfrom
llucax:name-delivery-area

Conversation

@llucax

@llucax llucax commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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 of BaseDeliveryArea.
  • frequenz.client.common.grid.InvalidDeliveryArea — malformed wire data; same fields as DeliveryArea with no invariants enforced, so callers can inspect whatever the server actually sent.

Also adds frequenz.client.common.grid.proto.v1alpha8.delivery_area_from_proto2 returning DeliveryArea | InvalidDeliveryArea. This is the replacement for delivery_area_from_proto (which is deprecated now) and is what microgrid_from_proto uses internally.

This deprecates construction of frequenz.client.common.grid.DeliveryArea with invalid data. A well-formed DeliveryArea has a non-empty code and a non-UNSPECIFIED code_type. Constructing one with invalid data currently emits a DeprecationWarning; a future release will replace the warning with a hard ValueError. Prefer delivery_area_from_proto2 to load delivery areas from the wire — malformed messages become InvalidDeliveryArea instances instead.

Part of #239 and #251.

@github-actions github-actions Bot added part:docs Affects the documentation part:tests Affects the unit, integration and performance (benchmarks) tests part:grid Affects the grid protobuf definitions part:microgrid Affects the microgrid protobuf definitions labels Jul 6, 2026
@llucax llucax mentioned this pull request Jul 6, 2026
5 tasks
@llucax
llucax force-pushed the name-delivery-area branch 2 times, most recently from 9e9a1c6 to 8e6ce9e Compare July 7, 2026 12:36
@github-actions github-actions Bot added part:tooling Affects the development tooling (CI, deployment, dependency management, etc.) part:metrics Affects the metrics protobuf definitions labels Jul 7, 2026
@llucax
llucax force-pushed the name-delivery-area branch from 8e6ce9e to 8a24201 Compare July 7, 2026 13:36
@llucax llucax self-assigned this Jul 7, 2026
@llucax
llucax marked this pull request as ready for review July 7, 2026 13:39
@llucax
llucax requested a review from a team as a code owner July 7, 2026 13:39
@llucax
llucax requested review from cwasicki, eduardiazf, ela-kotulska-frequenz, sandovalrr and tiyash-basu-frequenz and removed request for a team July 7, 2026 13:39
@llucax llucax added this to the v0.4.1 milestone Jul 7, 2026
@llucax
llucax marked this pull request as draft July 9, 2026 12:34
@llucax
llucax force-pushed the name-delivery-area branch 2 times, most recently from 63d7fc3 to 26372c3 Compare July 9, 2026 13:20
llucax added 3 commits July 13, 2026 10:20
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>
@llucax
llucax force-pushed the name-delivery-area branch 2 times, most recently from a9e633b to edf04b2 Compare July 13, 2026 09:03
@llucax
llucax marked this pull request as ready for review July 13, 2026 09:15
@llucax

llucax commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

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?

llucax added 2 commits July 14, 2026 13:41
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>
@llucax
llucax force-pushed the name-delivery-area branch from edf04b2 to 5a28455 Compare July 14, 2026 11:42
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>
@llucax

llucax commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Do you mean the European one?

I guess so, whatever you are using if you get no delivery area code.

llucax added 2 commits July 14, 2026 13:49
`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>
@cwasicki

Copy link
Copy Markdown
Contributor

I guess so, whatever you are using if you get no delivery area code.

For the foreseeable future this would indeed be convenient. But not sure if we should really silently default to it.

@llucax

llucax commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

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.

llucax added 2 commits July 16, 2026 10:39
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment thread src/frequenz/client/common/grid/_delivery_area.py
@llucax

llucax commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Update with:

  • Emojis removed
  • New "standard" for invalid data in __str__ (see Standardize <invalid:VALUE> across __str__ #251).
  • Replacing a UNSPECIFIED code_type with EUROPE_EIC in the new delivery_area_from_proto2() (overrideable via the keyword argument replace_unspecified_code_type_with, so if anyone needs to get the proper UNSPECIFIED they can, and in the future will allow different clients to move to the new required code_type at their own pace).

@cwasicki can you have a look and approve if it looks good to you?

@llucax
llucax enabled auto-merge July 16, 2026 09:47

@cwasicki cwasicki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@llucax
llucax added this pull request to the merge queue Jul 16, 2026
Merged via the queue into frequenz-floss:v0.x.x with commit a18fb40 Jul 16, 2026
9 checks passed
@llucax
llucax deleted the name-delivery-area branch July 16, 2026 10:13
llucax added a commit to llucax/frequenz-client-common-python that referenced this pull request Jul 17, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

part:docs Affects the documentation part:grid Affects the grid protobuf definitions part:metrics Affects the metrics protobuf definitions part:microgrid Affects the microgrid protobuf definitions part:tests Affects the unit, integration and performance (benchmarks) tests part:tooling Affects the development tooling (CI, deployment, dependency management, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants