Skip to content

Commit

Permalink
expr: Convert date time accessors to properties (#571)
Browse files Browse the repository at this point in the history
  • Loading branch information
aditya-nambiar authored Sep 24, 2024
1 parent 195bdb8 commit 503fd1e
Show file tree
Hide file tree
Showing 24 changed files with 97 additions and 52 deletions.
26 changes: 13 additions & 13 deletions docs/examples/api-reference/expressions/dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def test_year():
from fennel.expr import col

# docsnip-highlight next-line
expr = col("x").dt.year()
expr = col("x").dt.year

# year works for any datetime type or optional datetime type
assert expr.typeof(schema={"x": datetime}) == int
Expand All @@ -30,7 +30,7 @@ def test_year():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.year(timezone="US/Eastern")
expr = col("x").dt.with_tz(timezone="US/Eastern").year
assert expr.eval(df, schema=schema).tolist() == [2023, 2024, 2024]
# /docsnip

Expand All @@ -40,7 +40,7 @@ def test_month():
from fennel.expr import col

# docsnip-highlight next-line
expr = col("x").dt.month()
expr = col("x").dt.month

# month works for any datetime type or optional datetime type
assert expr.typeof(schema={"x": datetime}) == int
Expand All @@ -61,7 +61,7 @@ def test_month():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.month(timezone="US/Eastern")
expr = col("x").dt.with_tz(timezone="US/Eastern").month
assert expr.eval(df, schema=schema).tolist() == [12, 1, 1]
# /docsnip

Expand Down Expand Up @@ -94,7 +94,7 @@ def test_day():
from fennel.expr import col

# docsnip-highlight next-line
expr = col("x").dt.day()
expr = col("x").dt.day

# day works for any datetime type or optional datetime type
assert expr.typeof(schema={"x": datetime}) == int
Expand All @@ -115,7 +115,7 @@ def test_day():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.day(timezone="US/Eastern")
expr = col("x").dt.with_tz(timezone="US/Eastern").day
assert expr.eval(df, schema=schema).tolist() == [31, 1, 1]
# /docsnip

Expand All @@ -125,7 +125,7 @@ def test_hour():
from fennel.expr import col

# docsnip-highlight next-line
expr = col("x").dt.hour()
expr = col("x").dt.hour

# hour works for any datetime type or optional datetime type
assert expr.typeof(schema={"x": datetime}) == int
Expand All @@ -146,7 +146,7 @@ def test_hour():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.hour(timezone="US/Eastern")
expr = col("x").dt.with_tz(timezone="US/Eastern").hour
assert expr.eval(df, schema=schema).tolist() == [19, 5, 15]
# /docsnip

Expand All @@ -156,7 +156,7 @@ def test_minute():
from fennel.expr import col

# docsnip-highlight next-line
expr = col("x").dt.minute()
expr = col("x").dt.minute

# minute works for any datetime type or optional datetime type
assert expr.typeof(schema={"x": datetime}) == int
Expand All @@ -177,7 +177,7 @@ def test_minute():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.minute(timezone="US/Eastern")
expr = col("x").dt.with_tz(timezone="US/Eastern").minute
assert expr.eval(df, schema=schema).tolist() == [0, 0, 20]
# /docsnip

Expand All @@ -187,7 +187,7 @@ def test_second():
from fennel.expr import col

# docsnip-highlight next-line
expr = col("x").dt.second()
expr = col("x").dt.second

# second works for any datetime type or optional datetime type
assert expr.typeof(schema={"x": datetime}) == int
Expand All @@ -208,7 +208,7 @@ def test_second():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.second(timezone="Asia/Kathmandu")
expr = col("x").dt.with_tz(timezone="Asia/Kathmandu").second
assert expr.eval(df, schema=schema).tolist() == [1, 2, 3]
# /docsnip

Expand Down Expand Up @@ -374,7 +374,7 @@ def test_strftime():

# also works with timezone aware datetimes
# docsnip-highlight next-line
expr = col("x").dt.strftime("%Y-%m-%d", timezone="US/Eastern")
expr = col("x").dt.with_tz(timezone="US/Eastern").strftime("%Y-%m-%d")
assert expr.eval(df, schema=schema).tolist() == [
"2023-12-31",
"2024-01-02",
Expand Down
3 changes: 3 additions & 0 deletions fennel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [1.5.30] - 2024-09-24
- Add hour, minute, second, millisecond, microsecond, day, week accessors as properties instead of methods.

## [1.5.29] - 2024-09-22
- Enable support for complex literals (e.g. list literals or struct literals)

Expand Down
34 changes: 34 additions & 0 deletions fennel/client_tests/test_featureset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import unittest
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, List
from fennel.lib import meta

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -1229,3 +1230,36 @@ class IndexFeatures:
message="third_commit",
)
assert response.status_code == requests.codes.OK, response.json()


@mock
def test_query_time_features(client):
@meta(owner="decision-systems@theporter.in")
@featureset
class QueryTimeFeatures:
query_time: datetime
hour_utc: int = F(col("query_time").dt.hour)
bucket_3hour_utc: int = F((col("hour_utc") / lit(3)).floor() + lit(1))
bucket_6hour_utc: int = F((col("hour_utc") / lit(6)).floor() + lit(1))
day_utc: int = F(col("query_time").dt.day)
day_of_week_utc: int
week_of_month_utc: int = F((col("day_utc") / lit(7)).ceil())
week_bucket_utc: int

@extractor(version=1)
@outputs("query_time")
def current_time(cls, ts: pd.Series):
return pd.Series(name="query_time", data=ts)

@extractor(version=8)
@outputs(
"day_of_week_utc",
"week_bucket_utc",
)
def time_feature_extractor(cls, ts: pd.Series) -> pd.DataFrame:
pass

client.commit(
featuresets=[QueryTimeFeatures],
message="first_commit",
)
60 changes: 43 additions & 17 deletions fennel/expr/expr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from enum import Enum
import copy
import json
from dataclasses import dataclass
from typing import Any, Callable, Dict, Type, Optional
Expand Down Expand Up @@ -747,9 +748,12 @@ class DateTimeLiteral(Expr):


class _DateTime(Expr):
def __init__(self, expr: Expr, op: DateTimeOp):
def __init__(
self, expr: Expr, op: DateTimeOp, timezone: Optional[str] = "UTC"
):
self.op = op
self.operand = expr
self.timezone = timezone
super(_DateTime, self).__init__()

def parts(self, part: TimeUnit, timezone: Optional[str] = "UTC") -> _Number:
Expand All @@ -771,32 +775,54 @@ def since_epoch(self, unit: str = "second") -> _Number:
_DateTime(self, DateTimeSinceEpoch(time_unit)), MathNoop()
)

def strftime(self, format: str, timezone: Optional[str] = "UTC") -> _String:
def strftime(self, format: str) -> _String:
return _String(
_DateTime(self, DateTimeStrftime(format=format, timezone=timezone)),
_DateTime(
self, DateTimeStrftime(format=format, timezone=self.timezone)
),
StringNoop(),
)

def year(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.YEAR, timezone)
def with_tz(self, timezone: str) -> _DateTime:
new_dt = copy.deepcopy(self)
new_dt.timezone = timezone
return new_dt

def month(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.MONTH, timezone)
@property
def microsecond(self) -> _Number:
return self.parts(TimeUnit.MICROSECOND, self.timezone)

def week(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.WEEK, timezone)
@property
def millisecond(self) -> _Number:
return self.parts(TimeUnit.MILLISECOND, self.timezone)

def day(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.DAY, timezone)
@property
def second(self) -> _Number:
return self.parts(TimeUnit.SECOND, self.timezone)

def hour(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.HOUR, timezone)
@property
def minute(self) -> _Number:
return self.parts(TimeUnit.MINUTE, self.timezone)

def minute(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.MINUTE, timezone)
@property
def hour(self) -> _Number:
return self.parts(TimeUnit.HOUR, self.timezone)

def second(self, timezone: Optional[str] = "UTC") -> _Number:
return self.parts(TimeUnit.SECOND, timezone)
@property
def day(self) -> _Number:
return self.parts(TimeUnit.DAY, self.timezone)

@property
def week(self) -> _Number:
return self.parts(TimeUnit.WEEK, self.timezone)

@property
def month(self) -> _Number:
return self.parts(TimeUnit.MONTH, self.timezone)

@property
def year(self) -> _Number:
return self.parts(TimeUnit.YEAR, self.timezone)


#########################################################
Expand Down
6 changes: 3 additions & 3 deletions fennel/expr/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ def test_datetime():
cases = [
# Extract year from a datetime
ExprTestCase(
expr=(col("a").dt.year()),
expr=(col("a").dt.year),
df=pd.DataFrame(
{
"a": [
Expand All @@ -925,7 +925,7 @@ def test_datetime():
),
# Extract month from a datetime
ExprTestCase(
expr=(col("a").dt.month()),
expr=(col("a").dt.month),
df=pd.DataFrame(
{
"a": [
Expand All @@ -944,7 +944,7 @@ def test_datetime():
),
# Extract week from a datetime
ExprTestCase(
expr=(col("a").dt.week()),
expr=(col("a").dt.week),
df=pd.DataFrame(
{
"a": [
Expand Down
1 change: 0 additions & 1 deletion fennel/gen/auth_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/connector_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/dataset_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/expectations_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/expr_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/featureset_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/format_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/http_auth_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/index_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/kinesis_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/metadata_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/pycode_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion fennel/gen/schema_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 503fd1e

Please sign in to comment.