Source code for xsdata.models.datatype

import datetime
import operator
import re
from collections import UserString
from typing import Any, Callable, Dict, NamedTuple, Optional, Union

from xsdata.utils.dates import (
    calculate_offset,
    calculate_timezone,
    format_date,
    format_offset,
    format_time,
    parse_date_args,
    validate_date,
    validate_time,
)

xml_duration_re = re.compile(
    r"^([-]?)P"
    r"(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?"
    r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(.\d+)?)S)?)?$"
)

DS_YEAR = 31556926.0
DS_MONTH = 2629743
DS_DAY = 86400
DS_HOUR = 3600
DS_MINUTE = 60
DS_FRACTIONAL_SECOND = 0.000000001
DS_OFFSET = -60


class DateFormat:
    DATE = "%Y-%m-%d%z"
    TIME = "%H:%M:%S%z"
    DATE_TIME = "%Y-%m-%dT%H:%M:%S%z"
    G_DAY = "---%d%z"
    G_MONTH = "--%m%z"
    G_MONTH_DAY = "--%m-%d%z"
    G_YEAR = "%Y%z"
    G_YEAR_MONTH = "%Y-%m%z"


[docs] class XmlDate(NamedTuple): """ Concrete xs:date builtin type. Represents iso 8601 date format [-]CCYY-MM-DD[Z|(+|-)hh:mm] with rich comparisons and hashing. :param year: Any signed integer, eg (0, -535, 2020) :param month: Unsigned integer between 1-12 :param day: Unsigned integer between 1-31 :param offset: Signed integer representing timezone offset in minutes """ year: int month: int day: int offset: Optional[int] = None
[docs] def replace( self, year: Optional[int] = None, month: Optional[int] = None, day: Optional[int] = None, offset: Optional[int] = True, ) -> "XmlDate": """Return a new instance replacing the specified fields with new values.""" if year is None: year = self.year if month is None: month = self.month if day is None: day = self.day if offset is True: offset = self.offset return type(self)(year, month, day, offset)
[docs] @classmethod def from_string(cls, string: str) -> "XmlDate": """Initialize from string with format ``%Y-%m-%dT%z``""" return cls(*parse_date_args(string, DateFormat.DATE))
[docs] @classmethod def from_date(cls, obj: datetime.date) -> "XmlDate": """ Initialize from :class:`datetime.date` instance. .. warning:: date instances don't have timezone information! """ return cls(obj.year, obj.month, obj.day)
[docs] @classmethod def from_datetime(cls, obj: datetime.datetime) -> "XmlDate": """Initialize from :class:`datetime.datetime` instance.""" return cls(obj.year, obj.month, obj.day, calculate_offset(obj))
[docs] @classmethod def today(cls) -> "XmlDate": """Initialize from datetime.date.today()""" return cls.from_date(datetime.date.today())
[docs] def to_date(self) -> datetime.date: """Return a :class:`datetime.date` instance.""" return datetime.date(self.year, self.month, self.day)
[docs] def to_datetime(self) -> datetime.datetime: """Return a :class:`datetime.datetime` instance.""" tz_info = calculate_timezone(self.offset) return datetime.datetime(self.year, self.month, self.day, tzinfo=tz_info)
[docs] def __str__(self) -> str: """ Return the date formatted according to ISO 8601 for xml. Examples: - 2001-10-26 - 2001-10-26+02:00 - 2001-10-26Z """ return format_date(self.year, self.month, self.day) + format_offset(self.offset)
def __repr__(self) -> str: args = [self.year, self.month, self.day, self.offset] if args[-1] is None: del args[-1] return f"{self.__class__.__qualname__}({', '.join(map(str, args))})"
[docs] class XmlDateTime(NamedTuple): """ Concrete xs:dateTime builtin type. Represents iso 8601 date time format [-]CCYY-MM-DDThh :mm: ss[Z|(+|-)hh:mm] with rich comparisons and hashing. :param year: Any signed integer, eg (0, -535, 2020) :param month: Unsigned integer between 1-12 :param day: Unsigned integer between 1-31 :param hour: Unsigned integer between 0-24 :param minute: Unsigned integer between 0-59 :param second: Unsigned integer between 0-59 :param fractional_second: Unsigned integer between 0-999999999 :param offset: Signed integer representing timezone offset in minutes """ year: int month: int day: int hour: int minute: int second: int fractional_second: int = 0 offset: Optional[int] = None @property def microsecond(self) -> int: return self.fractional_second // 1000 @property def duration(self) -> float: if self.year < 0: negative = True year = -self.year else: negative = False year = self.year total = ( year * DS_YEAR + self.month * DS_MONTH + self.day * DS_DAY + self.hour * DS_HOUR + self.minute * DS_MINUTE + self.second + self.fractional_second * DS_FRACTIONAL_SECOND + (self.offset or 0) * DS_OFFSET ) return -total if negative else total
[docs] @classmethod def from_string(cls, string: str) -> "XmlDateTime": """Initialize from string with format ``%Y-%m-%dT%H:%M:%S%z``""" ( year, month, day, hour, minute, second, fractional_second, offset, ) = parse_date_args(string, DateFormat.DATE_TIME) validate_date(year, month, day) validate_time(hour, minute, second, fractional_second) return cls(year, month, day, hour, minute, second, fractional_second, offset)
[docs] @classmethod def from_datetime(cls, obj: datetime.datetime) -> "XmlDateTime": """Initialize from :class:`datetime.datetime` instance.""" return cls( obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second, obj.microsecond * 1000, calculate_offset(obj), )
[docs] @classmethod def now(cls, tz: Optional[datetime.timezone] = None) -> "XmlDateTime": """Initialize from datetime.datetime.now()""" return cls.from_datetime(datetime.datetime.now(tz=tz))
[docs] @classmethod def utcnow(cls) -> "XmlDateTime": """Initialize from datetime.now(timezone.utc)""" return cls.from_datetime(datetime.datetime.now(datetime.timezone.utc))
[docs] def to_datetime(self) -> datetime.datetime: """Return a :class:`datetime.datetime` instance.""" return datetime.datetime( self.year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond, tzinfo=calculate_timezone(self.offset), )
[docs] def replace( self, year: Optional[int] = None, month: Optional[int] = None, day: Optional[int] = None, hour: Optional[int] = None, minute: Optional[int] = None, second: Optional[int] = None, fractional_second: Optional[int] = None, offset: Optional[int] = True, ) -> "XmlDateTime": """Return a new instance replacing the specified fields with new values.""" if year is None: year = self.year if month is None: month = self.month if day is None: day = self.day if hour is None: hour = self.hour if minute is None: minute = self.minute if second is None: second = self.second if fractional_second is None: fractional_second = self.fractional_second if offset is True: offset = self.offset return type(self)( year, month, day, hour, minute, second, fractional_second, offset )
[docs] def __str__(self) -> str: """ Return the datetime formatted according to ISO 8601 for xml. Examples: - 2001-10-26T21:32:52 - 2001-10-26T21:32:52+02:00 - 2001-10-26T19:32:52Z - 2001-10-26T19:32:52.126789 - 2001-10-26T21:32:52.126 - -2001-10-26T21:32:52.126Z """ return "{}T{}{}".format( format_date(self.year, self.month, self.day), format_time(self.hour, self.minute, self.second, self.fractional_second), format_offset(self.offset), )
def __repr__(self) -> str: args = tuple(self) if args[-1] is None: args = args[:-1] if args[-1] == 0: args = args[:-1] return f"{self.__class__.__qualname__}({', '.join(map(str, args))})"
[docs] def __eq__(self, other: Any) -> bool: return cmp(self, other, operator.eq)
[docs] def __ne__(self, other: Any) -> bool: return cmp(self, other, operator.ne)
[docs] def __lt__(self, other: Any) -> bool: return cmp(self, other, operator.lt)
[docs] def __le__(self, other: Any) -> bool: return cmp(self, other, operator.le)
[docs] def __gt__(self, other: Any) -> bool: return cmp(self, other, operator.gt)
[docs] def __ge__(self, other: Any) -> bool: return cmp(self, other, operator.ge)
[docs] class XmlTime(NamedTuple): """ Concrete xs:time builtin type. Represents iso 8601 time format hh :mm: ss[Z|(+|-)hh:mm] with rich comparisons and hashing. :param hour: Unsigned integer between 0-24 :param minute: Unsigned integer between 0-59 :param second: Unsigned integer between 0-59 :param fractional_second: Unsigned integer between 0-999999999 :param offset: Signed integer representing timezone offset in minutes """ hour: int minute: int second: int fractional_second: int = 0 offset: Optional[int] = None @property def microsecond(self) -> int: return self.fractional_second // 1000 @property def duration(self) -> float: return ( self.hour * DS_HOUR + self.minute * DS_MINUTE + self.second + self.fractional_second * DS_FRACTIONAL_SECOND + (self.offset or 0) * DS_OFFSET )
[docs] def replace( self, hour: Optional[int] = None, minute: Optional[int] = None, second: Optional[int] = None, fractional_second: Optional[int] = None, offset: Optional[int] = True, ) -> "XmlTime": """Return a new instance replacing the specified fields with new values.""" if hour is None: hour = self.hour if minute is None: minute = self.minute if second is None: second = self.second if fractional_second is None: fractional_second = self.fractional_second if offset is True: offset = self.offset return type(self)(hour, minute, second, fractional_second, offset)
[docs] @classmethod def from_string(cls, string: str) -> "XmlTime": """Initialize from string format ``%H:%M:%S%z``""" hour, minute, second, fractional_second, offset = parse_date_args( string, DateFormat.TIME ) validate_time(hour, minute, second, fractional_second) return cls(hour, minute, second, fractional_second, offset)
[docs] @classmethod def from_time(cls, obj: datetime.time) -> "XmlTime": """Initialize from :class:`datetime.time` instance.""" return cls( obj.hour, obj.minute, obj.second, obj.microsecond * 1000, calculate_offset(obj), )
[docs] @classmethod def now(cls, tz: Optional[datetime.timezone] = None) -> "XmlTime": """Initialize from datetime.datetime.now()""" return cls.from_time(datetime.datetime.now(tz=tz).time())
[docs] @classmethod def utcnow(cls) -> "XmlTime": """Initialize from datetime.now(timezone.utc)""" return cls.from_time(datetime.datetime.now(datetime.timezone.utc).time())
[docs] def to_time(self) -> datetime.time: """Return a :class:`datetime.time` instance.""" return datetime.time( self.hour, self.minute, self.second, self.microsecond, tzinfo=calculate_timezone(self.offset), )
[docs] def __str__(self) -> str: """ Return the time formatted according to ISO 8601 for xml. Examples: - 21:32:52 - 21:32:52+02:00, - 19:32:52Z - 21:32:52.126789 - 21:32:52.126Z """ return "{}{}".format( format_time(self.hour, self.minute, self.second, self.fractional_second), format_offset(self.offset), )
def __repr__(self) -> str: args = list(self) if args[-1] is None: del args[-1] return f"{self.__class__.__qualname__}({', '.join(map(str, args))})"
[docs] def __eq__(self, other: Any) -> bool: return cmp(self, other, operator.eq)
[docs] def __ne__(self, other: Any) -> bool: return cmp(self, other, operator.ne)
[docs] def __lt__(self, other: Any) -> bool: return cmp(self, other, operator.lt)
[docs] def __le__(self, other: Any) -> bool: return cmp(self, other, operator.le)
[docs] def __gt__(self, other: Any) -> bool: return cmp(self, other, operator.gt)
[docs] def __ge__(self, other: Any) -> bool: return cmp(self, other, operator.ge)
DurationType = Union[XmlTime, XmlDateTime] def cmp(a: DurationType, b: DurationType, op: Callable) -> bool: if isinstance(b, a.__class__): return op(a.duration, b.duration) return NotImplemented class TimeInterval(NamedTuple): negative: bool years: Optional[int] months: Optional[int] days: Optional[int] hours: Optional[int] minutes: Optional[int] seconds: Optional[float]
[docs] class XmlDuration(UserString): """ Concrete xs:duration builtin type. Represents iso 8601 duration format PnYnMnDTnHnMnS with rich comparisons and hashing. Format PnYnMnDTnHnMnS: - **P**: literal value that starts the expression - **nY**: the number of years followed by a literal Y - **nM**: the number of months followed by a literal M - **nD**: the number of days followed by a literal D - **T**: literal value that separates date and time parts - **nH**: the number of hours followed by a literal H - **nM**: the number of minutes followed by a literal M - **nS**: the number of seconds followed by a literal S :param value: String representation of a xs:duration, eg **P2Y6M5DT12H** """ def __init__(self, value: str) -> None: super().__init__(value) self._interval = self._parse_interval(value) @property def years(self) -> Optional[int]: """Number of years in the interval.""" return self._interval.years @property def months(self) -> Optional[int]: """Number of months in the interval.""" return self._interval.months @property def days(self) -> Optional[int]: """Number of days in the interval.""" return self._interval.days @property def hours(self) -> Optional[int]: """Number of hours in the interval.""" return self._interval.hours @property def minutes(self) -> Optional[int]: """Number of minutes in the interval.""" return self._interval.minutes @property def seconds(self) -> Optional[float]: """Number of seconds in the interval.""" return self._interval.seconds @property def negative(self) -> bool: """Negative flag of the interval.""" return self._interval.negative @classmethod def _parse_interval(cls, value: str) -> TimeInterval: if not isinstance(value, str): raise ValueError("Value must be string") if len(value) < 3 or value.endswith("T"): raise ValueError(f"Invalid format '{value}'") match = xml_duration_re.match(value) if not match: raise ValueError(f"Invalid format '{value}'") sign, years, months, days, hours, minutes, seconds, _ = match.groups() return TimeInterval( negative=sign == "-", years=int(years) if years else None, months=int(months) if months else None, days=int(days) if days else None, hours=int(hours) if hours else None, minutes=int(minutes) if minutes else None, seconds=float(seconds) if seconds else None, ) def asdict(self) -> Dict: return self._interval._asdict() def __repr__(self) -> str: return f'{self.__class__.__qualname__}("{self.data}")'
class TimePeriod(NamedTuple): year: Optional[int] month: Optional[int] day: Optional[int] offset: Optional[int]
[docs] class XmlPeriod(UserString): """ Concrete xs:gYear/Month/Day builtin type. Represents iso 8601 period formats with rich comparisons and hashing. Formats: - xs:gDay: **---%d%z** - xs:gMonth: **--%m%z** - xs:gYear: **%Y%z** - xs:gMonthDay: **--%m-%d%z** - xs:gYearMonth: **%Y-%m%z** :param value: String representation of a xs:period, eg **--11-01Z** """ def __init__(self, value: str) -> None: value = value.strip() super().__init__(value) self._period = self._parse_period(value) @property def year(self) -> Optional[int]: """Period year.""" return self._period.year @property def month(self) -> Optional[int]: """Period month.""" return self._period.month @property def day(self) -> Optional[int]: """Period day.""" return self._period.day @property def offset(self) -> Optional[int]: """Period timezone offset in minutes.""" return self._period.offset @classmethod def _parse_period(cls, value: str) -> TimePeriod: year = month = day = offset = None if value.startswith("---"): day, offset = parse_date_args(value, DateFormat.G_DAY) elif value.startswith("--"): # Bogus format --MM--, --05---05:00 if value[4:6] == "--": value = value[:4] + value[6:] if len(value) in (4, 5, 10): # fixed lengths with/out timezone month, offset = parse_date_args(value, DateFormat.G_MONTH) else: month, day, offset = parse_date_args(value, DateFormat.G_MONTH_DAY) else: end = len(value) if value.find(":") > -1: # offset end -= 6 if value[:end].rfind("-") > 3: # Minimum position for month sep year, month, offset = parse_date_args(value, DateFormat.G_YEAR_MONTH) else: year, offset = parse_date_args(value, DateFormat.G_YEAR) validate_date(0, month or 1, day or 1) return TimePeriod(year=year, month=month, day=day, offset=offset)
[docs] def as_dict(self) -> Dict: """Return date units as dict.""" return self._period._asdict()
def __repr__(self) -> str: return f'{self.__class__.__qualname__}("{self.data}")'
[docs] def __eq__(self, other: Any) -> bool: if isinstance(other, XmlPeriod): return self._period == other._period return NotImplemented
[docs] class XmlHexBinary(bytes): """ Subclass bytes to infer base16 format. This type can be used with xs:anyType fields that don't have a format property to specify the target output format. """
[docs] class XmlBase64Binary(bytes): """ Subclass bytes to infer base64 format. This type can be used with xs:anyType fields that don't have a format property to specify the target output format. """