Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,10 +614,25 @@ Invariants worth knowing before touching this code:
- **`save_annotation()` replaces in full, including calculations**: omitting them clears *and deletes* the stored
object, and a replacement returns a new `calculationsId`. `get_annotation()` is the only method returning
calculations inline; `query_annotations()` results carry the id with empty content.
- Two criterion-helper differences from the older `PvMetadataQuery` / `ConfigurationQuery` helpers, both following
the proto: `attributes(key)` accepts an absent `values` list as a key-only existence search, and `criteria` is
optional because the server treats an empty list as match-all. Back-porting these to the five existing helpers is
[#40](https://github.com/osprey-dcs/dp-python-lib/issues/40) / [#41](https://github.com/osprey-dcs/dp-python-lib/issues/41).
- Two conventions these clients introduced, both following the proto, **now shared by every criteria-based client**
after the [#40](https://github.com/osprey-dcs/dp-python-lib/issues/40) /
[#41](https://github.com/osprey-dcs/dp-python-lib/issues/41) back-ports (`plan/tickets/40/plan.md`,
`plan/tickets/41/plan.md`):
- **`attributes(key)` / `attr(key)` accept an absent or empty `values` list as a key-only existence search** —
match every record possessing the key, whatever its value. All seven attribute helpers now do this
(`PvMetadataQuery`, `ConfigurationQuery`, `ConfigurationActivationQuery`, `DataSetQuery`, `AnnotationQuery`,
`PvQuery.attr`, `ConfigQuery.attr`). It is the one exception to the "helpers reject empty input" rule, because
Comment thread
craigmcchesney marked this conversation as resolved.
unlike `tags([])` it *narrows* the result set rather than matching everything. The **key** is still required,
and that check is load-bearing on the two v2 query selectors, where the server does not validate it: a blank key
would reach Mongo as an existence test on `"attributes."` and silently match nothing. Server side, empty values
build `Filters.exists("attributes.<key>")` (`MongoQueryFilterBuilder.attributeFilter()`), not an `$in: []`
- **`criteria` is optional on every paged annotation-service query/iter method**, because the server treats an
empty list as match-all — `iter_pv_metadata()` with no arguments is the browse-all form. The server's default
page size (100, hardcoded in `MongoSyncAnnotationClient.DEFAULT_QUERY_LIMIT`, **not** configurable) applies
**unconditionally**: dropping the last criterion does not change the page size, so a bare `query_*()` still
returns one page and a token. That is why `iter_*` is the right call for browsing. Note the v2 query methods
are deliberately *not* included: `QueryParams` still requires a PV selector or config criteria, since a
time-series query with no selection is unbounded rather than a browse-all
- `ExportFormat` makes the server-rejected `EXPORT_FORMAT_UNSPECIFIED` unreachable, and `ExportDataRequestParams`
requires at least one of `dataset_id` / `data_blocks` / `calculations_spec`. The exported file lives on the
**server's** filesystem and there is no retrieval RPC, so there is no download convenience.
Expand Down
34 changes: 32 additions & 2 deletions doc/cookbook/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ Some things to keep in mind:
- **Omitting `limit` does not mean "no limit".** The server applies its own default page size
(currently 100) when `limit` is absent or zero, so a `query_*` call without one still returns a
page, not the whole result set. Check `next_page_token`.
- **Omitting the criteria browses everything.** On the annotation-service queries — PV metadata,
configurations, activations, datasets, annotations — an omitted or empty criteria list matches
*all* records rather than being rejected, so `iter_pv_metadata()` with no arguments walks the
whole catalogue:

```python
# cookbook:partial
for record in client.annotation.pv_metadata.iter_pv_metadata():
print(record.pvName)
```

The default page size still applies unconditionally — it does not change just because you
dropped the last criterion — so the bare `query_*` form returns one page and a token, not the
collection. `iter_*` is the right call here. Mind how large the collection is before iterating
it.
- **There is no total count.** The API deliberately omits it — computing one requires a separate
expensive query — so you cannot know the result size in advance.
- **Results come back in a stable order.** The server sorts each collection by its natural key —
Expand Down Expand Up @@ -175,8 +190,8 @@ Name and alias criteria accept `exact`, `prefix`, and `contains` lists, which ma
criteria = [Q.pv_name(prefix=["BPMS:"], contains=["GUNB"])]
```

**The helpers reject empty input.** Every one of them raises `ValueError` rather than building a
criterion that would silently match everything:
**The helpers reject empty input.** They raise `ValueError` rather than building a criterion that
would silently match everything:

```python
# cookbook:partial
Expand All @@ -187,6 +202,21 @@ Q.pv_name() # ValueError: requires at least one non-empty of exa
That is a deliberate guard — an empty criterion is nearly always a bug in the caller's filter
construction, and failing loudly beats returning the whole collection.

**The one exception is `attributes()` / `attr()`, where an empty `values` list is meaningful.** It
is the protocol's key-only *existence* search: match every record possessing the attribute key,
whatever its value. That narrows the result set rather than matching everything, so the reasoning
above does not apply and the helpers allow it:

```python
# cookbook:partial
Q.attributes("S") # every PV that has an S attribute at all
Q.attributes("S", []) # the same thing
Q.attributes("", ["0.49"]) # ValueError: the *key* is still required
```

This holds for all seven attribute helpers — `Q`, `C`, `CA`, `DS`, `AQ`, and the `PV.attr()` /
`CFG.attr()` query selectors.

## Save semantics: full replace

Methods named `save_*` are **full-replace upserts**, not partial updates. Omitted fields are
Expand Down
16 changes: 16 additions & 0 deletions doc/cookbook/machine-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,25 @@ for configuration in client.annotation.machine_config.iter_configurations([
print(configuration.configurationName)
```

Omit the criteria entirely to browse everything — an empty criteria list matches all records:

```python
# cookbook:partial
for configuration in client.annotation.machine_config.iter_configurations():
print(configuration.configurationName)
```

The default page size still applies, so prefer `iter_configurations()` over a bare
`query_configurations()` when you want the whole set. The same holds for
`iter_configuration_activations()`, which on a busy machine can be a much larger collection.

`C` offers `name`, `category`, `tags`, `attributes`, and `parent`; `CA` offers `timestamp`,
`time_range`, `configuration_name`, `client_activation_id`, `category`, `tags`, and `attributes`.

Both `attributes(key, values)` helpers take the values list as optional: omit it for a **key-only
existence search** matching every record that has the key, whatever its value — say
`C.attributes("owner")` for "every configuration with an owner recorded".

## Addressing a specific activation

`get_configuration_activation()` and `delete_configuration_activation()` take **either** a client
Expand Down
39 changes: 24 additions & 15 deletions doc/cookbook/pv-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,30 +350,39 @@ This reads every matching record to filter client-side, so keep the server-side
as you can. Note also that string equality is exact: `"0.489650"` and `"0.48965"` are different
attribute values even though the numbers are equal.

### Key-only (existence) search is not exposed
### Browsing the whole catalogue

The protocol supports matching on an attribute *key* regardless of value, by sending an
`AttributesCriterion` with an empty `values` list. The `Q.attributes()` helper does not allow
this — it raises `ValueError` on empty values, since an accidentally-empty list is far more often
a bug than a deliberate existence search.

If you genuinely need it, build the criterion directly:
Omit the criteria entirely to walk every record — an empty criteria list matches all:

```python
# cookbook:partial
# cookbook:no-mypy (generated protobuf classes are built at import time; not statically visible)
from dp_python_lib.grpc import annotation_pb2
for record in client.annotation.pv_metadata.iter_pv_metadata():
print(record.pvName)
```

The server's default page size still applies, so use `iter_pv_metadata()` rather than a bare
`query_pv_metadata()`, which returns only the first page. Check how big the catalogue is before
iterating all of it.

criterion = annotation_pb2.QueryPvMetadataRequest.QueryPvMetadataCriterion()
criterion.attributesCriterion.key = "S" # no values -> match any PV having an S attribute
### Which PVs have this attribute at all?

for record in client.annotation.pv_metadata.iter_pv_metadata([criterion]):
Omit the values list — or pass an empty one — for a **key-only existence search**: every PV
possessing the key matches, whatever its value.

```python
# cookbook:partial
# every PV that has been surveyed, whatever its position
for record in client.annotation.pv_metadata.iter_pv_metadata([Q.attributes("S")]):
print(record.pvName)
```

Dropping to the generated stubs like this is the general escape hatch whenever a helper is
stricter than the protocol. The message classes are built dynamically at import time, so static
type checkers cannot see them — the code is correct, but your editor may flag it.
This is useful for finding gaps in the catalogue. Combine it with a positive criterion to ask
"which PVs in this area are *missing* a survey?" — the existence search finds the ones that have
it, and the difference is what you need to fill in.

The *key* is still required: `Q.attributes("", ["0.49"])` raises `ValueError`. The same holds for
the `key` argument of `C.attributes`, `CA.attributes`, and the `PV.attr` / `CFG.attr` query
selectors.

### Other details

Expand Down
4 changes: 4 additions & 0 deletions doc/cookbook/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ params = QueryParams(
`CFG` offers `configuration_name`, `client_activation_id`, `category`, `tags`, and
`attr(key, values)`.

On both `PV.attr()` and `CFG.attr()` the values list is optional: omit it for a **key-only
existence search** matching everything that has the key, whatever its value — `PV.attr("AREA")`
selects every PV with an area recorded.

### The result covers several disjoint intervals

If a configuration was active more than once inside the time range — two shifts in a day, say —
Expand Down
Loading