Add user, wrapping and client developer guides - #277
Open
llucax wants to merge 25 commits into
Open
Conversation
Downstream users of the wrapper types currently have nothing but the generated API reference to learn from. The reference documents each symbol in isolation, so it never explains the cross-cutting conventions a caller has to internalize: typed IDs, safe accessors, enum-or-int fields, validity carried in the type, and the string markers that show up in logs. Start a User Guide aimed at that audience: someone who receives wrapper objects from a Frequenz API client library and needs to read them correctly, without knowing anything about protobuf. This commit adds only the landing page and the navigation entries. The index describes and links every section, so the rest of this series can be reviewed one page at a time with the overall shape already visible. The consequence is that the links to those pages, and to the Client Developer Guide, dangle until the corresponding commits land, and `mkdocs build` (which runs with `strict: true`) fails in between. That breakage is deliberate to make reviewing easier. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The ID types are the first thing a caller touches, and their behavior is easy to get wrong when reading the API reference alone: `MicrogridId(123)` and `SensorId(123)` are both "123", yet they must never compare equal or collide as dictionary keys. Document that IDs are distinct types rather than plain integers, that equality and hashing take the type into account, and that the printed form carries a kind prefix (`MID`, `CID`, `SID`, `EID`), which is what a reader actually sees in logs. Point at `frequenz.core.id.BaseId` for the shared behavior instead of restating it here. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Wrapper types expose two ways to read a field that may hold something unusable: the plain attribute, which is widened to include the raw value, and a `get_*()` accessor, which either returns a validated value or raises. The API reference documents each accessor individually, so it never states the rule that a caller should reach for the accessor first and only drop to the attribute when the raw value is actually needed. Document that rule, and the exception hierarchy that makes it usable: `UnrecognizedEnumValueError` and `UnspecifiedEnumValueError` for the two enum cases, `InvalidAttributeError` as the shared fallback for any invalid field (and a `ValueError`, so existing handlers keep working), and `ClientCommonError` as the catch-all for this library. `MetricSample.get_metric()` is used as the worked example because it covers both the success path and the unrecognized-value path, including the `attr_name` and `value` attributes a caller needs to report the problem. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Numeric fields are annotated `FloatInt`, and the annotation is easy to misread. PEP 484's numeric tower already lets an `int` satisfy a `float` annotation, so a value annotated as a float can be an `int` at runtime. The trap is `isinstance(value, float)`, which is `False` for an `int` and silently sends such values down the wrong branch. Document what `FloatInt` means, where it shows up (`MetricSample.value`, `MetricSample.as_single_value()`, and the `Bounds` endpoints), and show the `match` form that handles both number types in one branch. Also note that `bool` is an `int` subclass and therefore satisfies `FloatInt`, so applications that must reject booleans have to say so explicitly. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Enum-valued fields are typed `TheEnum | int` so that a value added by a newer server reaches an older client as a raw number instead of making the whole message fail to parse. Callers routinely mishandle this: they reach for `isinstance()`, or look for an `UNSPECIFIED` member that the wrapper enums deliberately do not have. Document the three cases a caller must cover -- a known member, the raw `0` meaning unspecified, and any other integer meaning a value this client version does not recognize -- and show both ways to handle them: `get_metric()` with the two enum exceptions, and a `match` statement where `case 0` must precede `case int()`. Also draw the line to the neighboring topic: an unrecognized integer is not invalid data, it is data this client is too old to name, so it is not what the validity-in-the-type section covers. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The library never silently drops data that violates an invariant, and it does not raise at parse time either. It keeps the raw value and encodes the problem in the type, so a caller can decide what to do. That design is only discoverable from the API reference by noticing an `Invalid*` class next to every wrapper, which explains nothing about how to consume it. Document the three shapes this takes: a whole object replaced by its invalid counterpart (`DeliveryArea | InvalidDeliveryArea`), a valid object carrying an invalid field (`Location` with `InvalidLatitude` and friends), and a dedicated subclass in a class hierarchy (`UnspecifiedBattery`, `UnrecognizedBattery`, `MismatchedCategoryElectricalComponent`). Recommend `match` with `assert_never` over `isinstance()` chains, because that is what keeps the cases exhaustive under a type checker when new subtypes are added. Restate the boundary against enum-or-int fields from the other direction: `Invalid*` means an invariant was violated, an unrecognized integer means the data is well-formed but unnamed here. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
`Bounds` and `BoundsSet` support `in`, but the semantics are not obvious from the signature: endpoints are inclusive, `None` means unbounded in that direction, an infinite endpoint is canonicalized to `None`, and a reversed or `NaN` endpoint raises rather than producing a range that can never match. A `BoundsSet` accepts any iterable of `Bounds` and tests membership against the union. Document those rules together with the empty and unbounded cases, since `bool(bounds)` and `is_bounded()` answer different questions and are easy to confuse. Also cover the read path: `MetricSample.get_bounds_set()` raises `InvalidBoundsSetError`, while `MetricSample.bounds_set` may hand back an `InvalidBoundsSet`, and both `InvalidBounds` and `InvalidBoundsSet` retain the malformed values rather than discarding them. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Most people meet these types through a log line before they ever read the API reference, and the compact `__str__` forms encode a distinction that is invisible without explanation. An `<invalid:...>` marker means an invariant was violated and the raw value was preserved. A bare raw number in a suffix such as `:category=<int>` or `:type=<int>` means the opposite: the data is well-formed, this client version simply has no name for it yet. Document both markers and point each at the section that explains how to handle it. Also state that these strings are for human inspection only -- they are not a stable format, so application logic must go through the typed fields and accessors. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The generated API reference is organized by module path, which is the wrong index for someone who knows what data they received but not what it is called here. Add a map from the kind of common API data -- grid, metrics, microgrid, electrical components, sensors, common types, streaming, pagination -- to the wrapper types that represent it, and hand off to the API reference for the per-symbol detail. This closes the User Guide, so it goes last in the guide's navigation: the preceding sections teach the conventions, and this one is the lookup table you return to afterwards. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The rules for turning a generated protobuf message into an idiomatic wrapper have so far lived in three places: a short section of `CONTRIBUTING.md`, scattered notes in the `AGENTS.md` files, and the existing code, which has to be read and imitated. Nothing states the reasoning, so each new wrapper re-derives it and drifts. Start a Wrapping Guide as the canonical, example-backed source for that design work: package layout, enum representation, data types, validity, conversion functions, deprecation, and testing. Its audience is anyone adding or changing a wrapper here, or writing an equivalent wrapper in another Frequenz client library. As with the User Guide, this commit adds only the landing page and the navigation entries, so the sections that follow can be reviewed individually against a stated table of contents. The links to those sections, and to the Client Developer Guide, dangle until later commits. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The layout of a wrapper package is load-bearing rather than cosmetic. A public type module that imports a generated protobuf module drags the bindings into every caller's dependency graph and ties the type to one protobuf API version. Keeping conversion functions in a versioned `proto/<namespace>/` subpackage is what lets a single set of public types serve several protobuf API versions: a new version gets a sibling directory instead of edits to the existing one. Document that layer split, the rule that only conversion modules may import generated bindings, and the package-initializer convention that gives callers stable import paths and avoids import cycles. Also move the field-name and docstring rules here from `CONTRIBUTING.md`, where they were an isolated list with no surrounding rationale. They belong next to the rest of the wrapper design guidance; `CONTRIBUTING.md` is updated to point here once this guide is complete. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Copying a protobuf enum into a Python enum is the default reflex and it is usually wrong. The guide's central point is to ask what the values mean to callers first, and pick one of three representations: a `bool` when the enum expresses a single higher-level fact (`Microgrid.is_active()`), a class hierarchy when the category says what the object *is* (`Battery`, `Inverter`, `EvCharger` and their subtypes), or a public Python enum when the named vocabulary itself is the concept (`Metric`). Each is documented with the implementation mechanics and, just as importantly, with when *not* to use it. The section also fixes the rule that an exported wrapper enum has no `UNSPECIFIED` member: protobuf's `0` stays the plain `int` `0`, every enum-valued field is typed `TheEnum | int`, and `case 0` must precede `case int()` when matching. Update the `enum_from_proto` docstring example to follow both rules it is supposed to illustrate. It still showed `import enum` with `@enum.unique`, while wrappers use `frequenz.core.enum` for its deprecation support, and it declared an `UNSPECIFIED = 0` member that the guide now forbids. Drop that member and add the `enum_from_proto(0, ...)` case so the example demonstrates that `0` comes back as a raw `int` like any other unrecognized value. The example is executed by Sybil, so it is also a check that the documented behavior is real. Add the protobuf inventory to `mkdocs.yml`: this is the first page to cross-reference `google.protobuf`, via `Message.WhichOneof()`. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Document the three choices that apply to every non-enum wrapper. Frozen keyword-only dataclasses, because immutability is what makes equality and hashing depend on the values, and keyword-only construction survives field changes. Plus a short `__str__` sized for log lines rather than a dump of every field. Typed identifiers derived from `BaseId` with a `str_prefix`, because a bare `int` neither tells a reader what a function wants nor stops two unrelated IDs with the same number from comparing equal or colliding as dictionary keys. `FloatInt` for numbers, because PEP 484's numeric tower means a `float` annotation can hold an `int` at runtime, and a plain `float` annotation hides that from anyone who later checks the concrete type. Spell out the resulting rule -- never match only `float()` -- and point at the shared alias in `frequenz-core` so libraries do not each define a local copy. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
This is the design rule the User Guide's validity section describes from the consuming side. Write it down for the producing side, since it is the one most likely to be got wrong in a new wrapper: invalid protobuf data is still data the client received, so a conversion function must keep it rather than drop it or raise. Document the three representations -- a guarded `Base*` with valid and `Invalid*` subclasses, a field-specific `Invalid*` wrapper when only one field is bad, and a dedicated subclass for protobuf recovery cases -- and the accessor and error hierarchy that lets callers demand a valid value. The load-bearing rule is to annotate `X | InvalidX` and never the guarded base. A `Base*` annotation hides which states actually exist and admits any future subclass, so a type checker can neither force a caller to handle the invalid case nor keep a `match` exhaustive. State that explicitly, with the existing signatures as evidence that `BaseBounds` and `BaseDeliveryArea` appear nowhere public. Also record the `__str__` convention from the producing side: reserve `<invalid:...>` for a violated rule and use `:field=value` for data this version merely does not know, so the two stay distinguishable in logs. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Conversion functions are where all the preceding rules meet, so document how to actually write one. The parts worth stating are the ones a reader cannot infer from an existing converter. Enum conversion must delegate to `enum_from_proto` rather than reimplementing the member-or-int fallback, so unrecognized values behave identically in every wrapper package. A converter must return `X | InvalidX` and keep malformed data, even though the valid type's own constructor rejects it -- the constructor and the converter have deliberately different contracts. Unset-field handling gets its own section because generated scalar defaults are indistinguishable from an explicitly sent zero: use `HasField()` and `WhichOneof()` rather than testing a default. And when retaining content the wrapper does not model yet, `MessageToDict()` needs `preserving_proto_field_name=True`, otherwise the retained keys come back `lowerCamelCase` and no longer match the protobuf field names callers would look for. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
This library is consumed by every other Frequenz client, so removing or
retyping a public symbol is expensive for people who do not read its
commit log. The upstream semantic-versioning rules cover the release
process, but not what to do in the code.
Document the mechanics: the exact `typing_extensions.deprecated` message
form -- `"<old FQCN> is deprecated. Use <new FQCN> instead."` with both
names fully qualified -- so a caller can act on the warning without
opening the source; the numeric-suffix convention (`thing_from_proto2`)
for a converter whose contract changed incompatibly; and
`frequenz.core.enum.deprecated_member` for an enum member.
Describe tightening an invariant as a staged process -- warn, then offer
an opt-in strict flag, then enforce at a minor bump -- because that is
what lets callers with warnings-as-errors find and migrate affected
construction sites deliberately. Note that a conversion function keeps
returning the typed invalid result throughout; only construction gets
stricter.
Finally, require `pytest.deprecated_call()` on every public deprecation,
and record the pattern for a deprecated symbol that legitimately calls
another one: suppress the inner warning in a narrow
`warnings.catch_warnings()` block so the outer API still emits exactly
one public warning.
Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Close the Wrapping Guide with the conventions that keep the preceding rules from silently rotting. Document the test layout -- mirror `src/frequenz/client/common/` with the prefix stripped, drop the implementation underscore, add `test_` -- and the requirement to import through the public package path, since a test that reaches into an underscore module stops proving that the supported imports work. Point at `EnumParityTest` for exported enums, which is the mechanism that catches a protobuf enum gaining or renumbering a member: without it, each new enum would need its own hand-written parity checks and most would never get them. Record the two properties of the suite that surprise newcomers: Sybil runs the examples in docstrings, so an example is a test and must be self-contained, and warnings are errors, so any expected warning needs an explicit `pytest.deprecated_call()` and suppression must stay narrow and never global. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
`CONTRIBUTING.md` carried a six-point list of field-name and docstring rules for wrapping protobuf messages. Those rules are now part of the Wrapping Guide, next to the package layout, type design, and conversion guidance they belong with, and stated with the reasoning the bare list never had. Replace the list with a pointer to the guide so there is one place to change when a convention moves, and so a contributor arriving through `CONTRIBUTING.md` finds the whole of the wrapper conventions rather than the fragment that happened to be written down here. This lands after the Wrapping Guide is complete, so the link resolves. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The Wrapping Guide is generic enough so contributors to this repository can use it as a guide to create new wrappers or update existing ones, but it lacks details on how to build a client beyond the the protobuf wrappers they need to create for their own API. The new Client Developer Guide comes to answer questions the other two guides don't answer, like which versioned conversion package to import, what a client method that converts a message should look like, what this library already converts, and when to stop and write a wrapper of their own. As with the previous two guides, this commit adds only the landing page and navigation entries, so the sections can be reviewed one at a time. This also completes the cycle in the link graph: the User Guide and Wrapping Guide indexes both pointed here, and those links now resolve. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The versioned `proto/<namespace>/` layout is easy to misread as this library's own versioning, which leads to importing whatever package looks newest. State the actual rule: the namespace tracks the `frequenz-api-common` protobuf API version your service speaks, and the release version of this library has nothing to do with the choice. Move to `v1alphaN` only once your client actually receives `v1alphaN` messages. Also describe what happens when a new protobuf API version arrives -- a sibling package, with the existing one untouched -- so client authors know their imports will not break under them. Add the `frequenz-api-common` inventory to `mkdocs.yml`: this is the first page to cross-reference a generated protobuf message type, via `location_pb2.Location`. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Show the two shapes a client method actually takes -- convert one message, or convert a repeated field -- because that is the whole of the mechanical work, and seeing it stated plainly saves a client author from inventing something more elaborate. The part worth documenting is what a client should *not* do. Unknown enum numbers and malformed data are already handled inside the conversion function, which keeps them as a plain `int` or an `Invalid*` wrapper rather than raising. A client that adds its own validation, filtering, or error handling on top is discarding exactly the information the wrapper types were designed to carry through to the user. Say so, and hand the interpretation question to the User Guide, where the user-facing answer lives. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Before writing a conversion function, a client author needs to know whether one already exists. Answering that from the API reference means walking every `proto/v1alpha8` package, so most people will not, and will duplicate a converter instead. Add a table indexed the way the question is actually asked: by protobuf message. Each row names the domain package, the `frequenz-api-common` messages it translates, and the conversion functions involved, linking both sides to their reference pages. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Draw the boundary between the two libraries. Messages from `frequenz-api-common` are converted here, so every client returns the same wrapper types for shared data. Messages from a service-specific API are the client's own responsibility, and the wrapper belongs in the client library -- with the same rule applied locally, that protobuf types stay inside the conversion functions and never reach a public method. Route the design work to the Wrapping Guide rather than restating it, since a wrapper written in a client library should look like a wrapper written here. Also note the case that is easy to miss: a service-specific message with a nested `frequenz-api-common` message should call this library's converter for that field instead of translating it again. This closes the Client Developer Guide and the series of guide pages. All the cross-guide links introduced along the way now resolve, so `mkdocs build` succeeds again from this commit on. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
The `AGENTS.md` files carried a condensed restatement of the wrapper design rules, written before any of it was documented properly. Now that the guides exist, keeping both copies guarantees they diverge, and the condensed one is the one an agent reads first. Delete the duplicated guidance and replace it with routing. The root file gains `docs/` in the structure map and two `WHERE TO LOOK` rows for the guides. `src/frequenz/client/common/AGENTS.md` loses its design sections in favor of a task-to-page table, keeping only the mechanics the guides deliberately do not cover: the enum naming derivation, the delegation snippet, the dataclass and ID conventions. `tests/AGENTS.md` points at the testing page for philosophy and keeps the layout and naming rules. Two corrections come along, because they are in the text being rewritten and leaving them would contradict the guides being linked. The converter naming entry and the `DEVIATIONS` section still described `*_from_proto_with_issues` and the two coexisting issue-reporting styles; those functions were removed or deprecated, and the surviving rule is the typed `X | InvalidX` result the Wrapping Guide documents. The commands section still named a `pytest` nox session that never actually existed (intead we have `pytest_min` and `pytest_max`). This lands after all the guide pages so every link resolves, and in one commit because the three files are one routing decision. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
llucax
requested review from
daniel-zullo-frequenz
and removed request for
a team
August 21, 2026 12:23
Contributor
Author
|
You can preview the generate guides here: https://llucax.github.io/frequenz-client-common-python/v0-dev/. |
Contributor
Author
|
This should be the last PR before the release 🎉 I will do a final check to try not to forget anything I wanted to include on this released, so maybe there are some minor follow-up PRs, but it is a huge release already. |
llucax
requested review from
Marenz,
cwasicki,
shsms and
tiyash-basu-frequenz
and
a balanced review from Copilot
August 21, 2026 12:29
There was a problem hiding this comment.
Pull request overview
Adds audience-specific documentation for wrapper users, client developers, and wrapper authors.
Changes:
- Adds three guides with examples and API links.
- Integrates guides into navigation, contributor guidance, and release notes.
- Updates enum documentation and external API inventories.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
AGENTS.md |
References canonical guides and updated test commands. |
CONTRIBUTING.md |
Links wrapper conventions to the new guide. |
RELEASE_NOTES.md |
Announces the three guides. |
mkdocs.yml |
Adds protobuf and API inventories. |
docs/SUMMARY.md |
Adds guides to top-level navigation. |
docs/user-guide/SUMMARY.md |
Defines user-guide navigation. |
docs/user-guide/index.md |
Introduces the user guide. |
docs/user-guide/typed-ids.md |
Documents typed identifiers. |
docs/user-guide/safe-accessors.md |
Explains validated accessors and errors. |
docs/user-guide/numeric-types.md |
Covers FloatInt handling. |
docs/user-guide/enum-or-int-fields.md |
Explains forward-compatible enum fields. |
docs/user-guide/validity-in-the-type.md |
Documents invalid-value wrappers. |
docs/user-guide/membership-and-bounds.md |
Explains bounds and membership. |
docs/user-guide/reading-string-output.md |
Describes diagnostic string formats. |
docs/user-guide/overview.md |
Catalogs available wrappers. |
docs/client-developer-guide/SUMMARY.md |
Defines client-guide navigation. |
docs/client-developer-guide/index.md |
Introduces client-library integration. |
docs/client-developer-guide/namespace-and-versioning.md |
Explains versioned converter imports. |
docs/client-developer-guide/using-conversion-functions.md |
Shows common conversion patterns. |
docs/client-developer-guide/shipped-converters.md |
Lists provided converter packages. |
docs/client-developer-guide/building-your-own.md |
Guides service-specific wrapper development. |
docs/wrapping-guide/SUMMARY.md |
Defines wrapping-guide navigation. |
docs/wrapping-guide/index.md |
Introduces wrapper design guidance. |
docs/wrapping-guide/organizing-a-wrapper-package.md |
Documents package organization. |
docs/wrapping-guide/enums.md |
Covers protobuf enum representation. |
docs/wrapping-guide/data-types.md |
Documents wrapper data types. |
docs/wrapping-guide/validity-in-the-type.md |
Defines typed validity patterns. |
docs/wrapping-guide/conversion-functions.md |
Documents converter implementation. |
docs/wrapping-guide/deprecation-and-compatibility.md |
Defines compatibility transitions. |
docs/wrapping-guide/testing.md |
Documents wrapper testing practices. |
src/frequenz/client/common/AGENTS.md |
Redirects design guidance to the guides. |
src/frequenz/client/common/proto/_enum.py |
Updates the enum conversion example. |
tests/AGENTS.md |
Links testing philosophy to its guide. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds 3 guides to the top navigation bar in the documentation website:
Fixes #252.