Skip to content

Commit f3c78b6

Browse files
committed
Update RST docs to MD format
Look man sphinx is powerful and all but pdoc just deals with Pydantic
1 parent 2d7c84d commit f3c78b6

10 files changed

Lines changed: 220 additions & 99 deletions

File tree

.github/workflows/pdoc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- uses: actions/setup-python@v6
2727
with:
2828
python-version: '3.14'
29-
- run: uv run pdoc ./src/obelisk -o docs/
29+
- run: uv run pdoc ./src/obelisk -o docs/ --math
3030
- uses: actions/upload-pages-artifact@v4
3131
with:
3232
path: docs/

src/obelisk/__init__.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,79 @@
33
We support both "classic" Obelisk and HFS,
44
each with a synchronous and async API.
55
We also support Obelisk CORE, in async only for now.
6-
The PyPi package name is ``obelisk-py``, the Python module is called ``obelisk``.
76
8-
Your starting point will be one of the Obelisk instances in :mod:`~.sync` or :mod:`~.asynchronous` depending on your preferred API.
7+
The PyPi package name is `obelisk-py`, the Python module is called `obelisk`.
8+
9+
Your starting point will be one of the Obelisk instances in `.sync` or `.asynchronous` depending on your preferred API.
910
1011
The Obelisk classes in these modules both implement the same interface,
1112
but the asynchronous implementation returns Coroutines.
1213
13-
Error handling
14-
--------------
14+
## Error handling
1515
1616
Obelisk-py comes with robust retry logic to handle any errors that may come up.
1717
Issues like timeouts, temporary server errors or even DNS issues are fairly common, and handling them properly is important.
18-
Each Client accepts a retry strategy of type :class:`~.strategies.retry.RetryStrategy`.
19-
Several predefined strategies are available in :mod:`.strategies.retry`.
18+
Each Client accepts a retry strategy of type `.strategies.retry.RetryStrategy`.
19+
Several predefined strategies are available in `.strategies.retry`.
20+
21+
## Quick Start
22+
23+
Using CORE
24+
```py
25+
from obelisk.asynchronous.core import Client, QueryParams
26+
from obelisk.types.core import Filter, Comparison
27+
import os
28+
import asyncio
29+
30+
client_id = os.getenv('CLIENT_ID')
31+
client_secret = os.getenv('CLIENT_SECRET')
32+
33+
# You may want to specify a retry strategy
34+
client = Client(
35+
client=client_id,
36+
secret=client_secret
37+
)
38+
39+
query = QueryParams(
40+
dataset="some-dataset",
41+
fields=["metric","labels","value","timestamp"],
42+
dataType="number",
43+
filter_=Filter().add_and(
44+
Comparison.equal("metric", "heart_rate::number"),
45+
Comparison.greater("timestamp", 42069180)
46+
)
47+
48+
data = asyncio.get_event_loop().run_until_complete(client.query(query))
49+
```
50+
51+
Using Classic or HFS, synchronously (async is analogous)
52+
```py
53+
from obelisk.sync import Client
54+
from obelisk.types import ObeliskKind
55+
import os
56+
57+
client_id = os.getenv('CLIENT_ID')
58+
client_secret = os.getenv('CLIENT_SECRET')
59+
60+
# You may want to specify a retry strategy
61+
client = Client(
62+
client=client_id,
63+
secret=client_secret,
64+
kind=ObeliskKind.CLASSIC # or HFS, as you wish
65+
)
66+
67+
data = client.query(
68+
datasets=["some-dataset"],
69+
metrics=["heart-rate::number"],
70+
from_timestamp=42069180,
71+
filter_={
72+
"source": {
73+
"_startsWith": "user123"
74+
}
75+
}
76+
)
77+
```
78+
79+
## Changelog
80+
.. include:: ../../CHANGELOG.rst
2081
"""

src/obelisk/asynchronous/client.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class Obelisk(BaseClient):
1818
Component that contains all the logic to consume data from
1919
the Obelisk API (e.g. historical data, sse).
2020
21+
For most usecases, `query` will be the method you need.
22+
Have a look at `query_time_chunked` too, because it might just be very useful.
23+
2124
Obelisk API Documentation:
2225
https://obelisk.docs.apiary.io/
2326
"""
@@ -42,34 +45,34 @@ async def fetch_single_chunk(
4245
Parameters
4346
----------
4447
45-
datasets : List[str]
48+
- datasets:
4649
List of Dataset IDs.
47-
metrics : Optional[List[str]] = None
50+
- metrics:
4851
List of Metric IDs or wildcards (e.g. `*::number`), defaults to all metrics.
49-
fields : Optional[List[str]] = None
52+
- fields:
5053
List of fields to return in the result set.
5154
Defaults to `[metric, source, value]`
52-
from_timestamp : Optional[int] = None
55+
- from_timestamp:
5356
Limit output to events after (and including)
5457
this UTC millisecond timestamp, if present.
55-
to_timestamp : Optional[int] = None
58+
- to_timestamp:
5659
Limit output to events before (and excluding)
5760
this UTC millisecond timestamp, if present.
58-
order_by : Optional[dict] = None
61+
- order_by:
5962
Specifies the ordering of the output,
6063
defaults to ascending by timestamp.
6164
See Obelisk docs for format. Caller is responsible for validity.
62-
filter_ : Optional[dict] = None
65+
- filter_:
6366
Limit output to events matching the specified Filter expression.
6467
See Obelisk docs, caller is responsible for validity.
65-
limit : Optional[int] = None
68+
- limit:
6669
Limit output to a maximum number of events.
6770
Also determines the page size.
6871
Default is server-determined, usually 2500.
69-
limit_by : Optional[dict] = None
72+
- limit_by:
7073
Limit the combination of a specific set of Index fields
7174
to a specified maximum number.
72-
cursor : Optional[str] = None
75+
- cursor:
7376
Specifies the next cursor,
7477
used when paging through large result sets.
7578
"""
@@ -129,31 +132,31 @@ async def query(
129132
Parameters
130133
----------
131134
132-
datasets : List[str]
135+
- datasets:
133136
List of Dataset IDs.
134-
metrics : Optional[List[str]] = None
137+
- metrics:
135138
List of Metric IDs or wildcards (e.g. `*::number`), defaults to all metrics.
136-
fields : Optional[List[str]] = None
139+
- fields:
137140
List of fields to return in the result set.
138141
Defaults to `[metric, source, value]`
139-
from_timestamp : Optional[int] = None
142+
- from_timestamp:
140143
Limit output to events after (and including)
141144
this UTC millisecond timestamp, if present.
142-
to_timestamp : Optional[int] = None
145+
- to_timestamp:
143146
Limit output to events before (and excluding)
144147
this UTC millisecond timestamp, if present.
145-
order_by : Optional[dict] = None
148+
- order_by:
146149
Specifies the ordering of the output,
147150
defaults to ascending by timestamp.
148151
See Obelisk docs for format. Caller is responsible for validity.
149-
filter_ : Optional[dict] = None
152+
- filter_:
150153
Limit output to events matching the specified Filter expression.
151154
See Obelisk docs, caller is responsible for validity.
152-
limit : Optional[int] = None
155+
- limit:
153156
Limit output to a maximum number of events.
154157
Also determines the page size.
155158
Default is server-determined, usually 2500.
156-
limit_by : Optional[dict] = None
159+
- limit_by:
157160
Limit the combination of a specific set of Index fields
158161
to a specified maximum number.
159162
"""
@@ -209,19 +212,19 @@ async def query_time_chunked(
209212
Parameters
210213
----------
211214
212-
datasets : List[str]
215+
- datasets:
213216
Dataset IDs to query from
214-
metrics : List[str]
217+
- metrics:
215218
IDs of metrics to query
216-
from_time : `datetime.datetime`
219+
- from_time:
217220
Start time to fetch from
218-
to_time : `datetime.datetime`
221+
- to_time:
219222
End time to fetch until.
220-
jump : `datetime.timedelta`
223+
- jump:
221224
Size of one yielded chunk
222-
filter_ : Optional[dict] = None
225+
- filter_:
223226
Obelisk filter, caller is responsible for correct format
224-
direction : Literal['asc', 'desc'] = 'asc'
227+
- direction:
225228
Yield older data or newer data first, defaults to older first.
226229
"""
227230

@@ -249,23 +252,23 @@ async def send(
249252
250253
Parameters
251254
----------
252-
dataset : str
255+
- dataset:
253256
ID for the dataset to publish to
254-
data : List[dict]
257+
- data:
255258
List of Obelisk-acceptable datapoints.
256259
Exact format varies between Classic or HFS,
257260
caller is responsible for formatting.
258-
precision : :class:`~obelisk.types.TimestampPrecision` = TimestampPrecision.MILLISECONDS
261+
- precision:
259262
Precision used in the numeric timestamps contained in data.
260263
Ensure it matches to avoid weird errors.
261-
mode : :class:`~obelisk.types.IngestMode` = IngestMode.DEFAULT
262-
See docs for :class:`~obelisk.types.IngestMode`.
264+
- mode:
265+
See docs for `obelisk.types.IngestMode`.
263266
264267
Raises
265268
------
266269
267270
ObeliskError
268-
When the resulting status code is not 204, an empty :exc:`~obelisk.exceptions.ObeliskError` is raised.
271+
When the resulting status code is not 204, an empty `obelisk.exceptions.ObeliskError` is raised.
269272
"""
270273

271274
params = {

src/obelisk/asynchronous/core.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
22
This module contains the asynchronous API to interface with Obelisk CORE.
3-
These methods all return a :class:`Awaitable`.
3+
These methods all return a `Awaitable`.
44
5-
Relevant entrance points are :class:`Client`.
5+
Relevant entrance points are `Client`.
66
77
This API vaguely resembles that of clients to previous Obelisk versions,
88
but also significantly diverts from it where the underlying Obelisk CORE API does so.
@@ -87,7 +87,7 @@ class ObeliskPosition(BaseModel):
8787

8888
class IncomingDatapoint(BaseModel):
8989
"""A datapoint to be submitted to Obelisk. These are validated quite extensively, but not fully.
90-
.. automethod:: check_metric_type(self)
90+
We check roughly if the value type corresponds to the declared type if its one of `number`, `number[]`, `bool` or `string`.
9191
"""
9292

9393
timestamp: AwareDatetime | None = None
@@ -133,20 +133,29 @@ def serialize_comma_string(input: Any, handler: SerializerFunctionWrapHandler) -
133133

134134

135135
class QueryParams(BaseModel):
136+
"""
137+
To avoid having too many parameters on query functions,
138+
and sharing the implementation between query and chunked query,
139+
this model collects the information needed to execute a query.
140+
141+
Contrary to the name, this does not correlate directly to URL query parameters sent to Obelisk.
142+
"""
136143
dataset: str
137144
groupBy: Annotated[list[FieldName] | None, WrapSerializer(serialize_comma_string)] = None
145+
"""List of Field Names to aggregate by as defined in Obelisk docs, None selects the server-side defaults."""
138146
aggregator: Aggregator | None = None
139147
fields: Annotated[list[FieldName] | None, WrapSerializer(serialize_comma_string)] = None
140-
orderBy: Annotated[list[str] | None, WrapSerializer(serialize_comma_string)] = (
141-
None # More complex than just FieldName, can be prefixed with - to invert sort
142-
)
148+
"""List of Field Names as defined in Obelisk docs, None selects the server-side defaults."""
149+
orderBy: Annotated[list[str] | None, WrapSerializer(serialize_comma_string)] = None
150+
"""List of Field Names, with their potential prefixes and suffixes, to select ordering. None user server defaults."""
143151
dataType: DataType | None = None
152+
"""Data type expected to be returned, is mandatory if the `value` field is requested in the `fields` parameter"""
144153
filter_: Annotated[str | Filter | None, Field(serialization_alias="filter")] = None
145154
"""
146-
Obelisk CORE handles filtering in `RSQL format <https://obelisk.pages.ilabt.imec.be/obelisk-core/query.html#rsql-format>`__ ,
147-
to make it easier to also programatically write these filters, we provide the :class:`Filter` option as well.
155+
Obelisk CORE handles filtering in [RSQL format](https://obelisk.pages.ilabt.imec.be/obelisk-core/query.html#rsql-format),
156+
to make it easier to also programatically write these filters, we provide the `obelisk.types.core.Filter` option as well.
148157
149-
Suffix to avoid collisions.
158+
Suffix to avoid collisions with builtin Python filter function.
150159
"""
151160
cursor: str | None = None
152161
limit: int = 1000
@@ -161,10 +170,16 @@ def check_datatype_needed(self) -> Self:
161170
return self
162171

163172
def to_dict(self) -> dict[str, Any]:
164-
return self.model_dump(exclude_none=True, by_alias=True, mode='json')
173+
return self.model_dump(exclude_none=True, by_alias=True, mode='json', exclude={"dataset"})
165174

166175

167176
class ChunkedParams(BaseModel):
177+
"""
178+
The parameters to be used with `Client.query_time_chunked`,
179+
which allows fetching large spans of data in specified "chunks" specified in time units,
180+
for example processing weeks of data one hour at a time.
181+
This limits memory useage.
182+
"""
168183
dataset: str
169184
groupBy: list[FieldName] | None = None
170185
aggregator: Aggregator | None = None
@@ -178,6 +193,7 @@ class ChunkedParams(BaseModel):
178193
start: datetime
179194
end: datetime
180195
jump: timedelta = timedelta(hours=1)
196+
"""The size of one chunk. 1 hour is a common default. You will receive however many datapoints are included in this interval."""
181197

182198
model_config = ConfigDict(arbitrary_types_allowed=True)
183199

@@ -189,6 +205,7 @@ def check_datatype_needed(self) -> Self:
189205
return self
190206

191207
def chunks(self) -> Iterator[QueryParams]:
208+
"""Splits this model into an Iterator of ordinary `QueryParams` objects, to query one timestep at a time."""
192209
current_start = self.start
193210
while current_start < self.end:
194211
current_end = current_start + self.jump
@@ -210,19 +227,36 @@ def chunks(self) -> Iterator[QueryParams]:
210227

211228

212229
class QueryResult(BaseModel):
230+
"""The data returned by a single chunk fetch"""
213231
cursor: str | None = None
232+
"""Cursors always point to the next page of data matched by filters.
233+
They are none if there is no more data, they do not consider datapoint count limits."""
214234
items: list[Datapoint]
215235

216236

217237
class Client(BaseClient):
238+
"""
239+
This class performs all communication with Obelisk.
240+
241+
The intended methods to be used by consumers are `query` or `query_time_chunked`.
242+
These will respectively return all data matching specified parameters,
243+
or return all data, one timestep at a time respectively.
244+
245+
`send` is considered an implementation detail,
246+
but may be used by consumers for any endpoints not yet implemented by obelisk-py.
247+
248+
`fetch_single_chunk` is the underlying layer to both query methods and requires the user to handle cursors themselves.
249+
It may however still be useful in some circumstances.
250+
"""
251+
218252
page_limit: int = 250
219253
"""How many datapoints to request per page in a cursored fetch"""
220254

221255
def __init__(
222256
self,
223257
client: str,
224258
secret: str,
225-
retry_strategy: RetryStrategy = NoRetryStrategy(), # noqa: B008 # This is fine to bew shared
259+
retry_strategy: RetryStrategy = NoRetryStrategy(), # noqa: B008 # This is fine to be shared
226260
) -> None:
227261
BaseClient.__init__(
228262
self,
@@ -242,18 +276,18 @@ async def send(
242276
243277
Parameters
244278
----------
245-
dataset : str
279+
- dataset
246280
ID for the dataset to publish to
247-
data : List[IncomingDatapoint]
281+
- data
248282
List of Obelisk-acceptable datapoints.
249283
Exact format varies between Classic or HFS,
250284
caller is responsible for formatting.
251285
252286
Raises
253287
------
254288
255-
ObeliskError
256-
When the resulting status code is not 204, an :exc:`~obelisk.exceptions.ObeliskError` is raised.
289+
- ObeliskError
290+
When the resulting status code is not 204, an `obelisk.exceptions.ObeliskError` is raised.
257291
"""
258292

259293
response = await self.http_post(

0 commit comments

Comments
 (0)