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.
@@ -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
@@ -0,0 +1,4 @@
1
+ from typing import NoReturn
2
+
3
+
4
+ Never = NoReturn
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))