import datetime as dt
import re
from dataclasses import dataclass, field
from typing import List as ListType, Optional, Tuple
from uuid import UUID
from dateutil.parser import isoparse
DURATION_PATTERN = re.compile(
r"([+-])?P(\d+Y)?(\d+M)?(\d+D)?(?:T(\d+H)?(\d+M)?(\d+(?:\.\d+)?S)?)?"
)
@dataclass(frozen=True)
class _Node:
pass
[docs]@dataclass(frozen=True)
class Identifier(_Node):
name: str
namespace: Tuple[str, ...] = field(default_factory=tuple)
[docs] def full_name(self):
return ".".join(self.namespace + (self.name,))
[docs]@dataclass(frozen=True)
class Attribute(_Node):
owner: _Node
attr: str
###############################################################################
# Literals
###############################################################################
@dataclass(frozen=True)
class _Literal(_Node):
pass
@property
def py_val(self):
raise NotImplementedError()
[docs]@dataclass(frozen=True)
class Null(_Literal):
@property
def py_val(self) -> None:
return None
[docs]@dataclass(frozen=True)
class Integer(_Literal):
val: str
@property
def py_val(self) -> int:
return int(self.val)
[docs]@dataclass(frozen=True)
class Float(_Literal):
val: str
@property
def py_val(self) -> float:
return float(self.val)
[docs]@dataclass(frozen=True)
class Boolean(_Literal):
val: str
@property
def py_val(self) -> bool:
return self.val.lower() == "true"
[docs]@dataclass(frozen=True)
class String(_Literal):
val: str
@property
def py_val(self) -> str:
return self.val
[docs]@dataclass(frozen=True)
class Geography(_Literal):
val: str
[docs] def wkt(self):
return self.val
[docs]@dataclass(frozen=True)
class Date(_Literal):
val: str
@property
def py_val(self) -> dt.date:
return dt.date.fromisoformat(self.val)
[docs]@dataclass(frozen=True)
class Time(_Literal):
val: str
@property
def py_val(self) -> dt.time:
return dt.time.fromisoformat(self.val)
[docs]@dataclass(frozen=True)
class DateTime(_Literal):
val: str
@property
def py_val(self) -> dt.datetime:
return isoparse(self.val)
[docs]@dataclass(frozen=True)
class Duration(_Literal):
val: str
@property
def py_val(self) -> dt.timedelta:
sign, years, months, days, hours, minutes, seconds = self.unpack()
# Initialize days to 0 if None
num_days = float(days or 0)
# Approximate conversion, adjust as necessary for more precision
num_days += float(years or 0) * 365.25 # Average including leap years
num_days += float(months or 0) * 30.44 # Average month length
delta = dt.timedelta(
days=num_days,
hours=float(hours or 0),
minutes=float(minutes or 0),
seconds=float(seconds or 0),
)
if sign and sign == "-":
delta = -1 * delta
return delta
[docs] def unpack(
self,
) -> Tuple[
Optional[str],
Optional[str],
Optional[str],
Optional[str],
Optional[str],
Optional[str],
Optional[str],
]:
"""
Returns:
``(sign, years, months, days, hours, minutes, seconds)``
"""
match = DURATION_PATTERN.fullmatch(self.val)
if not match:
raise ValueError(f"Could not unpack Duration with value {self.val}")
sign, years, months, days, hours, minutes, seconds = match.groups()
_years = years[:-1] if years else None
_months = months[:-1] if months else None
_days = days[:-1] if days else None
_hours = hours[:-1] if hours else None
_minutes = minutes[:-1] if minutes else None
_seconds = seconds[:-1] if seconds else None
return sign, _years, _months, _days, _hours, _minutes, _seconds
[docs]@dataclass(frozen=True)
class GUID(_Literal):
val: str
@property
def py_val(self) -> UUID:
return UUID(self.val)
[docs]@dataclass(frozen=True)
class List(_Literal):
val: ListType[_Literal]
@property
def py_val(self) -> list:
return [v.py_val for v in self.val]
###############################################################################
# Arithmetic
###############################################################################
@dataclass(frozen=True)
class _BinOpToken(_Node):
pass
[docs]@dataclass(frozen=True)
class Add(_BinOpToken):
pass
[docs]@dataclass(frozen=True)
class Sub(_BinOpToken):
pass
[docs]@dataclass(frozen=True)
class Mult(_BinOpToken):
pass
[docs]@dataclass(frozen=True)
class Div(_BinOpToken):
pass
[docs]@dataclass(frozen=True)
class Mod(_BinOpToken):
pass
[docs]@dataclass(frozen=True)
class BinOp(_Node):
op: _BinOpToken
left: _Node
right: _Node
###############################################################################
# Comparison
###############################################################################
@dataclass(frozen=True)
class _Comparator(_Node):
pass
[docs]@dataclass(frozen=True)
class Eq(_Comparator):
pass
[docs]@dataclass(frozen=True)
class NotEq(_Comparator):
pass
[docs]@dataclass(frozen=True)
class Lt(_Comparator):
pass
[docs]@dataclass(frozen=True)
class LtE(_Comparator):
pass
[docs]@dataclass(frozen=True)
class Gt(_Comparator):
pass
[docs]@dataclass(frozen=True)
class GtE(_Comparator):
pass
[docs]@dataclass(frozen=True)
class In(_Comparator):
pass
[docs]@dataclass(frozen=True)
class Compare(_Node):
comparator: _Comparator
left: _Node
right: _Node
###############################################################################
# Boolean ops
###############################################################################
@dataclass(frozen=True)
class _BoolOpToken(_Node):
pass
[docs]@dataclass(frozen=True)
class And(_BoolOpToken):
pass
[docs]@dataclass(frozen=True)
class Or(_BoolOpToken):
pass
[docs]@dataclass(frozen=True)
class BoolOp(_Node):
op: _BoolOpToken
left: _Node
right: _Node
###############################################################################
# Unary ops
###############################################################################
@dataclass(frozen=True)
class _UnaryOpToken(_Node):
pass
[docs]@dataclass(frozen=True)
class Not(_UnaryOpToken):
pass
[docs]@dataclass(frozen=True)
class USub(_UnaryOpToken):
pass
[docs]@dataclass(frozen=True)
class UnaryOp(_Node):
op: _UnaryOpToken
operand: _Node
###############################################################################
# Function calls
###############################################################################
[docs]@dataclass(frozen=True)
class NamedParam(_Node):
name: Identifier
param: _Node
[docs]@dataclass(frozen=True)
class Call(_Node):
func: Identifier
args: ListType[_Node]
###############################################################################
# Collections
###############################################################################
@dataclass(frozen=True)
class _CollectionOperator(_Node):
pass
[docs]@dataclass(frozen=True)
class Any(_CollectionOperator):
pass
[docs]@dataclass(frozen=True)
class All(_CollectionOperator):
pass
[docs]@dataclass(frozen=True)
class Lambda(_Node):
identifier: Identifier
expression: _Node
[docs]@dataclass(frozen=True)
class CollectionLambda(_Node):
owner: _Node
operator: _CollectionOperator
lambda_: Optional[Lambda]