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,196 @@
1
+ import itertools
2
+ from dataclasses import dataclass
3
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
4
+
5
+
6
+ def make_function(
7
+ name: str,
8
+ params: Sequence[Union[str, "Param"]],
9
+ body: Sequence[Union[str, "Line"]],
10
+ ) -> Callable:
11
+ params = [Param(p, default=None) if isinstance(p, str) else p for p in params]
12
+
13
+ def make_locals() -> _Locals:
14
+ # First, collect all the (key, value) pairs.
15
+ # There may be both key duplicates and value duplicates. They will be resolved
16
+ # later.
17
+ all_pieces: list[Union[str, _Local]] = []
18
+ for param in params:
19
+ if param._default is not None:
20
+ all_pieces.extend(param._default._pieces)
21
+ for line in body:
22
+ if isinstance(line, LineSpan):
23
+ all_pieces.extend(line._pieces)
24
+ all_locals = (p for p in all_pieces if isinstance(p, _Local))
25
+
26
+ locals: dict[str, Any] = {}
27
+ reversed: dict[int, str] = {}
28
+ prefix_to_count: dict[str, int] = {}
29
+ for local in all_locals:
30
+ name = local.name
31
+ value = local.value
32
+ if id(value) in reversed:
33
+ # We already have this value, either under the same name or under a
34
+ # different name. Either way, we can reuse the name we already have.
35
+ continue
36
+ if name.endswith("?"):
37
+ # Replace the "?" suffix with a number obtained from a sequence so there
38
+ # is no name conflict.
39
+ prefix = name[0:-1]
40
+ count = prefix_to_count.get(prefix, 0)
41
+ prefix_to_count[prefix] = count + 1
42
+ name = prefix + str(count)
43
+ if name in locals:
44
+ raise ValueError(f"Duplicate local: name={name}; value={value}")
45
+ locals[name] = value
46
+ reversed[id(value)] = name
47
+
48
+ return _Locals(locals, reversed)
49
+
50
+ locals = make_locals()
51
+
52
+ def line_to_code(l: Union[str, Line]) -> str:
53
+ if isinstance(l, str):
54
+ return l
55
+ return l._to_code(locals)
56
+
57
+ body_str = "\n ".join(line_to_code(l) for l in body) if body else "pass"
58
+ text = f"""
59
+ def __create_function__({', '.join(locals.locals.keys())}):
60
+ def {name}({', '.join(p._to_code(locals) for p in params)}):
61
+ {body_str}
62
+ return {name}
63
+ """
64
+ ns = {}
65
+ exec(text, None, ns)
66
+ return ns["__create_function__"](**locals.locals)
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class LineSpan:
71
+ "An immutable span within a line of Python code."
72
+
73
+ _pieces: tuple[Union[str, "_Local"], ...]
74
+
75
+ @staticmethod
76
+ def join(*spans: Union[str, "LineSpan"], separator: str = "") -> "LineSpan":
77
+ def get_pieces(
78
+ span: Union[str, "LineSpan"],
79
+ ) -> tuple[Union[str, "_Local"], ...]:
80
+ if isinstance(span, LineSpan):
81
+ return span._pieces
82
+ return ((span),)
83
+
84
+ # Remove empty strings.
85
+ spans = tuple(s for s in spans if s)
86
+ if len(spans) == 0:
87
+ return _EMPTY_LINE_SPAN
88
+ elif len(spans) == 1:
89
+ only_span = spans[0]
90
+ if isinstance(only_span, LineSpan):
91
+ return only_span
92
+ return LineSpan((only_span,))
93
+ if separator:
94
+ pieces = itertools.chain(
95
+ get_pieces(spans[0]),
96
+ *itertools.chain(
97
+ itertools.chain((separator,), get_pieces(s)) for s in spans[1:]
98
+ ),
99
+ )
100
+ else:
101
+ pieces = itertools.chain(*(get_pieces(s) for s in spans))
102
+ return LineSpan(tuple(pieces))
103
+
104
+ @staticmethod
105
+ def local(name: str, value: Any):
106
+ return LineSpan((_Local(name, value),))
107
+
108
+ def _to_code(self, locals: "_Locals") -> str:
109
+ ret = ""
110
+ for piece in self._pieces:
111
+ if isinstance(piece, _Local):
112
+ # Look up the resolved name for the value.
113
+ # It migth be different from `piece.name` in some edge cases.
114
+ ret += locals.reversed[id(piece.value)]
115
+ else:
116
+ ret += piece
117
+ return ret
118
+
119
+
120
+ _EMPTY_LINE_SPAN = LineSpan(())
121
+
122
+
123
+ # TODO: comment
124
+ LineSpanLike = Union[str, LineSpan]
125
+
126
+
127
+ # A type alias to use when the line span is a Python expression.
128
+ Expr = LineSpan
129
+
130
+
131
+ # TODO: comment
132
+ ExprLike = LineSpanLike
133
+
134
+
135
+ # A type alias to use when the line span is the whole line.
136
+ Line = LineSpan
137
+
138
+
139
+ # An immutable sequence of lines.
140
+ BodySpan = tuple[Union[str, Line], ...]
141
+
142
+
143
+ # A type alias to use when the lines constitute the whole body of a function.
144
+ Body = BodySpan
145
+
146
+
147
+ class BodyBuilder:
148
+ """Arnold Schwarzenegger."""
149
+
150
+ _lines: list[LineSpan]
151
+
152
+ def __init__(self):
153
+ self._lines = []
154
+
155
+ def append_ln(self, *spans: Union[str, LineSpan]):
156
+ self._lines.append(Line.join(*spans))
157
+
158
+ def extend(self, other: Iterable[Union[str, Line]], indent: str = ""):
159
+ self._lines.extend(Line.join(indent, l) for l in other)
160
+ return self
161
+
162
+ def build(self) -> Body:
163
+ return tuple(self._lines)
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class Param:
168
+ name: str
169
+ default: Optional[ExprLike] = None
170
+
171
+ def _to_code(self, locals: "_Locals") -> str:
172
+ if self._default is None:
173
+ return self.name
174
+ return f"{self.name}={self._default._to_code(locals)}"
175
+
176
+ @property
177
+ def _default(self) -> Optional[Expr]:
178
+ if self.default is None:
179
+ return None
180
+ else:
181
+ return Expr.join(self.default)
182
+
183
+
184
+ Params = list[Union[str, Param]]
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class _Local:
189
+ name: str
190
+ value: Any
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class _Locals:
195
+ locals: dict[str, Any]
196
+ reversed: dict[Any, str]
@@ -0,0 +1,74 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import TypeVar
4
+ from weakref import WeakKeyDictionary
5
+
6
+ from soialib import spec
7
+ from soialib.impl.encoding import NULL_WIRE
8
+ from soialib.impl.function_maker import Expr, ExprLike
9
+ from soialib.impl.type_adapter import TypeAdapter
10
+
11
+ Other = TypeVar("Other")
12
+
13
+
14
+ def get_optional_adapter(other_adapter: TypeAdapter) -> TypeAdapter:
15
+ return _other_adapter_to_optional_adapter.setdefault(
16
+ other_adapter, _OptionalAdapter(other_adapter)
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class _OptionalAdapter(TypeAdapter):
22
+ __slots__ = ("other_adapter",)
23
+
24
+ other_adapter: TypeAdapter
25
+
26
+ def default_expr(self) -> ExprLike:
27
+ return "None"
28
+
29
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
30
+ return Expr.join(
31
+ "(None if ",
32
+ arg_expr,
33
+ " is None else ",
34
+ self.other_adapter.to_frozen_expr(arg_expr),
35
+ ")",
36
+ )
37
+
38
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
39
+ return Expr.join(arg_expr, " is not None")
40
+
41
+ def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
42
+ other_to_json = self.other_adapter.to_json_expr(in_expr, readable)
43
+ if other_to_json == in_expr:
44
+ return in_expr
45
+ return Expr.join(
46
+ "(None if ",
47
+ in_expr,
48
+ " is None else ",
49
+ other_to_json,
50
+ ")",
51
+ )
52
+
53
+ def from_json_expr(self, json_expr: ExprLike) -> ExprLike:
54
+ other_from_json = self.other_adapter.from_json_expr(json_expr)
55
+ if other_from_json == json_expr:
56
+ return json_expr
57
+ return Expr.join(
58
+ "(None if ",
59
+ json_expr,
60
+ " is None else ",
61
+ other_from_json,
62
+ ")",
63
+ )
64
+
65
+ def finalize(
66
+ self,
67
+ resolve_type_fn: Callable[[spec.Type], "TypeAdapter"],
68
+ ) -> None:
69
+ self.other_adapter.finalize(resolve_type_fn)
70
+
71
+
72
+ _other_adapter_to_optional_adapter: WeakKeyDictionary[TypeAdapter, TypeAdapter] = (
73
+ WeakKeyDictionary()
74
+ )
@@ -0,0 +1,210 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import Any, Final, final
4
+
5
+ from soialib import spec
6
+ from soialib.impl.function_maker import Expr, ExprLike
7
+ from soialib.impl.type_adapter import TypeAdapter
8
+ from soialib.timestamp import Timestamp
9
+
10
+
11
+ class AbstractPrimitiveAdapter(TypeAdapter):
12
+ @final
13
+ def finalize(
14
+ self,
15
+ resolve_type_fn: Callable[[spec.Type], "TypeAdapter"],
16
+ ) -> None:
17
+ pass
18
+
19
+
20
+ class _BoolAdapter(AbstractPrimitiveAdapter):
21
+ def default_expr(self) -> ExprLike:
22
+ return "False"
23
+
24
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
25
+ return Expr.join("(True if ", arg_expr, " else False)")
26
+
27
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
28
+ return arg_expr
29
+
30
+ def to_json_expr(
31
+ self,
32
+ in_expr: ExprLike,
33
+ readable: bool,
34
+ ) -> ExprLike:
35
+ if readable:
36
+ return Expr.join("(1 if ", in_expr, " else 0)")
37
+ else:
38
+ return in_expr
39
+
40
+ def from_json_expr(self, json_expr: ExprLike) -> Expr:
41
+ return Expr.join("(True if ", json_expr, " else False)")
42
+
43
+
44
+ BOOL_ADAPTER: Final[TypeAdapter] = _BoolAdapter()
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class _AbstractIntAdapter(AbstractPrimitiveAdapter):
49
+ """Type adapter implementation for int32, int64 and uint64."""
50
+
51
+ def default_expr(self) -> ExprLike:
52
+ return "0"
53
+
54
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
55
+ return Expr.join("(", arg_expr, " | 0)")
56
+
57
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
58
+ return arg_expr
59
+
60
+ def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
61
+ return in_expr
62
+
63
+ def from_json_expr(self, json_expr: ExprLike) -> Expr:
64
+ return Expr.join("(", json_expr, " | 0)")
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class _Int32Adapter(_AbstractIntAdapter):
69
+ pass
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class _Int64Adapter(_AbstractIntAdapter):
74
+ pass
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class _Uint64Adapter(_AbstractIntAdapter):
79
+ pass
80
+
81
+
82
+ INT32_ADAPTER: Final[TypeAdapter] = _Int32Adapter()
83
+ INT64_ADAPTER: Final[TypeAdapter] = _Int64Adapter()
84
+ UINT64_ADAPTER: Final[TypeAdapter] = _Uint64Adapter()
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class _AbstractFloatAdapter(AbstractPrimitiveAdapter):
89
+ """Type adapter implementation for float32 and float64."""
90
+
91
+ def default_expr(self) -> ExprLike:
92
+ return "0.0"
93
+
94
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
95
+ return Expr.join("(", arg_expr, " + 0.0)")
96
+
97
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
98
+ return arg_expr
99
+
100
+ def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
101
+ return in_expr
102
+
103
+ def from_json_expr(self, json_expr: ExprLike) -> Expr:
104
+ return Expr.join("(", json_expr, " + 0.0)")
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class _Float32Adapter(_AbstractFloatAdapter):
109
+ """Type adapter implementation for float32."""
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class _Float64Adapter(_AbstractFloatAdapter):
114
+ """Type adapter implementation for float32."""
115
+
116
+
117
+ FLOAT32_ADAPTER: Final[TypeAdapter] = _Float32Adapter()
118
+ FLOAT64_ADAPTER: Final[TypeAdapter] = _Float64Adapter()
119
+
120
+
121
+ class _TimestampAdapter(AbstractPrimitiveAdapter):
122
+ def default_expr(self) -> Expr:
123
+ return Expr.local("_EPOCH", Timestamp.EPOCH)
124
+
125
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
126
+ timestamp_local = Expr.local("Timestamp", Timestamp)
127
+ return Expr.join(
128
+ "(",
129
+ arg_expr,
130
+ " if ",
131
+ arg_expr,
132
+ ".__class__ is ",
133
+ timestamp_local,
134
+ " else ",
135
+ timestamp_local,
136
+ "(unix_millis=",
137
+ arg_expr,
138
+ ".unix_millis))",
139
+ )
140
+
141
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
142
+ return Expr.join(arg_expr, ".unix_millis")
143
+
144
+ def to_json_expr(self, in_expr: ExprLike, readable: bool) -> Expr:
145
+ if readable:
146
+ return Expr.join(in_expr, "._trj()")
147
+ else:
148
+ return Expr.join(in_expr, ".unix_millis")
149
+
150
+ def from_json_expr(self, json_expr: ExprLike) -> Expr:
151
+ fn = Expr.local("_timestamp_from_json", _timestamp_from_json)
152
+ return Expr.join(fn, "(", json_expr, ")")
153
+
154
+
155
+ def _timestamp_from_json(json: Any) -> Timestamp:
156
+ if json.__class__ is int or isinstance(json, int):
157
+ return Timestamp(unix_millis=json)
158
+ else:
159
+ return Timestamp(unix_millis=json["unix_millis"])
160
+
161
+
162
+ TIMESTAMP_ADAPTER: Final[TypeAdapter] = _TimestampAdapter()
163
+
164
+
165
+ class _StringAdapter(AbstractPrimitiveAdapter):
166
+ def default_expr(self) -> ExprLike:
167
+ return '""'
168
+
169
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
170
+ return Expr.join("('' + ", arg_expr, ")")
171
+
172
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
173
+ return arg_expr
174
+
175
+ def to_json_expr(self, in_expr: ExprLike, readable: bool) -> ExprLike:
176
+ return in_expr
177
+
178
+ def from_json_expr(self, json_expr: ExprLike) -> Expr:
179
+ return Expr.join("('' + (", json_expr, " or ''))")
180
+
181
+
182
+ STRING_ADAPTER: Final[TypeAdapter] = _StringAdapter()
183
+
184
+
185
+ class _BytesAdapter(AbstractPrimitiveAdapter):
186
+ _fromhex_fn: Final = bytes.fromhex
187
+
188
+ def default_expr(self) -> ExprLike:
189
+ return 'b""'
190
+
191
+ def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
192
+ return Expr.join("(b'' + ", arg_expr, ")")
193
+
194
+ def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
195
+ return arg_expr
196
+
197
+ def to_json_expr(
198
+ self,
199
+ in_expr: ExprLike,
200
+ readable: bool,
201
+ ) -> Expr:
202
+ return Expr.join(in_expr, ".hex()")
203
+
204
+ def from_json_expr(self, json_expr: ExprLike) -> Expr:
205
+ return Expr.join(
206
+ Expr.local("fromhex", _BytesAdapter._fromhex_fn), "(", json_expr, ")"
207
+ )
208
+
209
+
210
+ BYTES_ADAPTER: Final[TypeAdapter] = _BytesAdapter()
soialib/impl/repr.py ADDED
@@ -0,0 +1,43 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+
6
+ @dataclass
7
+ class ReprResult:
8
+ repr: str
9
+ complex: bool
10
+
11
+ @property
12
+ def indented(self) -> str:
13
+ if complex:
14
+ return self.repr.replace("\n", "\n ")
15
+ else:
16
+ return self.repr
17
+
18
+
19
+ def repr_impl(input: Any) -> ReprResult:
20
+ if isinstance(input, list) or isinstance(input, tuple):
21
+ if len(input) == 0:
22
+ return ReprResult("[]", complex=False)
23
+ elif len(input) == 1:
24
+ only_item_repr = repr_impl(input[0])
25
+ if only_item_repr.complex:
26
+ return ReprResult(f"[\n {only_item_repr.indented},\n]", complex=True)
27
+ else:
28
+ return ReprResult(f"[{only_item_repr.repr}]", complex=True)
29
+ else:
30
+ body = "".join(f" {repr_impl(item).indented},\n" for item in input)
31
+ return ReprResult(f"[\n{body}]", complex=True)
32
+ elif isinstance(input, str):
33
+ if "\n" in input:
34
+ # TODO: comment
35
+ lines = input.split("\n")
36
+ body = "".join(f" {repr(line)},\n" for line in lines)
37
+ return ReprResult(f"'\\n'.join([\n{body}])", complex=True)
38
+ else:
39
+ return ReprResult(repr(input), complex=False)
40
+ else:
41
+ r = repr(input)
42
+ # Complex if the representation contains '[', '(' or '\n'.
43
+ return ReprResult(r, complex=re.search(r"[\\[\\(\\\n]", r) is not None)