Skip to content

Add user, wrapping and client developer guides - #277

Open
llucax wants to merge 25 commits into
frequenz-floss:v0.x.xfrom
llucax:user-guide
Open

Add user, wrapping and client developer guides#277
llucax wants to merge 25 commits into
frequenz-floss:v0.x.xfrom
llucax:user-guide

Conversation

@llucax

@llucax llucax commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

This PR adds 3 guides to the top navigation bar in the documentation website:

  • User Guide: A guide targeting users of the library, explaining how to use the library and its features.
  • Wrapping Guide: A guide targeting developers who need to write their own protobuf wrappers (including contributors to this library).
  • Client Developer Guide: A guide targeting developers who want to use the library to write a API client library.

Fixes #252.

llucax added 25 commits August 21, 2026 12:50
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
llucax requested a review from a team as a code owner August 21, 2026 12:23
@llucax
llucax requested review from daniel-zullo-frequenz and removed request for a team August 21, 2026 12:23
@github-actions github-actions Bot added part:docs Affects the documentation part:tests Affects the unit, integration and performance (benchmarks) tests labels Aug 21, 2026
@github-actions github-actions Bot added the part:tooling Affects the development tooling (CI, deployment, dependency management, etc.) label Aug 21, 2026
@llucax

llucax commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

You can preview the generate guides here: https://llucax.github.io/frequenz-client-common-python/v0-dev/.

@llucax

llucax commented Aug 21, 2026

Copy link
Copy Markdown
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.

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

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.

Comment thread docs/wrapping-guide/enums.md
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: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.

Add guidelines on how to translate protobuf to Python wrappers

2 participants