Skip to content

Commit dc1809b

Browse files
committed
🦺 Enhance CubeJS model with enums and improved type safety
- Add Granularity and FilterOperators enums for better type checking and developer experience. - Extend model classes with additional fields like compare_date_range, logical operators for filters, and offset parameter for pagination. - Make values field optional in Filter class to support operators that don't require values.
1 parent 80c3489 commit dc1809b

4 files changed

Lines changed: 465 additions & 19 deletions

File tree

cubejs/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
CubeJSRequest,
88
CubeJSResponse,
99
Filter,
10+
FilterOperators,
11+
Granularity,
12+
LogicalOperator,
13+
OrderBy,
1014
TimeDimension,
1115
)
1216

@@ -18,4 +22,8 @@
1822
"CubeJSResponse",
1923
"TimeDimension",
2024
"Filter",
25+
"OrderBy",
26+
"Granularity",
27+
"FilterOperators",
28+
"LogicalOperator",
2129
]

cubejs/model.py

Lines changed: 164 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,133 @@
11
"""Data model."""
22

3-
from pydantic import BaseModel, Field
3+
from enum import Enum
4+
5+
from pydantic import BaseModel, Field, model_validator
6+
7+
8+
class OrderBy(str, Enum):
9+
"""CubeJS order by available options.
10+
11+
If the order property is not specified in the query, Cube sorts results by default:
12+
1. First time dimension with granularity (ascending)
13+
2. If no time dimension exists, first measure (descending)
14+
3. If no measure exists, first dimension (ascending)
15+
16+
The order can be specified either as a dict mapping fields to ASC/DESC,
17+
or as an array of tuples for controlling the ordering sequence.
18+
"""
19+
20+
ASC = "asc"
21+
DESC = "desc"
22+
23+
24+
class Granularity(str, Enum):
25+
"""CubeJS granularity available for time dimensions.
26+
27+
Time-based properties are modeled using dimensions of the time type. They allow
28+
grouping the result set by a unit of time (e.g., days, weeks, month, etc.), also
29+
known as the time dimension granularity.
30+
31+
The following granularities are available by default for any time dimension
32+
"""
33+
34+
YEAR = "year"
35+
QUARTER = "quarter"
36+
MONTH = "month"
37+
WEEK = "week"
38+
DAY = "day"
39+
HOUR = "hour"
40+
MINUTE = "minute"
41+
SECOND = "second"
42+
43+
44+
class FilterOperators(str, Enum):
45+
"""CubeJS available filter operations.
46+
47+
Different operators are available depending on whether they're applied to measures
48+
or dimensions, and for dimensions, the available operators depend on the dimension
49+
type.
50+
51+
Operators for measures:
52+
- equals, notEquals: Exact match or its opposite. Supports multiple values.
53+
- gt, gte, lt, lte: Greater than, greater than or equal, less than,
54+
less than or equal.
55+
- set, notSet: Checks if value is not NULL or is NULL respectively.
56+
- measureFilter: Applies an existing measure's filters to the current query.
57+
58+
Operators for dimensions (availability depends on dimension type):
59+
- string: equals, notEquals, contains, notContains, startsWith, notStartsWith,
60+
endsWith, notEndsWith, set, notSet
61+
- number: equals, notEquals, gt, gte, lt, lte, set, notSet
62+
- time: equals, notEquals, inDateRange, notInDateRange, beforeDate, afterDate,
63+
set, notSet
64+
"""
65+
66+
EQUALS = "equals"
67+
NOT_EQUALS = "notEquals"
68+
CONTAINS = "contains"
69+
NOT_CONTAINS = "notContains"
70+
STARTS_WITH = "startsWith"
71+
NOT_STARTS_WITH = "notStartsWith"
72+
ENDS_WITH = "endsWith"
73+
NOT_ENDS_WITH = "notEndsWith"
74+
GREATER_THAN = "gt"
75+
GREATER_THAN_OR_EQUAL = "gte"
76+
LESS_THAN = "lt"
77+
LESS_THAN_OR_EQUAL = "lte"
78+
SET = "set"
79+
NOT_SET = "notSet"
80+
IN_DATE_RANGE = "inDateRange"
81+
NOT_IN_DATE_RANGE = "notInDateRange"
82+
BEFORE_DATE = "beforeDate"
83+
AFTER_DATE = "afterDate"
84+
MEASURE_FILTER = "measureFilter"
485

586

687
class TimeDimension(BaseModel):
7-
"""Time dimension section of a cubejs request.
88+
"""Time dimension filters and grouping.
889
9-
Args:
10-
dimension: column name to use as time reference.
11-
granularity: granularity to transform the timestamp.
12-
date_range: date range to filter the query.
90+
Provides a convenient shortcut to pass a dimension and filter as a TimeDimension.
1391
92+
Args:
93+
dimension: Time dimension name to use for filtering and/or grouping.
94+
granularity: A granularity for the time dimension. Can be one of the default
95+
granularities (e.g., year, week, day) or a custom granularity. If not
96+
provided, Cube will only filter by the time dimension without grouping.
97+
date_range: Date range for filtering. Can be:
98+
- An array of dates in YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.SSS format
99+
- A single date (equivalent to passing two identical dates)
100+
- A string with a relative date range (e.g., "last quarter")
101+
Values should be local and in query timezone. YYYY-MM-DD dates are padded
102+
to start/end of day when used as range boundaries.
103+
compare_date_range: An array of date ranges to compare measure values across
104+
different time periods.
14105
"""
15106

16107
dimension: str
17-
granularity: str | None = None
108+
granularity: Granularity | None = None
18109
date_range: list[str] | str | None = Field(
19110
default=None, serialization_alias="dateRange"
20111
)
112+
compare_date_range: list[list[str] | str] | None = Field(
113+
default=None, serialization_alias="compareDateRange"
114+
)
115+
116+
@model_validator(mode="after")
117+
def validate_date_ranges(self) -> "TimeDimension":
118+
"""Validate date range configurations."""
119+
if self.date_range is not None and self.compare_date_range is not None:
120+
raise ValueError("Cannot provide both date_range and compare_date_range")
121+
122+
if self.compare_date_range is not None:
123+
for date_range in self.compare_date_range:
124+
if isinstance(date_range, list) and len(date_range) != 2:
125+
raise ValueError(
126+
"Each compare_date_range entry must contain exactly 2 "
127+
"dates when provided"
128+
)
129+
130+
return self
21131

22132
class Config: # noqa: D106
23133
exclude_none = True
@@ -27,16 +137,52 @@ class Config: # noqa: D106
27137
class Filter(BaseModel):
28138
"""Filter section of a cubejs request.
29139
30-
Args:
31-
member: member to filter by.
32-
operator: operator to apply.
33-
values: values to filter by.
140+
Filters can be applied to dimensions or measures:
141+
- When filtering dimensions, raw data is restricted before calculations
142+
- When filtering measures, results are restricted after measure calculation
34143
144+
Args:
145+
member: Dimension or measure to filter by (e.g., "stories.isDraft").
146+
operator: Operator to apply to the filter. Available operators depend on
147+
whether filtering a dimension or measure, and the type of dimension.
148+
See FilterOperators for available options.
149+
values: Array of values for the filter. Values must be strings.
150+
For dates, use YYYY-MM-DD format. Optional for some operators
151+
like 'set' and 'notSet'.
35152
"""
36153

37154
member: str
38155
operator: str
39-
values: list[str]
156+
values: list[str] | None = None
157+
158+
159+
class LogicalOperator(BaseModel):
160+
"""Logical operator for combining filters.
161+
162+
Allows combining multiple filters with boolean logic. You can use either 'or_' or
163+
'and_' to create complex filter conditions.
164+
165+
Note:
166+
- You cannot mix dimension and measure filters in the same logical operator
167+
- Dimension filters apply to raw data (WHERE clause in SQL)
168+
- Measure filters apply to aggregated data (HAVING clause in SQL)
169+
170+
Args:
171+
or_: List of filters or other logical operators to combine with OR.
172+
and_: List of filters or other logical operators to combine with AND.
173+
"""
174+
175+
or_: list["FilterOrLogical"] | None = Field(default=None, serialization_alias="or")
176+
and_: list["FilterOrLogical"] | None = Field(
177+
default=None, serialization_alias="and"
178+
)
179+
180+
class Config: # noqa: D106
181+
exclude_none = True
182+
populate_by_name = True
183+
184+
185+
FilterOrLogical = Filter | LogicalOperator
40186

41187

42188
class CubeJSRequest(BaseModel):
@@ -47,21 +193,23 @@ class CubeJSRequest(BaseModel):
47193
time_dimensions: time dimensions to aggregate measures by.
48194
dimensions: dimensions to group by.
49195
segments: segments to filter by.
50-
filters: other filters to apply.
196+
filters: other filters to apply (can include logical operators).
51197
order: order records in response by.
52198
limit: limit the number of records in response.
199+
offset: number of records to skip in response.
53200
54201
"""
55202

56-
measures: list[str]
203+
measures: list[str] = Field(default_factory=list)
57204
time_dimensions: list[TimeDimension] | None = Field(
58205
serialization_alias="timeDimensions", default=None
59206
)
60207
dimensions: list[str] | None = None
61208
segments: list[str] | None = None
62-
filters: list[Filter] | None = None
63-
order: dict[str, str] | None = None
209+
filters: list[FilterOrLogical] = Field(default_factory=list)
210+
order: dict[str, OrderBy] | None = None
64211
limit: int | None = None
212+
offset: int | None = None
65213

66214

67215
class CubeJSAuth(BaseModel):

tests/test_client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_measures,
1313
)
1414
from cubejs.client import _error_handler
15+
from cubejs.model import FilterOperators, Granularity, OrderBy
1516

1617

1718
@pytest.mark.asyncio
@@ -49,19 +50,19 @@ async def test_get_metrics(httpx_mock):
4950
time_dimensions=[
5051
TimeDimension(
5152
dimension="orders.created_at",
52-
granularity="day",
53+
granularity=Granularity.DAY,
5354
date_range="last 30 days",
5455
)
5556
],
5657
dimensions=["orders.status"],
5758
filters=[
5859
Filter(
5960
member="orders.status",
60-
operator="equals",
61+
operator=FilterOperators.EQUALS,
6162
values=["completed", "processing"],
6263
)
6364
],
64-
order={"orders.count": "desc"},
65+
order={"orders.count": OrderBy.DESC},
6566
),
6667
)
6768

0 commit comments

Comments
 (0)