Skip to content

Commit 33a569c

Browse files
committed
Improve CORE documentation
1 parent 6dcf2c4 commit 33a569c

File tree

4 files changed

+65
-6
lines changed

4 files changed

+65
-6
lines changed

src/obelisk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Obelisk-py is a client for the Obelisk data platform.
33
We support both "classic" Obelisk and HFS,
44
each with a synchronous and async API.
5+
We also support Obelisk CORE, in async only for now.
56
The PyPi package name is ``obelisk-py``, the Python module is called ``obelisk``.
67
78
Your starting point will be one of the Obelisk instances in :mod:`~.sync` or :mod:`~.asynchronous` depending on your preferred API.

src/obelisk/asynchronous/core.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525

2626

2727
def type_suffix(metric: str) -> DataType:
28+
"""
29+
Extracts the :any:`DataType` from a string metric,
30+
useful for the dataType field in queries.
31+
32+
Throws a :py:exc:`ValueError` if the provided string does not appear to be a typed metric,
33+
or the found type suffix is not a known one.
34+
"""
2835
split = metric.split('::')
2936

3037
if len(split) != 2:
@@ -96,7 +103,12 @@ class QueryParams(BaseModel):
96103
orderBy: Optional[List[str]] = None # More complex than just FieldName, can be prefixed with - to invert sort
97104
dataType: Optional[DataType] = None
98105
filter_: Annotated[Optional[str|Filter], Field(serialization_alias='filter')] = None
99-
"""Filter in `RSQL format <https://obelisk.pages.ilabt.imec.be/obelisk-core/query.html#rsql-format>`__ Suffix to avoid collisions."""
106+
"""
107+
Obelisk CORE handles filtering in `RSQL format <https://obelisk.pages.ilabt.imec.be/obelisk-core/query.html#rsql-format>`__ ,
108+
to make it easier to also programatically write these filters, we provide the :class:`Filter` option as well.
109+
110+
Suffix to avoid collisions.
111+
"""
100112
cursor: Optional[str] = None
101113
limit: int = 1000
102114

src/obelisk/types/core.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,43 @@
1212
... Comparison.less('timestamp', datetime.fromtimestamp(1757422128))
1313
... ))
1414
>>> print(f)
15-
((('source'=='test source');('metricType'=in=('number', 'number[]'))),('timestamp'<'2025-09-09T14:48:48'))
15+
(('source'=='test source';'metricType'=in=('number', 'number[]')),'timestamp'<'2025-09-09T14:48:48')
1616
"""
1717
from __future__ import annotations
1818
from abc import ABC
1919
from datetime import datetime
2020
from typing import Any, Iterable, List
2121

2222

23-
FieldName = str # TODO: validate field names?
23+
FieldName = str
2424
"""https://obelisk.pages.ilabt.imec.be/obelisk-core/query.html#available-data-point-fields
2525
Field names are not validated at this time, due to the inherent complexity.
2626
"""
2727

2828

2929
class Constraint(ABC):
30+
"""
31+
Constraints are simply groups of :class:`Comparison`,
32+
such as :class:`And`, or :class:`Or`.
33+
34+
These constraints always enclose their contents in parentheses,
35+
to avoid confusing precendence situations in serialised format.
36+
"""
3037
pass
3138

3239

3340
class Comparison():
41+
"""
42+
Comparisons are the basic items of a :class:`Filter`.
43+
They consist of a field name, operator, and possibly a value on the right.
44+
45+
It is strongly suggested you create comparisons by using the staticmethods
46+
on this class, rather than trying to construct them yourselves.
47+
48+
When serializing to RSQL format,
49+
each argument is single quoted as to accept any otherwise reserved characters,
50+
and serialised using :func:`str`.
51+
"""
3452
left: FieldName
3553
right: Any
3654
op: str
@@ -42,10 +60,10 @@ def __init__(self, left: FieldName, right: Any, op: str):
4260

4361
def __str__(self) -> str:
4462
right = self._sstr(self.right)
45-
if not right.startswith('('):
63+
if not right.startswith('(') and len(right) > 0:
4664
right = f"'{right}'"
4765

48-
return f"('{self.left}'{self.op}{right})"
66+
return f"'{self.left}'{self.op}{right}"
4967

5068
@staticmethod
5169
def _sstr(item: Any):
@@ -124,6 +142,18 @@ def __str__(self) -> str:
124142

125143

126144
class Filter():
145+
"""
146+
Filter is an easier way to programatically create filters for the Obelisk CORE platform.
147+
148+
We still recommend you familiarise yourself with the CORE filter documentation,
149+
as not everything is typechecked.
150+
Specifically, the left hand side of any comparison is left unchecked.
151+
Checking this would be borderline impossible with the optional arguments to some fields,
152+
and the dot notation for labels.
153+
154+
As this field is not checked, we also do not check whether the type of left operand
155+
and right operand make sense in the provided comparison.
156+
"""
127157
content: Item | None = None
128158

129159
def __init__(self, content: Constraint | None = None):
@@ -133,13 +163,29 @@ def __str__(self) -> str:
133163
return str(self.content)
134164

135165
def add_and(self, *other: Item) -> Filter:
166+
"""
167+
Encloses the current filter contents, if any,
168+
in an :class:`And`, along with any other provided Items.
169+
170+
>>> str(Filter(Comparison.not_null('labels.username'))
171+
... .add_and(Comparison.equal('labels.username', 'stpletin'), Comparison.less_equal('timestamp', '2025-01-12T00:00:00Z')))
172+
"('labels.username'=notnull=;'labels.username'=='stpletin';'timestamp'<='2025-01-12T00:00:00Z')"
173+
"""
136174
if self.content is None:
137175
self.content = And(*other)
138176
else:
139177
self.content = And(self.content, *other)
140178
return self
141179

142180
def add_or(self, *other: Item) -> Filter:
181+
"""
182+
Encloses the current filter contents, if any,
183+
in an :class:`Or`, along with any other provided Items.
184+
185+
>>> str(Filter(Comparison.not_null('labels.username'))
186+
... .add_or(Comparison.equal('labels.username', 'stpletin'), Comparison.less_equal('timestamp', '2025-01-12T00:00:00Z')))
187+
"('labels.username'=notnull=,'labels.username'=='stpletin','timestamp'<='2025-01-12T00:00:00Z')"
188+
"""
143189
if self.content is None:
144190
self.content = Or(*other)
145191
else:

src/tests/typetest/filter_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ def test_basic_filter():
1515
Comparison.is_in('metricType', ['number', 'number[]']),
1616
)
1717

18-
expected = f"(((('source'=='test source')),('timestamp'<'{test_dt.isoformat()}')),('metricType'=in=('number', 'number[]')))"
18+
expected = f"((('source'=='test source'),'timestamp'<'{test_dt.isoformat()}'),'metricType'=in=('number', 'number[]'))"
1919
assert str(f) == expected

0 commit comments

Comments
 (0)