Skip to content

Filters & ordering

filter= and order_by= take builder objects, not strings. They cover the whole query grammar, validate client-side, and escape values correctly — so a filter is wrong at construction, in Python, rather than as a parse error from the server.

AttrFilter(path) names an attribute; an operator turns it into a Condition:

from metrana.query import AttrFilter
AttrFilter("config/lr") < 0.01
AttrFilter("config/lr") >= 0.001
AttrFilter("sys/status") == "CLOSED"
AttrFilter("sys/status") != "FAILED"

Every operator also has a method spelling, for functional style or where an operator reads badly: .lt(), .lte(), .gt(), .gte(), .eq(), .ne().

Beyond comparison:

.matches(regex) RE2-style regex, evaluated server-side
.contains(value) substring on strings, membership on string sets
.contains_any([...]) / .contains_all([...]) string sets
.is_in([...]) / .not_in([...]) set membership
.exists() the attribute is present at all

Which of these are meaningful depends on the attribute’s kindcontains_all is a string-set operation, and < on a string compares lexicographically, not numerically. list_run_attribute_definitions reports the kinds.

Use & and |:

(AttrFilter("sys/status") == "CLOSED") & (AttrFilter("config/lr") < 0.01)
(AttrFilter("config/seed") == 0) | (AttrFilter("config/seed") == 1)

SeriesFilter(metric) filters on a summary of a series. Pick the function, then compare:

from metrana.query import SeriesFilter
SeriesFilter("loss").last() < 0.1
SeriesFilter("accuracy").max() >= 0.95
SeriesFilter("reward").with_scale("EPISODE").with_label("policy", "greedy").avg() > 100
SeriesFilter("loss").exists()

Labels here take the same operators and the same partial matching as a point fetch:

SeriesFilter("loss").with_label_matches("worker", "^gpu.*").avg() < 0.1
SeriesFilter("loss").with_label_in("shard", ["0", "1"]).max() < 2.0
SeriesFilter("loss").avg() < 0.1 # every series of the metric

A condition still yields one value per run: where the labels match several of a run’s series, the function is computed across them, and the comparison is applied to that. See Across-label aggregation.

from metrana.query import AttrOrderBy, SeriesOrderBy
AttrOrderBy("sys/creation_time").desc()
AttrOrderBy("config/lr").asc()
SeriesOrderBy("accuracy", "max").desc()
SeriesOrderBy("loss", "last").with_scale("EPISODE").asc()

Runs default to creation time descending when you pass no order_by.

Context Attribute conditions Series conditions
Runs (fetch_runs, run selectors)
RL environments (fetch_rl_envs, env selectors)
Projects, experiments, definition listings

A Condition remembers whether it contains a series condition, so passing one where it cannot work is rejected client-side with a message naming the context — not sent and refused.

Well-known attribute paths are exported as constants, so a typo is an ImportError rather than an empty result:

from metrana.query import SYS_STATUS, SYS_TAGS, SYS_CREATION_TIME, ENV_ID
AttrFilter(SYS_STATUS) == "CLOSED"
AttrFilter(SYS_TAGS).contains_all(["baseline"])
AttrOrderBy(SYS_CREATION_TIME).desc()

Every SYS_* constant, importable from metrana.query:

Constant Path
SYS_NAME sys/name
SYS_ID sys/id
SYS_FAMILY sys/family
SYS_EXPERIMENT_NAME sys/experiment/name
SYS_EXPERIMENT_ID sys/experiment/id
SYS_PROJECT_NAME sys/project/name
SYS_PROJECT_ID sys/project/id
SYS_PARENT_DIRECTORY_ID sys/parent_directory_id
SYS_DESCRIPTION sys/description
SYS_TAGS sys/tags
SYS_ARCHIVED sys/archived
SYS_CREATION_TIME sys/creation_time
SYS_MODIFICATION_TIME sys/modification_time
SYS_CLOSE_TIME sys/close_time
SYS_OWNER sys/owner
SYS_STATUS sys/status
SYS_RUNNING_TIME_SECONDS sys/running_time_seconds
SYS_FORKING_PARENT_RUN_NAME sys/forking/parent_run_name
SYS_FORKING_PARENT_RUN_ID sys/forking/parent_run_id
SYS_FORKING_STEP sys/forking/step
SYS_DATA_READINESS_READY sys/data_readiness/ready
SYS_DATA_READINESS_READY_TIME sys/data_readiness/ready_time

SYS_STATUS takes the run states you see in the portal — CREATED, RUNNING, CLOSED, CRASHED (see run status). SYS_ARCHIVED is the flag behind the runs table’s archived toggle, so AttrFilter(SYS_ARCHIVED) == False reproduces the portal’s default view.

Constant Meaning
ENV_ID The environment’s env_id.
ENV_LATEST_EPISODE Highest episode the environment has logged.
ENV_LATEST_RL_STEP Highest RL step the environment has logged.

RESERVED_ENV_FIELDS is the set of all three, useful if you need to tell a reserved field apart from one of your own logged environment attributes:

from metrana.query import RESERVED_ENV_FIELDS
is_reserved = path in RESERVED_ENV_FIELDS

Every fetch that needs a scope takes it in one of four forms. The accepted union is named RunsSelector for runs and EnvironmentsSelector for environments:

Form Example Use when
A single name "run-001" You know exactly which one.
A sequence of names ["run-001", "run-002"] You have a fixed list, e.g. from fetch_runs.
A Condition AttrFilter("config/lr") < 0.01 You want “whichever match”, without listing them.
A RunsSelection / EnvironmentsSelection see below You need ordering or a cap as well as a filter.

The first three cover most code. Reach for the *Selection form when which ones you get depends on order — “the five most recent closed runs” is a filter plus an ordering, not a filter alone:

from metrana.query import AttrFilter, AttrOrderBy, RunsSelection, SYS_CREATION_TIME, SYS_STATUS
client.fetch_float_series(
RunsSelection(
filter=AttrFilter(SYS_STATUS) == "CLOSED",
order_by=AttrOrderBy(SYS_CREATION_TIME).desc(),
),
"loss",
max_runs=5,
)

Both fields are optional, and order_by is applied before any max_runs / max_environments cap — which is the whole point: the cap keeps the first N in your order, not an arbitrary N. The default ordering differs between the two:

  • RunsSelectioncreation time descending.
  • EnvironmentsSelectionenvironment id ascending.

EnvironmentsSelection is otherwise the same shape, and accepts both the reserved environment fields and your own logged attributes.