diff --git a/docs/examples/api-reference/expressions/dt.py b/docs/examples/api-reference/expressions/dt.py index 206e9d0b..b93a97b9 100644 --- a/docs/examples/api-reference/expressions/dt.py +++ b/docs/examples/api-reference/expressions/dt.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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", diff --git a/fennel/CHANGELOG.md b/fennel/CHANGELOG.md index fc861aa1..703820a8 100644 --- a/fennel/CHANGELOG.md +++ b/fennel/CHANGELOG.md @@ -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) diff --git a/fennel/client_tests/test_featureset.py b/fennel/client_tests/test_featureset.py index 3ae09914..8d52dffd 100644 --- a/fennel/client_tests/test_featureset.py +++ b/fennel/client_tests/test_featureset.py @@ -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 @@ -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", + ) diff --git a/fennel/expr/expr.py b/fennel/expr/expr.py index 03cb25c4..5e2c8810 100644 --- a/fennel/expr/expr.py +++ b/fennel/expr/expr.py @@ -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 @@ -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: @@ -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) ######################################################### diff --git a/fennel/expr/test_expr.py b/fennel/expr/test_expr.py index 8ae32697..2be31827 100644 --- a/fennel/expr/test_expr.py +++ b/fennel/expr/test_expr.py @@ -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": [ @@ -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": [ @@ -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": [ diff --git a/fennel/gen/auth_pb2.py b/fennel/gen/auth_pb2.py index c51bec9c..5fb3361e 100644 --- a/fennel/gen/auth_pb2.py +++ b/fennel/gen/auth_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: auth.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/connector_pb2.py b/fennel/gen/connector_pb2.py index 5e921636..5ae41e6b 100644 --- a/fennel/gen/connector_pb2.py +++ b/fennel/gen/connector_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: connector.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/dataset_pb2.py b/fennel/gen/dataset_pb2.py index e6176a5a..30befc88 100644 --- a/fennel/gen/dataset_pb2.py +++ b/fennel/gen/dataset_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: dataset.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/expectations_pb2.py b/fennel/gen/expectations_pb2.py index a149c87e..67ed2757 100644 --- a/fennel/gen/expectations_pb2.py +++ b/fennel/gen/expectations_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: expectations.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/expr_pb2.py b/fennel/gen/expr_pb2.py index 85172f5c..69df78aa 100644 --- a/fennel/gen/expr_pb2.py +++ b/fennel/gen/expr_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: expr.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/featureset_pb2.py b/fennel/gen/featureset_pb2.py index 4df083d9..20c5a4ba 100644 --- a/fennel/gen/featureset_pb2.py +++ b/fennel/gen/featureset_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: featureset.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/format_pb2.py b/fennel/gen/format_pb2.py index a83bd4cd..5e7459a7 100644 --- a/fennel/gen/format_pb2.py +++ b/fennel/gen/format_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: format.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/http_auth_pb2.py b/fennel/gen/http_auth_pb2.py index 34797b8d..a51d6608 100644 --- a/fennel/gen/http_auth_pb2.py +++ b/fennel/gen/http_auth_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: http_auth.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/index_pb2.py b/fennel/gen/index_pb2.py index d7cfdffb..9e3863f8 100644 --- a/fennel/gen/index_pb2.py +++ b/fennel/gen/index_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: index.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/kinesis_pb2.py b/fennel/gen/kinesis_pb2.py index 37fe3cc6..520ff7fd 100644 --- a/fennel/gen/kinesis_pb2.py +++ b/fennel/gen/kinesis_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: kinesis.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/metadata_pb2.py b/fennel/gen/metadata_pb2.py index 82b9ffc4..d9d355cc 100644 --- a/fennel/gen/metadata_pb2.py +++ b/fennel/gen/metadata_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: metadata.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/pycode_pb2.py b/fennel/gen/pycode_pb2.py index d53c3ede..d17598f7 100644 --- a/fennel/gen/pycode_pb2.py +++ b/fennel/gen/pycode_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: pycode.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/schema_pb2.py b/fennel/gen/schema_pb2.py index 4f56afb6..e8d3e150 100644 --- a/fennel/gen/schema_pb2.py +++ b/fennel/gen/schema_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: schema.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/schema_registry_pb2.py b/fennel/gen/schema_registry_pb2.py index f1248b25..5bfb5b17 100644 --- a/fennel/gen/schema_registry_pb2.py +++ b/fennel/gen/schema_registry_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: schema_registry.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/services_pb2.py b/fennel/gen/services_pb2.py index 0bf595ae..00770b71 100644 --- a/fennel/gen/services_pb2.py +++ b/fennel/gen/services_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: services.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/spec_pb2.py b/fennel/gen/spec_pb2.py index 72220935..b1a8f493 100644 --- a/fennel/gen/spec_pb2.py +++ b/fennel/gen/spec_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: spec.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/status_pb2.py b/fennel/gen/status_pb2.py index f24662be..cdc144a8 100644 --- a/fennel/gen/status_pb2.py +++ b/fennel/gen/status_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: status.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/fennel/gen/window_pb2.py b/fennel/gen/window_pb2.py index 6b51a69d..ac2bb8d5 100644 --- a/fennel/gen/window_pb2.py +++ b/fennel/gen/window_pb2.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: window.proto -# Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/pyproject.toml b/pyproject.toml index e56e9a40..ecef1497 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fennel-ai" -version = "1.5.29" +version = "1.5.30" description = "The modern realtime feature engineering platform" authors = ["Fennel AI "] packages = [{ include = "fennel" }]