Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/obelisk/asynchronous/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
from obelisk.types.core import IngestMode


DataType = Literal["number", "number[]", "json", "bool", "string", "integer", "integer[]"]
DataType = Literal[
"number", "number[]", "json", "bool", "string", "integer", "integer[]"
]
"""The possible types of data Obelisk can accept"""


Expand Down Expand Up @@ -168,7 +170,12 @@ class QueryParams(BaseModel):
"""List of Field Names, with their potential prefixes and suffixes, to select ordering. None user server defaults."""
dataType: DataType | None = None
"""Data type expected to be returned, is mandatory if the `value` field is requested in the `fields` parameter"""
filter_: Annotated[str | Filter | None, Field(serialization_alias="filter",)] = None
filter_: Annotated[
str | Filter | None,
Field(
serialization_alias="filter",
),
] = None
"""
Obelisk CORE handles filtering in [RSQL format](https://obelisk.pages.ilabt.imec.be/obelisk-core/query.html#rsql-format),
to make it easier to also programatically write these filters, we provide the `obelisk.types.core.Filter` option as well.
Expand All @@ -192,7 +199,7 @@ def to_dict(self) -> dict[str, Any]:
exclude_none=True, by_alias=True, mode="json", exclude={"dataset"}
)

@field_serializer('filter_', mode='plain')
@field_serializer("filter_", mode="plain")
def serialize_filter(self, value: Filter | str | None) -> str | None:
if value is None or isinstance(value, str):
return value
Expand Down Expand Up @@ -323,7 +330,7 @@ async def send(
response = await self.http_post(
f"{self.kind.root_url}/{dataset}/data/ingest",
data=[x.model_dump(mode="json") for x in data],
params={"mode": ingest_mode.value}
params={"mode": ingest_mode.value},
)
if response.status_code != 204:
msg = f"An error occured during data ingest. Status {response.status_code}, message: {response.text}"
Expand Down
12 changes: 9 additions & 3 deletions src/obelisk/types/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
To create a filter, look at `Filter`.
Example:

>>> from datetime import datetime
>>> from datetime import datetime, timezone
>>> f = (Filter().add_and(
... Comparison.equal('source', 'test source'),
... Comparison.is_in('metricType', ['number', 'number[]']),
... ).add_or(
... Comparison.less('timestamp', datetime.fromtimestamp(1757422128))
... Comparison.less('timestamp', datetime.fromtimestamp(1757422128).astimezone(timezone.utc))
... ))
>>> print(f)
(('source'=='test source';'metricType'=in=('number', 'number[]')),'timestamp'<'2025-09-09T14:48:48')
(('source'=='test source';'metricType'=in=('number', 'number[]')),'timestamp'<'2025-09-09T12:48:48+00:00')
"""

from __future__ import annotations
from abc import ABC
from datetime import datetime

from typing import Any
from collections.abc import Iterable
from enum import Enum
Expand Down Expand Up @@ -55,6 +56,8 @@ class Comparison:
When serializing to RSQL format,
each argument is single quoted as to accept any otherwise reserved characters,
and serialised using :func:`str`.

If the right-hand value is a datetime, it _must_ be timezone-aware.
"""

left: FieldName
Expand All @@ -77,6 +80,8 @@ def __str__(self) -> str:
def _sstr(item: Any) -> str:
"""Smart string conversion"""
if isinstance(item, datetime):
if item.tzinfo is None:
raise ValueError(f"Right side of comparison is naive datetime: {item}")
return item.isoformat()
return str(item)

Expand Down Expand Up @@ -204,6 +209,7 @@ def add_or(self, *other: Item) -> Filter:

class IngestMode(str, Enum):
"""Whether the ingested datapoints should be streamed, stored, or both (the default)"""

BOTH = "DEFAULT"
STREAM = "STREAM_ONLY"
STORE = "STORE_ONLY"
11 changes: 9 additions & 2 deletions src/tests/typetest/filter_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from obelisk.types.core import Filter, Comparison
from datetime import datetime
from datetime import datetime, timezone
import pytest


def test_basic_filter():
test_dt = datetime.now()
test_dt = datetime.now(tz=timezone.utc)
f = (
Filter()
.add_and(
Expand All @@ -17,3 +18,9 @@ def test_basic_filter():

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


def test_naive_datetime():
with pytest.raises(ValueError):
c = Comparison.less("timestamp", datetime.fromtimestamp(1879282))
print(str(c))
Loading