# Copyright 2023 Goldman Sachs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from abc import ABCMeta, abstractmethod
from decimal import Decimal as PythonDecimal
from datetime import date, datetime
from pylegend._typing import (
PyLegendSequence,
PyLegendDict,
PyLegendUnion,
PyLegendList,
TYPE_CHECKING,
)
from pylegend.core.sql.metamodel import (
Expression,
QuerySpecification
)
from pylegend.core.language.shared.expression import PyLegendExpression
from pylegend.core.language.shared.literal_expressions import convert_literal_to_literal_expression
from pylegend.core.language.shared.operations.primitive_operation_expressions import (
PyLegendPrimitiveEqualsExpression,
PyLegendPrimitiveNotEqualsExpression,
PyLegendIsEmptyExpression,
PyLegendIsNotEmptyExpression,
PyLegendPrimitiveToStringExpression,
PyLegendInListExpression,
)
from pylegend.core.tds.pandas_api.frames.helpers.series_helper import grammar_method
from pylegend.core.tds.tds_frame import FrameToSqlConfig
from pylegend.core.tds.tds_frame import FrameToPureConfig
if TYPE_CHECKING:
from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
from pylegend.core.language.shared.primitives.string import PyLegendString
__all__: PyLegendSequence[str] = [
"PyLegendPrimitive",
"PyLegendPrimitiveOrPythonPrimitive",
]
class PyLegendPrimitive(metaclass=ABCMeta):
@abstractmethod
def to_sql_expression(
self,
frame_name_to_base_query_map: PyLegendDict[str, QuerySpecification],
config: FrameToSqlConfig
) -> Expression:
pass
@abstractmethod
def to_pure_expression(self, config: FrameToPureConfig) -> str:
pass
[docs]
@grammar_method
def __eq__( # type: ignore
self,
other: "PyLegendUnion[int, float, bool, str, date, datetime, PythonDecimal, PyLegendPrimitive]"
) -> "PyLegendBoolean":
"""
Test element-wise equality (``==``).
Compare this expression against another primitive value or
expression. Returns a boolean expression that is ``True`` where
the values are equal.
Parameters
----------
other : int, float, bool, str, date, datetime, Decimal, or PyLegendPrimitive
The value or expression to compare against.
Returns
-------
PyLegendBoolean
A boolean expression representing the equality test.
Raises
------
TypeError
If *other* is not a supported primitive type.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Filter rows where Ship Name equals a literal
frame[frame["Ship Name"] == "Around the Horn"].head(3).to_pandas()
"""
PyLegendPrimitive.__validate_param_to_be_primitive(other, "Equals (==) parameter")
if isinstance(other, (int, float, bool, str, date, datetime, PythonDecimal)):
other_op = convert_literal_to_literal_expression(other)
else:
other_op = other.value()
from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
return PyLegendBoolean(PyLegendPrimitiveEqualsExpression(self.value(), other_op))
[docs]
@grammar_method
def __ne__( # type: ignore
self,
other: "PyLegendUnion[int, float, bool, str, date, datetime, PythonDecimal, PyLegendPrimitive]"
) -> "PyLegendBoolean":
"""
Test element-wise inequality (``!=``).
Compare this expression against another primitive value or
expression. Returns a boolean expression that is ``True`` where
the values differ.
Parameters
----------
other : int, float, bool, str, date, datetime, Decimal, or PyLegendPrimitive
The value or expression to compare against.
Returns
-------
PyLegendBoolean
A boolean expression representing the inequality test.
Raises
------
TypeError
If *other* is not a supported primitive type.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Filter rows where Ship Name is not a specific value
frame[frame["Ship Name"] != "Ship1"].head(3).to_pandas()
"""
PyLegendPrimitive.__validate_param_to_be_primitive(other, "Not Equals (!=) parameter")
if isinstance(other, (int, float, bool, str, date, datetime, PythonDecimal)):
other_op = convert_literal_to_literal_expression(other)
else:
other_op = other.value()
from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
return PyLegendBoolean(PyLegendPrimitiveNotEqualsExpression(self.value(), other_op))
[docs]
@grammar_method
def is_empty(self) -> "PyLegendBoolean":
"""
Test whether the value is null / empty.
Returns a boolean expression that is ``True`` where the
underlying column value is ``NULL``.
Returns
-------
PyLegendBoolean
``True`` where the value is null.
See Also
--------
is_not_empty : The inverse test.
is_null : Alias for ``is_empty``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Keep only rows where Ship Name is null
frame[frame["Shipped Date"].is_empty()].head(3).to_pandas()
"""
from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
return PyLegendBoolean(PyLegendIsEmptyExpression(self.value()))
[docs]
@grammar_method
def is_null(self) -> "PyLegendBoolean":
"""
Test whether the value is null.
Alias for :meth:`is_empty`.
Returns
-------
PyLegendBoolean
``True`` where the value is null.
See Also
--------
is_empty : Canonical null-check method.
is_not_null : The inverse test.
"""
return self.is_empty()
[docs]
@grammar_method
def is_not_empty(self) -> "PyLegendBoolean":
"""
Test whether the value is not null / not empty.
Returns a boolean expression that is ``True`` where the
underlying column value is not ``NULL``.
Returns
-------
PyLegendBoolean
``True`` where the value is not null.
See Also
--------
is_empty : The inverse test.
is_not_null : Alias for ``is_not_empty``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Keep only rows where Ship Name is present
frame[frame["Ship Name"].is_not_empty()].head(3).to_pandas()
"""
from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
return PyLegendBoolean(PyLegendIsNotEmptyExpression(self.value()))
[docs]
@grammar_method
def is_not_null(self) -> "PyLegendBoolean":
"""
Test whether the value is not null.
Alias for :meth:`is_not_empty`.
Returns
-------
PyLegendBoolean
``True`` where the value is not null.
See Also
--------
is_not_empty : Canonical not-null-check method.
is_null : The inverse test.
"""
return self.is_not_empty()
[docs]
@grammar_method
def to_string(self) -> "PyLegendString":
"""
Convert the value to its string representation.
Returns a string expression produced by applying the database's
``CAST(... AS TEXT)`` (SQL) or ``->toString()`` (Pure).
Returns
-------
PyLegendString
The stringified expression.
See Also
--------
toString : Alias for ``to_string``.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Convert an integer column to a string
frame["order_str"] = frame["Order Id"].to_string()
frame.head(3).to_pandas()
"""
from pylegend.core.language.shared.primitives.string import PyLegendString
return PyLegendString(PyLegendPrimitiveToStringExpression(self.value()))
[docs]
@grammar_method
def toString(self) -> "PyLegendString":
"""
Convert the value to its string representation.
Alias for :meth:`to_string`.
Returns
-------
PyLegendString
The stringified expression.
See Also
--------
to_string : Canonical string-conversion method.
"""
return self.to_string()
[docs]
@grammar_method
def in_list(
self,
lst: "PyLegendList[PyLegendUnion[int, float, bool, str, date, datetime, PythonDecimal, PyLegendPrimitive]]"
) -> "PyLegendBoolean":
"""
Test whether the value is contained in a list of values.
Returns a boolean expression equivalent to SQL ``IN (...)``.
Parameters
----------
lst : list of int, float, bool, str, date, datetime, Decimal, or PyLegendPrimitive
A **non-empty** list of literal values or expressions to
check membership against.
Returns
-------
PyLegendBoolean
``True`` where the value matches any element in *lst*.
Raises
------
ValueError
If *lst* is not a list or is empty.
TypeError
If any element in *lst* is not a supported primitive type.
Examples
--------
.. ipython:: python
import pylegend
frame = pylegend.samples.pandas_api.northwind_orders_frame()
# Keep rows where Ship Name is one of several values
frame[
frame["Ship Name"].in_list(["Hanari Carnes", "Victuailles en stock", "Suprêmes délices"])
].head(3).to_pandas()
"""
if not isinstance(lst, list) or len(lst) == 0:
raise ValueError("in_list parameter should be a non-empty list of primitive values.")
operands: PyLegendList[PyLegendExpression] = [self.value()]
for item in lst:
PyLegendPrimitive.__validate_param_to_be_primitive(item, "in_list list element")
if isinstance(item, (int, float, bool, str, date, datetime, PythonDecimal)):
operands.append(convert_literal_to_literal_expression(item))
else:
operands.append(item.value())
from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
return PyLegendBoolean(PyLegendInListExpression(operands))
@staticmethod
def __validate_param_to_be_primitive(
param: "PyLegendUnion[int, float, bool, str, date, datetime, PythonDecimal, PyLegendPrimitive]",
desc: str
) -> None:
if not isinstance(param, (int, float, bool, str, date, datetime, PythonDecimal, PyLegendPrimitive)):
raise TypeError(
desc + " should be a int/float/bool/str/datetime.date/datetime.datetime/decimal.Decimal"
" or a primitive expression."
" Got value " + str(param) + " of type: " + str(type(param)))
@abstractmethod
def value(self) -> PyLegendExpression:
pass
PyLegendPrimitiveOrPythonPrimitive = PyLegendUnion[int, float, str, bool, date, datetime, PythonDecimal, PyLegendPrimitive]