soia-client 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soia_client-1.0.0.dist-info/METADATA +12 -0
- soia_client-1.0.0.dist-info/RECORD +21 -0
- soia_client-1.0.0.dist-info/WHEEL +5 -0
- soia_client-1.0.0.dist-info/top_level.txt +1 -0
- soialib/__init__.py +0 -0
- soialib/impl/__init__.py +0 -0
- soialib/impl/arrays.py +163 -0
- soialib/impl/encoding.py +41 -0
- soialib/impl/enums.py +414 -0
- soialib/impl/function_maker.py +196 -0
- soialib/impl/optionals.py +74 -0
- soialib/impl/primitives.py +210 -0
- soialib/impl/repr.py +43 -0
- soialib/impl/structs.py +718 -0
- soialib/impl/type_adapter.py +56 -0
- soialib/keyed_items.py +14 -0
- soialib/module_initializer.py +79 -0
- soialib/never.py +4 -0
- soialib/serializer.py +87 -0
- soialib/spec.py +148 -0
- soialib/timestamp.py +127 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from soialib import spec
|
|
5
|
+
from soialib.impl.function_maker import ExprLike
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TypeAdapter(Protocol):
|
|
9
|
+
def default_expr(self) -> ExprLike:
|
|
10
|
+
"""
|
|
11
|
+
The default value for T.
|
|
12
|
+
"""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> ExprLike:
|
|
16
|
+
"""
|
|
17
|
+
Transforms the argument passed to the constructor of a frozen class into a
|
|
18
|
+
frozen value which will be assigned to the attribute of the frozen object.
|
|
19
|
+
|
|
20
|
+
The type of the returned expression must be T even if the argument does not have
|
|
21
|
+
the expected type. Ideally, the expression should raise an error in the latter
|
|
22
|
+
case.
|
|
23
|
+
"""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
27
|
+
"""
|
|
28
|
+
Returns an expression which evaluates to true if the given value is *not* the
|
|
29
|
+
default value for T.
|
|
30
|
+
This expression is inserted in the constructor of a frozen class, after the
|
|
31
|
+
attribute has been assigned from the result of freezing arg_expr.
|
|
32
|
+
If possible, an implemtation should try to use arg_expr instead of attr_expr as
|
|
33
|
+
it offers a marginal performance advantage ('x' vs 'self.x').
|
|
34
|
+
"""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
|
|
38
|
+
"""
|
|
39
|
+
Returns an expression which can be passed to 'json.dumps()' in order to
|
|
40
|
+
serialize the given T into JSON format.
|
|
41
|
+
The JSON flavor (dense versus readable) is given by the 'readable' arg.
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def from_json_expr(self, json_expr: ExprLike) -> ExprLike:
|
|
46
|
+
"""
|
|
47
|
+
Transforms 'json_expr' into a T.
|
|
48
|
+
The 'json_expr' arg is obtained by calling 'json.loads()'.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
# TODO: comment!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
53
|
+
def finalize(
|
|
54
|
+
self,
|
|
55
|
+
resolve_type_fn: Callable[[spec.Type], "TypeAdapter"],
|
|
56
|
+
) -> None: ...
|
soialib/keyed_items.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Generic, Optional, TypeVar
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Item = TypeVar("Item")
|
|
6
|
+
Key = TypeVar("Key")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KeyedItems(Generic[Item, Key], tuple[Item, ...], metaclass=abc.ABCMeta):
|
|
10
|
+
@abc.abstractmethod
|
|
11
|
+
def find(self, key: Key) -> Optional[Item]: ...
|
|
12
|
+
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def find_or_default(self, key: Key) -> Item: ...
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from typing import Any, Union
|
|
2
|
+
|
|
3
|
+
from soialib import spec
|
|
4
|
+
from soialib.impl import arrays, enums, optionals, primitives, structs
|
|
5
|
+
from soialib.impl.type_adapter import TypeAdapter
|
|
6
|
+
from soialib.serializer import make_serializer
|
|
7
|
+
|
|
8
|
+
RecordAdapter = Union[structs.StructAdapter, enums.EnumAdapter]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_record_id_to_adapter: dict[str, RecordAdapter] = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init_module(
|
|
15
|
+
module: spec.Module,
|
|
16
|
+
globals: dict[str, Any],
|
|
17
|
+
# For testing
|
|
18
|
+
record_id_to_adapter: dict[str, RecordAdapter] = _record_id_to_adapter,
|
|
19
|
+
) -> None:
|
|
20
|
+
def resolve_type(type: spec.Type) -> TypeAdapter:
|
|
21
|
+
if isinstance(type, spec.PrimitiveType):
|
|
22
|
+
if type == spec.PrimitiveType.BOOL:
|
|
23
|
+
return primitives.BOOL_ADAPTER
|
|
24
|
+
elif type == spec.PrimitiveType.BYTES:
|
|
25
|
+
return primitives.BYTES_ADAPTER
|
|
26
|
+
elif type == spec.PrimitiveType.FLOAT32:
|
|
27
|
+
return primitives.FLOAT32_ADAPTER
|
|
28
|
+
elif type == spec.PrimitiveType.FLOAT64:
|
|
29
|
+
return primitives.FLOAT64_ADAPTER
|
|
30
|
+
elif type == spec.PrimitiveType.INT32:
|
|
31
|
+
return primitives.INT32_ADAPTER
|
|
32
|
+
elif type == spec.PrimitiveType.INT64:
|
|
33
|
+
return primitives.INT64_ADAPTER
|
|
34
|
+
elif type == spec.PrimitiveType.STRING:
|
|
35
|
+
return primitives.STRING_ADAPTER
|
|
36
|
+
elif type == spec.PrimitiveType.TIMESTAMP:
|
|
37
|
+
return primitives.TIMESTAMP_ADAPTER
|
|
38
|
+
elif type == spec.PrimitiveType.UINT64:
|
|
39
|
+
return primitives.UINT64_ADAPTER
|
|
40
|
+
elif isinstance(type, spec.ArrayType):
|
|
41
|
+
return arrays.get_array_adapter(
|
|
42
|
+
resolve_type(type.item),
|
|
43
|
+
type.key_attributes,
|
|
44
|
+
)
|
|
45
|
+
elif isinstance(type, spec.OptionalType):
|
|
46
|
+
return optionals.get_optional_adapter(resolve_type(type.value))
|
|
47
|
+
elif isinstance(type, str):
|
|
48
|
+
# A record id.
|
|
49
|
+
return record_id_to_adapter[type]
|
|
50
|
+
|
|
51
|
+
module_adapters: list[RecordAdapter] = []
|
|
52
|
+
for record in module.records:
|
|
53
|
+
if record.id in record_id_to_adapter:
|
|
54
|
+
raise AssertionError(record.id)
|
|
55
|
+
adapter: RecordAdapter
|
|
56
|
+
if isinstance(record, spec.Struct):
|
|
57
|
+
adapter = structs.StructAdapter(record)
|
|
58
|
+
else:
|
|
59
|
+
adapter = enums.EnumAdapter(record)
|
|
60
|
+
module_adapters.append(adapter)
|
|
61
|
+
record_id_to_adapter[record.id] = adapter
|
|
62
|
+
# Once all the adapters of the module have been created, we can finalize them.
|
|
63
|
+
for adapter in module_adapters:
|
|
64
|
+
adapter.finalize(resolve_type)
|
|
65
|
+
gen_class = adapter.gen_class
|
|
66
|
+
# Add the class name to either globals() if the record is defined at the top
|
|
67
|
+
# level, or the parent class otherwise.
|
|
68
|
+
record_id = spec.RecordId.parse(adapter.spec.id)
|
|
69
|
+
parent_id = record_id.parent
|
|
70
|
+
class_name = adapter.spec.class_name
|
|
71
|
+
if parent_id:
|
|
72
|
+
parent_adapter = record_id_to_adapter[parent_id.record_id]
|
|
73
|
+
setattr(parent_adapter.gen_class, class_name, gen_class)
|
|
74
|
+
gen_class._parent_class = parent_adapter.gen_class
|
|
75
|
+
else:
|
|
76
|
+
globals[class_name] = gen_class
|
|
77
|
+
gen_class._parent_class = None
|
|
78
|
+
# TODO: comment
|
|
79
|
+
gen_class.SERIALIZER = make_serializer(adapter)
|
soialib/never.py
ADDED
soialib/serializer.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json as jsonlib
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import FrozenInstanceError
|
|
5
|
+
from typing import Any, Generic, TypeVar, cast, final
|
|
6
|
+
|
|
7
|
+
from soialib.impl.function_maker import BodyBuilder, Expr, LineSpan, make_function
|
|
8
|
+
from soialib.impl.type_adapter import TypeAdapter
|
|
9
|
+
from soialib.never import Never
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@final
|
|
15
|
+
class Serializer(Generic[T]):
|
|
16
|
+
__slots__ = (
|
|
17
|
+
"_to_dense_json_fn",
|
|
18
|
+
"_to_readable_json_fn",
|
|
19
|
+
"_from_json_fn",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_to_dense_json_fn: Callable[[T], Any]
|
|
23
|
+
_to_readable_json_fn: Callable[[T], Any]
|
|
24
|
+
_from_json_fn: Callable[[Any], T]
|
|
25
|
+
|
|
26
|
+
def __init__(self, adapter: Never):
|
|
27
|
+
# Use Never (^) as a trick to make the constructor internal.
|
|
28
|
+
object.__setattr__(
|
|
29
|
+
self, "_to_dense_json_fn", _make_to_json_fn(adapter, readable=False)
|
|
30
|
+
)
|
|
31
|
+
object.__setattr__(
|
|
32
|
+
self,
|
|
33
|
+
"_to_readable_json_fn",
|
|
34
|
+
_make_to_json_fn(adapter, readable=True),
|
|
35
|
+
)
|
|
36
|
+
object.__setattr__(self, "_from_json_fn", _make_from_json_fn(adapter))
|
|
37
|
+
|
|
38
|
+
def to_json(self, input: T, readable_flavor=False) -> Any:
|
|
39
|
+
if readable_flavor:
|
|
40
|
+
return self._to_readable_json_fn(input)
|
|
41
|
+
else:
|
|
42
|
+
return self._to_dense_json_fn(input)
|
|
43
|
+
|
|
44
|
+
def to_json_code(self, input: T, readable_flavor=False) -> str:
|
|
45
|
+
return jsonlib.dumps(self.to_json(input, readable_flavor))
|
|
46
|
+
|
|
47
|
+
def from_json(self, json: Any) -> T:
|
|
48
|
+
return self._from_json_fn(json)
|
|
49
|
+
|
|
50
|
+
def from_json_code(self, json_code: str) -> T:
|
|
51
|
+
return self._from_json_fn(jsonlib.loads(json_code))
|
|
52
|
+
|
|
53
|
+
def __setattr__(self, name: str, value: Any):
|
|
54
|
+
raise FrozenInstanceError(self.__class__.__qualname__)
|
|
55
|
+
|
|
56
|
+
def __delattr__(self, name: str):
|
|
57
|
+
raise FrozenInstanceError(self.__class__.__qualname__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_serializer(adapter: TypeAdapter) -> Serializer:
|
|
61
|
+
return Serializer(cast(Never, adapter))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _make_to_json_fn(adapter: TypeAdapter, readable: bool) -> Callable[[Any], Any]:
|
|
65
|
+
return make_function(
|
|
66
|
+
name="to_json",
|
|
67
|
+
params=["input"],
|
|
68
|
+
body=[
|
|
69
|
+
LineSpan.join(
|
|
70
|
+
"return ",
|
|
71
|
+
adapter.to_json_expr(
|
|
72
|
+
adapter.to_frozen_expr(Expr.join("input")),
|
|
73
|
+
readable=readable,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _make_from_json_fn(adapter: TypeAdapter) -> Callable[[Any], Any]:
|
|
81
|
+
return make_function(
|
|
82
|
+
name="from_json",
|
|
83
|
+
params=["json"],
|
|
84
|
+
body=[
|
|
85
|
+
LineSpan.join("return ", adapter.from_json_expr(Expr.join("json"))),
|
|
86
|
+
],
|
|
87
|
+
)
|
soialib/spec.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PrimitiveType(enum.Enum):
|
|
7
|
+
BOOL = enum.auto()
|
|
8
|
+
INT32 = enum.auto()
|
|
9
|
+
INT64 = enum.auto()
|
|
10
|
+
UINT64 = enum.auto()
|
|
11
|
+
FLOAT32 = enum.auto()
|
|
12
|
+
FLOAT64 = enum.auto()
|
|
13
|
+
TIMESTAMP = enum.auto()
|
|
14
|
+
STRING = enum.auto()
|
|
15
|
+
BYTES = enum.auto()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ArrayType:
|
|
20
|
+
item: "Type"
|
|
21
|
+
# TODO: comment
|
|
22
|
+
key_attributes: tuple[str, ...] = ()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class OptionalType:
|
|
27
|
+
value: "Type"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# TODO: comment
|
|
31
|
+
Type = Union[PrimitiveType, ArrayType, OptionalType, str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Field:
|
|
36
|
+
"""Field of a struct."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
number: int
|
|
40
|
+
type: Type
|
|
41
|
+
has_mutable_getter: bool = False
|
|
42
|
+
_attribute: str = "" # If different from 'name'
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def attribute(self):
|
|
46
|
+
return self._attribute or self.name
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Struct:
|
|
51
|
+
id: str
|
|
52
|
+
fields: tuple[Field, ...] = ()
|
|
53
|
+
removed_numbers: tuple[int, ...] = ()
|
|
54
|
+
_class_name: str = "" # If different from the record name
|
|
55
|
+
_class_qualname: str = "" # If different from the qualified name of the record
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def class_name(self):
|
|
59
|
+
return self._class_name or RecordId.parse(self.id).name
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def class_qualname(self):
|
|
63
|
+
return self._class_qualname or RecordId.parse(self.id).qualname
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class ConstantField:
|
|
68
|
+
"""Constant field of an enum."""
|
|
69
|
+
|
|
70
|
+
name: str
|
|
71
|
+
number: int
|
|
72
|
+
_attribute: str = "" # If different from 'name'
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def attribute(self):
|
|
76
|
+
return self._attribute or self.name
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ValueField:
|
|
81
|
+
"""Value field of an enum."""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
number: int
|
|
85
|
+
type: Type
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class Enum:
|
|
90
|
+
id: str
|
|
91
|
+
constant_fields: tuple[ConstantField, ...] = ()
|
|
92
|
+
value_fields: tuple[ValueField, ...] = ()
|
|
93
|
+
removed_numbers: tuple[int, ...] = ()
|
|
94
|
+
_class_name: str = "" # If different from the record name
|
|
95
|
+
_class_qualname: str = "" # If different from the qualified name of the record
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def class_name(self):
|
|
99
|
+
return self._class_name or RecordId.parse(self.id).name
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def class_qualname(self):
|
|
103
|
+
return self._class_qualname or RecordId.parse(self.id).qualname
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
Record = Union[Struct, Enum]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class Module:
|
|
111
|
+
records: tuple[Record, ...] = ()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class RecordId:
|
|
116
|
+
record_id: str
|
|
117
|
+
module_path: str
|
|
118
|
+
name: str
|
|
119
|
+
qualname: str
|
|
120
|
+
name_parts: tuple[str, ...]
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def parse(record_id: str) -> "RecordId":
|
|
124
|
+
colon_index = record_id.rfind(":")
|
|
125
|
+
module_path = record_id[0:colon_index]
|
|
126
|
+
qualname = record_id[colon_index + 1 :]
|
|
127
|
+
name_parts = tuple(qualname.split("."))
|
|
128
|
+
return RecordId(
|
|
129
|
+
record_id=record_id,
|
|
130
|
+
module_path=module_path,
|
|
131
|
+
name=name_parts[-1],
|
|
132
|
+
qualname=qualname,
|
|
133
|
+
name_parts=name_parts,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def parent(self) -> Optional["RecordId"]:
|
|
138
|
+
if len(self.name_parts) == 1:
|
|
139
|
+
return None
|
|
140
|
+
parent_name_parts = self.name_parts[0:-1]
|
|
141
|
+
parent_qualname = ".".join(parent_name_parts)
|
|
142
|
+
return RecordId(
|
|
143
|
+
record_id=f"{self.module_path}:{parent_qualname}",
|
|
144
|
+
module_path=self.module_path,
|
|
145
|
+
name=parent_name_parts[-1],
|
|
146
|
+
qualname=parent_qualname,
|
|
147
|
+
name_parts=parent_name_parts,
|
|
148
|
+
)
|
soialib/timestamp.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import Any, Final, Union, final, overload
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@final
|
|
7
|
+
class Timestamp:
|
|
8
|
+
__slots__ = ("unix_millis",)
|
|
9
|
+
|
|
10
|
+
unix_millis: int
|
|
11
|
+
|
|
12
|
+
def __init__(self, unix_millis: int, _formatted: str = ""):
|
|
13
|
+
object.__setattr__(
|
|
14
|
+
self,
|
|
15
|
+
"unix_millis",
|
|
16
|
+
round(min(max(unix_millis, -8640000000000000), 8640000000000000)),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def from_unix_millis(unix_millis: int) -> "Timestamp":
|
|
21
|
+
return Timestamp(unix_millis=unix_millis)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def from_unix_seconds(unix_seconds: float) -> "Timestamp":
|
|
25
|
+
return Timestamp(unix_millis=round(unix_seconds * 1000))
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def from_datetime(dt: datetime.datetime) -> "Timestamp":
|
|
29
|
+
return Timestamp.from_unix_seconds(dt.timestamp())
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def now() -> "Timestamp":
|
|
33
|
+
return Timestamp.from_datetime(datetime.datetime.now(tz=datetime.timezone.utc))
|
|
34
|
+
|
|
35
|
+
EPOCH: Final["Timestamp"] = ...
|
|
36
|
+
MIN: Final["Timestamp"] = ...
|
|
37
|
+
MAX: Final["Timestamp"] = ...
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def unix_seconds(self) -> float:
|
|
41
|
+
return self.unix_millis / 1000.0
|
|
42
|
+
|
|
43
|
+
def to_datetime_or_raise(self) -> datetime.datetime:
|
|
44
|
+
return datetime.datetime.fromtimestamp(
|
|
45
|
+
self.unix_seconds, tz=datetime.timezone.utc
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __add__(self, td: datetime.timedelta) -> "Timestamp":
|
|
49
|
+
return Timestamp(
|
|
50
|
+
unix_millis=self.unix_millis + round(td.total_seconds() * 1000)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def __sub__(self, other: datetime.timedelta) -> "Timestamp": ...
|
|
55
|
+
@overload
|
|
56
|
+
def __sub__(self, other: "Timestamp") -> datetime.timedelta: ...
|
|
57
|
+
def __sub__(
|
|
58
|
+
self, other: Union["Timestamp", datetime.timedelta]
|
|
59
|
+
) -> Union["Timestamp", datetime.timedelta]:
|
|
60
|
+
if isinstance(other, Timestamp):
|
|
61
|
+
return datetime.timedelta(milliseconds=self.unix_millis - other.unix_millis)
|
|
62
|
+
else:
|
|
63
|
+
return self.__add__(-other)
|
|
64
|
+
|
|
65
|
+
def __lt__(self, other: "Timestamp"):
|
|
66
|
+
return self.unix_millis < other.unix_millis
|
|
67
|
+
|
|
68
|
+
def __gt__(self, other: "Timestamp"):
|
|
69
|
+
return self.unix_millis > other.unix_millis
|
|
70
|
+
|
|
71
|
+
def __le__(self, other: "Timestamp"):
|
|
72
|
+
return self.unix_millis <= other.unix_millis
|
|
73
|
+
|
|
74
|
+
def __ge__(self, other: "Timestamp"):
|
|
75
|
+
return self.unix_millis >= other.unix_millis
|
|
76
|
+
|
|
77
|
+
def __eq__(self, other: Any):
|
|
78
|
+
if isinstance(other, Timestamp):
|
|
79
|
+
return other.unix_millis == self.unix_millis
|
|
80
|
+
return NotImplemented
|
|
81
|
+
|
|
82
|
+
def __hash__(self):
|
|
83
|
+
return hash(("ts", self.unix_millis))
|
|
84
|
+
|
|
85
|
+
def __repr__(self) -> str:
|
|
86
|
+
iso = self._iso_format()
|
|
87
|
+
if iso:
|
|
88
|
+
return f"Timestamp(\n unix_millis={self.unix_millis},\n _formatted='{iso}',\n)"
|
|
89
|
+
else:
|
|
90
|
+
return f"Timestamp(unix_millis={self.unix_millis})"
|
|
91
|
+
|
|
92
|
+
def __setattr__(self, name: str, value: Any):
|
|
93
|
+
raise dataclasses.FrozenInstanceError(self.__class__.__qualname__)
|
|
94
|
+
|
|
95
|
+
def __delattr__(self, name: str):
|
|
96
|
+
raise dataclasses.FrozenInstanceError(self.__class__.__qualname__)
|
|
97
|
+
|
|
98
|
+
def _trj(self) -> Any:
|
|
99
|
+
"""To readable JSON."""
|
|
100
|
+
iso = self._iso_format
|
|
101
|
+
if iso:
|
|
102
|
+
return {
|
|
103
|
+
"unix_millis": self.unix_millis,
|
|
104
|
+
"formatted": iso,
|
|
105
|
+
}
|
|
106
|
+
else:
|
|
107
|
+
return {
|
|
108
|
+
"unix_millis": self.unix_millis,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def _iso_format(self) -> str:
|
|
112
|
+
try:
|
|
113
|
+
dt = self.to_datetime_or_raise()
|
|
114
|
+
except:
|
|
115
|
+
return ""
|
|
116
|
+
if dt:
|
|
117
|
+
ret = dt.isoformat()
|
|
118
|
+
bad_suffix = "+00:00"
|
|
119
|
+
if ret.endswith(bad_suffix):
|
|
120
|
+
ret = ret[0 : -len(bad_suffix)] + "Z"
|
|
121
|
+
return ret
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Use 'setattr' because we marked these class attributes as Final.
|
|
125
|
+
setattr(Timestamp, "EPOCH", Timestamp.from_unix_millis(0))
|
|
126
|
+
setattr(Timestamp, "MIN", Timestamp.from_unix_millis(-8640000000000000))
|
|
127
|
+
setattr(Timestamp, "MAX", Timestamp.from_unix_millis(8640000000000000))
|