Skip to content

Querying and pagination

Hubuum filters use field__operator=value. The Python client represents them as immutable Query values:

from hubuum_client import FilterOperator, Query

query = (
    Query()
    .where("name", "server", FilterOperator.ICONTAINS)
    .where("validate_schema", True)
    .sort("name.asc")
    .limit(25)
    .include_total()
)
page = client.classes.page(query)

Dates and datetimes are encoded as ISO 8601 values. Booleans are encoded as lowercase JSON-style strings.

Operators

FilterOperator includes equality, case-insensitive equality, containment, prefix, suffix, SQL-like, regular-expression, comparison, range, membership, null, JSON structure, and IP/network operators, along with each operator's negated form. The server validates whether an operator is legal for a particular field.

Object data

Use data() to select a key path inside an object's JSON data document. A terminal filter returns a new immutable Query, so filters compose naturally:

query = (
    Query()
    .data("status")
    .equals("active")
    .data("metrics", "cpu_count")
    .gte(4)
    .data("tags")
    .contains_all("web", "api")
)

objects = client.classes.by_id(class_id).objects.all(query)

The path is passed as one key per argument. For example, data("network", "address") selects data["network"]["address"] and encodes the server value network,address=.... Commas and equals signs cannot be used in path keys because Hubuum v0.0.16 does not define escaping for those delimiters.

Common scalar and textual filters use direct method names:

from datetime import date

Query().data("hostname").icontains("web")
Query().data("enabled").equals(True)
Query().data("maintenance", "starts_at").between(
    date(2026, 7, 1),
    date(2026, 7, 31),
)

Available scalar methods are equals, iequals, contains, icontains, starts_with, istarts_with, ends_with, iends_with, like, regex, gt, gte, lt, lte, and between. Booleans use lowercase wire values, and dates and datetimes use ISO 8601. Set negate=True on any terminal to use the corresponding server not_ operator.

JSON arrays, objects, and nulls have semantic helpers:

Query().data("status").one_of("active", "standby")
Query().data("tags").contains_all("web", "api")
Query().data("tags").array_length(2)
Query().data("config").has_key("hostname")
Query().data("retired_at").is_null()
Query().data("retired_at").is_null(negate=True)

one_of matches either a scalar in the supplied set or an array containing at least one supplied value. contains_all requires every supplied array value. is_null matches both a missing path and JSON null, following server semantics.

Network-aware filters accept strings or values from Python's ipaddress module:

from ipaddress import ip_network

Query().data("network", "address").within_network(ip_network("10.0.0.0/24"))
Query().data("network", "address").contains_network("10.0.0.0/25")
Query().data("network", "address").contains_ip("10.0.0.10")
Query().data("network", "address").overlaps_network("10.0.0.64/26")
Query().data("network", "address").inet_equals("10.0.0.10/32")

These helpers generate normal json_data__operator parameters and work unchanged with both synchronous and asynchronous object services.

Result shapes

Resource services expose four common terminals:

  • list(query) returns the current page's items as a list;
  • page(query) returns Page[T] with items and cursor metadata;
  • all(query) follows cursors and collects bounded results;
  • one(query) requires exactly one item and raises ResultCardinalityError otherwise.
page = client.classes.by_id(class_id).objects.page(Query().limit(50).include_total())

print(page.total_count)
print(page.page_limit)
if page.has_next:
    next_page = client.classes.by_id(class_id).objects.page(Query().cursor(page.next_cursor))

total_count is None when include_total=False or the server omits the header. page_limit is the effective server-applied limit.

Automatic pagination safeguards

items = client.classes.all(query, max_pages=50, max_items=5_000)

The client rejects repeated cursors, more than max_pages, and more than max_items. Bounds must be positive and are validated before the client makes a request. The async client provides the same API, and its pages() method is an async iterator.

The default bounds are intentionally conservative. Increase them explicitly for a known large result set, or consume pages individually when streaming work is more appropriate.

Group members are returned as typed PrincipalMember values. Membership pagination follows the same bounds and cursor rules through members_page(), member_pages(), and all_members():

page = client.groups.members_page(group_id, Query().limit(50).include_total())
members = client.groups.all_members(group_id, max_pages=20, max_items=5_000)

Each membership has its own revision, group_id, and principal_id. List responses also include the optional nested principal details, whose revision is independent of the enclosing membership revision.

Exact-name routes

Hubuum v0.0.16 supports explicit natural-key aliases for classes and objects. Use the complete name-addressed service when class and object names are already known:

selected_class = client.classes.by_name("12345")
hubuum_class = selected_class.get()
hubuum_object = selected_class.objects.get("67890")
all_objects = selected_class.objects.all()

id_selected_class = client.classes.by_id(class_id)
same_class = id_selected_class.get()
id_scoped_objects = id_selected_class.objects.all()

Both names are encoded as opaque path segments after explicit by-name markers, so spaces, slashes, and numeric-looking values remain unambiguous. The nested selectors make the resource hierarchy and ID-versus-name choice explicit.

Task discovery

Use TaskQuery for client.tasks.page(), list(), pages(), and all(). Task filters use plain parameter names; generic resource Query.where() produces operator-suffixed parameters that the task endpoint does not accept.

from datetime import UTC, datetime
from hubuum_client import TaskKind, TaskQuery, TaskStatus

query = (
    TaskQuery(
        kind=(TaskKind.EXPORT, TaskKind.BACKUP),
        status=(TaskStatus.SUCCEEDED, TaskStatus.FAILED),
        terminal=True,
        created_after=datetime(2026, 9, 22, tzinfo=UTC),
    )
    .limit(25)
    .sort("finished_at.desc,id.desc")
    .include_total()
)
tasks = client.tasks.all(query)

Kinds and statuses use comma-separated OR lists; different filters combine with AND. Immutable pagination builders preserve all filters across cursors. The server validates combinations: relation identity requires both relation_type and relation_id, and schema/computation revisions require class_id. Supply timezone-qualified timestamps; lower bounds are inclusive and upper bounds exclusive. Explicit statuses must agree with terminal.

Resource filters include class_id, object_id, collection_id, relation identity, remote_target_id, and export_template_id. Kind-specific filters cover schema work, computation revision, remote side-effect state, export warnings/truncation, import options/failures, backup history, and output_state. Lifecycle controls also include cancel_requested, terminal_reason, submitted_by (administrator-only), and trace_id. See TaskQuery in the API reference for every typed field.

Task.details supports all six kinds: import_, export, backup, schema_validation, reindex, and remote_call. Import/export/backup details include typed retained options. Schema details include the class, revision, work status, and retained results URL; rebuild details include the computation revision, and remote calls include their explicit target.

Historical unknown values remain None, distinct from False. Output state is available, expired, not_produced, or unknown. Discovery checks current resource authorization before matching/counting and may suppress details and output links while retaining basic task status. Resource filters match explicit captured targets, not present-day membership or resources inferred from an import payload or export query. See the server task reference.