Source code for quantumengine.fields.datetime

import datetime
from typing import Any, Optional

from surrealdb.data.types.datetime import IsoDateTimeWrapper

from .base import Field

[docs] class DateTimeField(Field[datetime.datetime]): """DateTime field type. This field type stores datetime values and provides validation and conversion between Python datetime objects and SurrealDB datetime format. SurrealDB v2.0.0+ requires datetime values to have a ``d`` prefix or be cast as ``<datetime>``. This field handles the conversion automatically, so you can use standard Python datetime objects in your code. Example: >>> class Event(Document): ... created_at = DateTimeField(default=datetime.datetime.now) ... scheduled_for = DateTimeField() >>> >>> # Python datetime objects are automatically converted to SurrealDB format >>> event = Event(scheduled_for=datetime.datetime.now() + datetime.timedelta(days=7)) >>> await event.save() """
[docs] def __init__(self, **kwargs: Any) -> None: """Initialize a new DateTimeField. Args: **kwargs: Additional arguments to pass to the parent class """ super().__init__(**kwargs) self.py_type = datetime.datetime
[docs] def validate(self, value: Any) -> datetime.datetime: """Validate the datetime value. This method checks if the value is a valid datetime or can be converted to a datetime from an ISO format string. Args: value: The value to validate Returns: The validated datetime value Raises: TypeError: If the value cannot be converted to a datetime """ value = super().validate(value) if value is not None and not isinstance(value, datetime.datetime): try: return datetime.datetime.fromisoformat(value) except (TypeError, ValueError): raise TypeError(f"Expected datetime for field '{self.name}', got {type(value)}") return value
def _to_db_backend_specific(self, value: Any, backend: str) -> Optional[Any]: """Backend-specific datetime conversion logic. Args: value: The Python datetime to convert backend: The backend type Returns: Backend-appropriate datetime representation """ if value is not None: if isinstance(value, str): try: value = datetime.datetime.fromisoformat(value) except ValueError: return value if isinstance(value, datetime.datetime): if backend == 'clickhouse': # ClickHouse prefers datetime strings in specific format return value.strftime('%Y-%m-%d %H:%M:%S') elif backend == 'surrealdb': # SurrealDB requires actual datetime objects for SCHEMAFULL tables # Return the datetime object directly instead of converting to string return value else: # Default to ISO format return value.isoformat() return value def _from_db_backend_specific(self, value: Any, backend: str) -> Optional[datetime.datetime]: """Backend-specific datetime conversion logic. Args: value: The database value to convert backend: The backend type Returns: Python datetime object """ if value is not None: if backend == 'surrealdb': # Handle IsoDateTimeWrapper instances (SurrealDB specific) if isinstance(value, IsoDateTimeWrapper): try: return datetime.datetime.fromisoformat(value.dt) except ValueError: pass # Handle string representations with SurrealDB prefix elif isinstance(value, str): # Remove `d` prefix if present (SurrealDB format) if value.startswith("d'") and value.endswith("'"): value = value[2:-1] try: return datetime.datetime.fromisoformat(value) except ValueError: pass elif backend == 'clickhouse': # ClickHouse typically returns datetime objects or strings if isinstance(value, str): try: # Try common ClickHouse datetime formats for fmt in ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S']: try: return datetime.datetime.strptime(value, fmt) except ValueError: continue # Fallback to ISO format return datetime.datetime.fromisoformat(value) except ValueError: pass else: # Default behavior for other backends if isinstance(value, str): try: return datetime.datetime.fromisoformat(value) except ValueError: pass # Handle datetime objects directly (common across all backends) if isinstance(value, datetime.datetime): return value return value
[docs] class TimeSeriesField(DateTimeField): """Field for time series data. This field type extends DateTimeField and adds support for time series data. It can be used to store timestamps for time series data and supports additional metadata for time series operations. Example: class SensorReading(Document): timestamp = TimeSeriesField(index=True) value = FloatField() class Meta: time_series = True time_field = "timestamp" """
[docs] def __init__(self, **kwargs: Any) -> None: """Initialize a new TimeSeriesField. Args: **kwargs: Additional arguments to pass to the parent class """ super().__init__(**kwargs)
[docs] def validate(self, value: Any) -> Optional[datetime.datetime]: """Validate the timestamp value. This method checks if the value is a valid timestamp for time series data. Args: value: The value to validate Returns: The validated timestamp value """ return super().validate(value)
[docs] class DurationField(Field): """Duration field type. This field type stores durations of time and provides validation and conversion between Python timedelta objects and SurrealDB duration strings. """
[docs] def __init__(self, **kwargs: Any) -> None: """Initialize a new DurationField. Args: **kwargs: Additional arguments to pass to the parent class """ super().__init__(**kwargs) self.py_type = datetime.timedelta
[docs] def validate(self, value: Any) -> Optional[datetime.timedelta]: """Validate the duration value. This method checks if the value is a valid timedelta or can be converted to a timedelta from a string. Args: value: The value to validate Returns: The validated timedelta value Raises: TypeError: If the value cannot be converted to a timedelta """ value = super().validate(value) if value is not None: if isinstance(value, datetime.timedelta): return value if isinstance(value, str): try: # Parse SurrealDB duration format (e.g., "1y2m3d4h5m6s") # This is a simplified implementation and may need to be expanded total_seconds = 0 num_buffer = "" for char in value: if char.isdigit(): num_buffer += char elif char == 'y' and num_buffer: total_seconds += int(num_buffer) * 365 * 24 * 60 * 60 num_buffer = "" elif char == 'm' and num_buffer: # Ambiguous: could be month or minute # Assume month if previous char was 'y', otherwise minute if 'y' in value[:value.index(char)]: total_seconds += int(num_buffer) * 30 * 24 * 60 * 60 else: total_seconds += int(num_buffer) * 60 num_buffer = "" elif char == 'd' and num_buffer: total_seconds += int(num_buffer) * 24 * 60 * 60 num_buffer = "" elif char == 'h' and num_buffer: total_seconds += int(num_buffer) * 60 * 60 num_buffer = "" elif char == 's' and num_buffer: total_seconds += int(num_buffer) num_buffer = "" return datetime.timedelta(seconds=total_seconds) except (ValueError, TypeError): pass raise TypeError(f"Expected duration for field '{self.name}', got {type(value)}") return value
[docs] def to_db(self, value: Any) -> Optional[Any]: """Convert Python timedelta to database representation. This method converts a Python timedelta object to a SurrealDB Duration object for storage in the database. Args: value: The Python timedelta to convert Returns: The SurrealDB Duration object for the database """ if value is None: return None # Import SurrealDB Duration class from surrealdb import Duration if isinstance(value, str): # If it's already a string, convert to a supported format self.validate(value) # Validate first # Convert years to days (approximate: 1 year = 365 days) if 'y' in value: # Simple conversion for basic year formats like "2y" import re year_match = re.search(r'(\d+)y', value) if year_match: years = int(year_match.group(1)) days = years * 365 # Replace the year part with days converted = re.sub(r'\d+y', f'{days}d', value) return Duration.parse(converted) return Duration.parse(value) if isinstance(value, datetime.timedelta): # Convert timedelta to SurrealDB duration format seconds = int(value.total_seconds()) minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) result = "" if days > 0: result += f"{days}d" if hours > 0: result += f"{hours}h" if minutes > 0: result += f"{minutes}m" if seconds > 0 or not result: result += f"{seconds}s" return Duration.parse(result) # If it's already a Duration object, return as is if hasattr(value, 'to_string') and hasattr(value, 'elapsed'): return value raise TypeError(f"Cannot convert {type(value)} to duration")
[docs] def from_db(self, value: Any) -> Optional[datetime.timedelta]: """Convert database value to Python timedelta. This method converts a SurrealDB duration string from the database to a Python timedelta object. Args: value: The database value to convert Returns: The Python timedelta object """ if value is not None and isinstance(value, str): return self.validate(value) return value