Skip to content

GlideAggregate support (/api/now/stats/{tableName}) #145

@orwellsanimal

Description

@orwellsanimal

GlideAggregate support (/api/now/stats/{tableName})

The README lists this under "Feature Wants and TODO." I'd like to contribute it. Before I open a PR I want to confirm a few design choices, since GlideAggregate doesn't slot in quite as cleanly as GlideRecord and there are a couple of plausible shapes.

Proposed shape

A GlideAggregate class accessed via a factory method on ServiceNowClient, mirroring client.GlideRecord(...):

ga = client.GlideAggregate('incident')
ga.add_active_query()                       # inherited query builder
ga.add_aggregate('COUNT')
ga.add_aggregate('AVG', 'reassignment_count')
ga.group_by('priority')
ga.add_having('COUNT', 'sys_id', '>', '5')  # field required even for COUNT (Stats API)
ga.query()

for row in ga:
    priority = row.get_value('priority')
    count    = row.get_aggregate('COUNT')
    avg_rc   = row.get_aggregate('AVG', 'reassignment_count')
    print(priority, count, avg_rc)

Class sketch:

class GlideAggregate(Query):
    """Aggregate queries against /api/now/stats/{table}. Mirrors the
    server-side GlideAggregate."""

    def __init__(self, client: 'ServiceNowClient', table: str) -> None: ...

    # Inherited from Query: add_query, add_encoded_query,
    # add_null_query, add_not_null_query, add_active_query, get_encoded_query

    def add_aggregate(self, agg_type: str, field: Optional[str] = None) -> None: ...
    def group_by(self, field: str) -> None: ...
    def order_by(self, field: str) -> None: ...
    def order_by_desc(self, field: str) -> None: ...
    def add_having(self, agg_type: str, field: str, operator: str, value) -> None: ...
    def set_display_value(self, display_value: bool = True) -> None: ...

    def query(self) -> None: ...
    def next(self) -> bool: ...
    def has_next(self) -> bool: ...
    def get_row_count(self) -> int: ...

    def get_aggregate(self, agg_type: str, field: Optional[str] = None) -> Optional[str]: ...
    def get_value(self, field: str) -> Optional[str]: ...
    def get_display_value(self, field: str) -> Optional[str]: ...

    def __iter__(self): ...
    def __next__(self): ...
    def __len__(self) -> int: ...

Plus an AggregateType constants class (COUNT, SUM, AVG, MIN, MAX, STDDEV) for ergonomics — strings everywhere on the wire.

Note on add_having signature: the Stats API's sysparm_having is aggregate^field^operator^value (caret-separated, four tokens, multi-clause joined by ,). The field token is required by the server even for COUNT — the docs and live API both reject 3-token forms. Hence field is a required positional arg, not an optional kwarg; callers pass sys_id (or any field) when there's no semantic field to aggregate. Surfacing this here because it's a non-obvious wire detail and I don't want to bake an ergonomic-but-broken shape into v1.

Open design questions

These are the calls I'd appreciate input on before writing the PR — I have a default proposal for each, but each is reasonable to flip.

1. Inherit from Query or from GlideRecord?

Server-side GlideAggregate extends GlideRecord. In PySNC, GlideRecord is ~45KB and carries methods that don't apply to aggregates (insert, update, delete, set_value, to_pandas, add_attachment, serialize, …). Subclassing Query matches the existing JoinQuery(Query) / RLQuery(Query) precedent and gives us the query-builder methods for free without the CRUD baggage. Result-cursor methods (next, get_value, get_aggregate, __iter__) live on GlideAggregate directly because the Stats API wire shape (groupby_fields[] + stats{}) differs from Table API records.

Default proposal: class GlideAggregate(Query). Happy to flip to GlideRecord if you want server-side fidelity over Python ergonomics.

2. Internal API class or direct client.session.get?

ServiceNowClient has internal TableAPI / AttachmentAPI / BatchAPI peers. For symmetry there'd be a StatsAPI peer; for simplicity v1 could just call client.session.get(f"{instance}/api/now/stats/{table}", ...) directly.

Default proposal: direct session for v1, listed as a follow-up refactor. Happy to add StatsAPI if you'd prefer it in the same PR.

3. Async sibling now, or follow-up?

pysnc/asyncio/ has sync siblings for everything. I can include pysnc/asyncio/aggregate.py in this PR, or punt to a follow-up.

Default proposal: sync first; async as a follow-up PR with maintainer involvement on async patterns I haven't internalized yet.

4. AggregateType constants — yes/no/different style?

Strings on the wire, but adding a class AggregateType: with constants gives users IDE autocomplete without introducing enum (not used elsewhere in pysnc). Open to dropping it if you'd rather keep the surface area minimal.

5. STDDEV

Server-side GlideAggregate supports STDDEV; the Stats API may or may not expose a sysparm_stddev_fields parameter on every release. I'll verify against a live PDI during prototyping. If unsupported, I'll omit STDDEV with a docstring note rather than implementing it client-side.

6. order_by_aggregate — server-side, confirmed live

Verified against a live PDI: Stats API's sysparm_order_by supports both group-by fields and aggregates directly. From the docs (api-reference/rest-apis/c_AggregateAPI.md):

  • sysparm_order_by=AVG^state — order by avg(state) ascending
  • sysparm_order_by=COUNT — order by row count
  • sysparm_order_by=state^DESC — descending suffix

So order_by_aggregate(type, field) is a one-liner that builds the caret-separated value. Will implement server-side; no client-side post-sort.

Files touched

  • new pysnc/aggregate.py — the class + constants
  • edit pysnc/client.py — add GlideAggregate(table) factory method, import
  • edit pysnc/__init__.py — add from .aggregate import *
  • new test/test_snc_aggregate.pyunittest.TestCase + Constants, hits a real PDI (mirrors existing test_snc_api*.py style)
  • edit README.md — move the GlideAggregate support line out of "Feature Wants and TODO," add a Quick Start example

What I'm asking for

Just a thumbs-up (or pushback) on the inheritance choice (#1) and any of the other questions where you have an opinion. Anything you don't weigh in on, I'll go with the defaults above and you can redirect at PR review.

Happy to prototype on a fork and link the branch here for inspection before opening the PR. Thanks for considering it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions