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.py — unittest.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.
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
GlideAggregatedoesn't slot in quite as cleanly asGlideRecordand there are a couple of plausible shapes.Proposed shape
A
GlideAggregateclass accessed via a factory method onServiceNowClient, mirroringclient.GlideRecord(...):Class sketch:
Plus an
AggregateTypeconstants class (COUNT,SUM,AVG,MIN,MAX,STDDEV) for ergonomics — strings everywhere on the wire.Note on
add_havingsignature: the Stats API'ssysparm_havingisaggregate^field^operator^value(caret-separated, four tokens, multi-clause joined by,). The field token is required by the server even forCOUNT— the docs and live API both reject 3-token forms. Hencefieldis a required positional arg, not an optional kwarg; callers passsys_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
Queryor fromGlideRecord?Server-side
GlideAggregate extends GlideRecord. In PySNC,GlideRecordis ~45KB and carries methods that don't apply to aggregates (insert,update,delete,set_value,to_pandas,add_attachment,serialize, …). SubclassingQuerymatches the existingJoinQuery(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 onGlideAggregatedirectly because the Stats API wire shape (groupby_fields[]+stats{}) differs from Table API records.Default proposal:
class GlideAggregate(Query). Happy to flip toGlideRecordif you want server-side fidelity over Python ergonomics.2. Internal API class or direct
client.session.get?ServiceNowClienthas internalTableAPI/AttachmentAPI/BatchAPIpeers. For symmetry there'd be aStatsAPIpeer; for simplicity v1 could just callclient.session.get(f"{instance}/api/now/stats/{table}", ...)directly.Default proposal: direct session for v1, listed as a follow-up refactor. Happy to add
StatsAPIif you'd prefer it in the same PR.3. Async sibling now, or follow-up?
pysnc/asyncio/has sync siblings for everything. I can includepysnc/asyncio/aggregate.pyin 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.
AggregateTypeconstants — yes/no/different style?Strings on the wire, but adding a
class AggregateType:with constants gives users IDE autocomplete without introducingenum(not used elsewhere in pysnc). Open to dropping it if you'd rather keep the surface area minimal.5. STDDEV
Server-side
GlideAggregatesupports STDDEV; the Stats API may or may not expose asysparm_stddev_fieldsparameter 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 liveVerified against a live PDI: Stats API's
sysparm_order_bysupports 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) ascendingsysparm_order_by=COUNT— order by row countsysparm_order_by=state^DESC— descending suffixSo
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
pysnc/aggregate.py— the class + constantspysnc/client.py— addGlideAggregate(table)factory method, importpysnc/__init__.py— addfrom .aggregate import *test/test_snc_aggregate.py—unittest.TestCase+Constants, hits a real PDI (mirrors existingtest_snc_api*.pystyle)README.md— move theGlideAggregate supportline out of "Feature Wants and TODO," add a Quick Start exampleWhat 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.