lythonic 0.0.1__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.
- lythonic/__init__.py +198 -0
- lythonic/logic.py +23 -0
- lythonic/misc.py +21 -0
- lythonic/periodic.py +380 -0
- lythonic/py.typed +0 -0
- lythonic/state.py +249 -0
- lythonic/types.py +461 -0
- lythonic-0.0.1.dist-info/METADATA +101 -0
- lythonic-0.0.1.dist-info/RECORD +12 -0
- lythonic-0.0.1.dist-info/WHEEL +4 -0
- lythonic-0.0.1.dist-info/entry_points.txt +2 -0
- lythonic-0.0.1.dist-info/licenses/LICENSE +21 -0
lythonic/__init__.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from inspect import isclass, iscoroutinefunction, isfunction, ismodule
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from typing import Annotated, Any, Generic, TypeVar, final
|
|
6
|
+
|
|
7
|
+
from pydantic import (
|
|
8
|
+
AfterValidator,
|
|
9
|
+
GetCoreSchemaHandler,
|
|
10
|
+
PlainSerializer,
|
|
11
|
+
WithJsonSchema,
|
|
12
|
+
)
|
|
13
|
+
from pydantic_core import CoreSchema, core_schema
|
|
14
|
+
from typing_extensions import override
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GlobalRef:
|
|
20
|
+
"""
|
|
21
|
+
>>> ref = GlobalRef('lythonic:GlobalRef')
|
|
22
|
+
>>> ref
|
|
23
|
+
GlobalRef('lythonic:GlobalRef')
|
|
24
|
+
>>> ref.get_instance().__name__
|
|
25
|
+
'GlobalRef'
|
|
26
|
+
>>> ref.is_module()
|
|
27
|
+
False
|
|
28
|
+
>>> ref.get_module().__name__
|
|
29
|
+
'lythonic'
|
|
30
|
+
>>> grgr = GlobalRef(GlobalRef)
|
|
31
|
+
>>> grgr
|
|
32
|
+
GlobalRef('lythonic:GlobalRef')
|
|
33
|
+
>>> grgr.get_instance()
|
|
34
|
+
<class 'lythonic.GlobalRef'>
|
|
35
|
+
>>> grgr.is_class()
|
|
36
|
+
True
|
|
37
|
+
>>> grgr.is_function()
|
|
38
|
+
False
|
|
39
|
+
>>> grgr.is_module()
|
|
40
|
+
False
|
|
41
|
+
>>> uref = GlobalRef('lythonic:')
|
|
42
|
+
>>> uref.is_module()
|
|
43
|
+
True
|
|
44
|
+
>>> uref.get_module().__name__
|
|
45
|
+
'lythonic'
|
|
46
|
+
>>> uref = GlobalRef('lythonic')
|
|
47
|
+
>>> uref.is_module()
|
|
48
|
+
True
|
|
49
|
+
>>> uref = GlobalRef(uref)
|
|
50
|
+
>>> uref.is_module()
|
|
51
|
+
True
|
|
52
|
+
>>> uref.get_module().__name__
|
|
53
|
+
'lythonic'
|
|
54
|
+
>>> uref = GlobalRef(uref.get_module())
|
|
55
|
+
>>> uref.is_module()
|
|
56
|
+
True
|
|
57
|
+
>>> uref.get_module().__name__
|
|
58
|
+
'lythonic'
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
module: str
|
|
62
|
+
name: str
|
|
63
|
+
|
|
64
|
+
def __init__(self, s: Any) -> None:
|
|
65
|
+
if isinstance(s, GlobalRef):
|
|
66
|
+
self.module, self.name = s.module, s.name
|
|
67
|
+
elif ismodule(s):
|
|
68
|
+
self.module, self.name = s.__name__, ""
|
|
69
|
+
elif isclass(s) or isfunction(s):
|
|
70
|
+
self.module, self.name = s.__module__, s.__name__
|
|
71
|
+
else:
|
|
72
|
+
split = s.split(":")
|
|
73
|
+
if len(split) == 1:
|
|
74
|
+
assert bool(split[0]), f"is {repr(s)} empty?"
|
|
75
|
+
split.append("")
|
|
76
|
+
else:
|
|
77
|
+
assert len(split) == 2, f"too many ':' in: {repr(s)}"
|
|
78
|
+
self.module, self.name = split
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def __str__(self):
|
|
82
|
+
return f"{self.module}:{self.name}"
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
def __repr__(self):
|
|
86
|
+
return f"{self.__class__.__name__}({repr(str(self))})"
|
|
87
|
+
|
|
88
|
+
def get_module(self) -> ModuleType:
|
|
89
|
+
return __import__(self.module, fromlist=[""])
|
|
90
|
+
|
|
91
|
+
def is_module(self) -> bool:
|
|
92
|
+
return not (self.name)
|
|
93
|
+
|
|
94
|
+
def is_class(self) -> bool:
|
|
95
|
+
return not (self.is_module()) and isclass(self.get_instance())
|
|
96
|
+
|
|
97
|
+
def is_function(self) -> bool:
|
|
98
|
+
return not (self.is_module()) and isfunction(self.get_instance())
|
|
99
|
+
|
|
100
|
+
def is_async(self) -> bool:
|
|
101
|
+
if self.is_module():
|
|
102
|
+
return False
|
|
103
|
+
if self.is_class():
|
|
104
|
+
return iscoroutinefunction(self.get_instance().__call__)
|
|
105
|
+
return iscoroutinefunction(self.get_instance())
|
|
106
|
+
|
|
107
|
+
def get_instance(self) -> Any:
|
|
108
|
+
assert not self.is_module(), f"{repr(self)}.get_module() only"
|
|
109
|
+
attr = getattr(self.get_module(), self.name)
|
|
110
|
+
return attr
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def __get_pydantic_core_schema__(
|
|
114
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
|
115
|
+
) -> CoreSchema:
|
|
116
|
+
return core_schema.no_info_after_validator_function(cls, handler(str))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
GRef = Annotated[
|
|
120
|
+
GlobalRef,
|
|
121
|
+
PlainSerializer(str, return_type=str),
|
|
122
|
+
AfterValidator(GlobalRef),
|
|
123
|
+
WithJsonSchema({"type": "string"}, mode="serialization"),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_module(name: str) -> ModuleType:
|
|
128
|
+
"""
|
|
129
|
+
>>> type(get_module('lythonic'))
|
|
130
|
+
<class 'module'>
|
|
131
|
+
>>> get_module('lythonic.c99')
|
|
132
|
+
Traceback (most recent call last):
|
|
133
|
+
...
|
|
134
|
+
ModuleNotFoundError: No module named 'lythonic.c99'
|
|
135
|
+
"""
|
|
136
|
+
if name in sys.modules:
|
|
137
|
+
return sys.modules[name]
|
|
138
|
+
return __import__(name, fromlist=[""])
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
TOk = TypeVar("TOk")
|
|
142
|
+
TErr = TypeVar("TErr")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@final
|
|
146
|
+
class Result(Generic[TOk, TErr]):
|
|
147
|
+
"""
|
|
148
|
+
A generic Result type inspired by Rust, representing either success (Ok) or failure (Err).
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
_ok: TOk | None
|
|
152
|
+
_err: TErr | None
|
|
153
|
+
|
|
154
|
+
__slots__ = ("_ok", "_err")
|
|
155
|
+
|
|
156
|
+
def __init__(self, ok: TOk | None = None, err: TErr | None = None) -> None:
|
|
157
|
+
assert (ok is None and err is not None) or (ok is not None and err is None), (
|
|
158
|
+
"Result can only have one of ok or err set."
|
|
159
|
+
)
|
|
160
|
+
self._ok = ok
|
|
161
|
+
self._err = err
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def Ok(cls, value: TOk) -> "Result[TOk, TErr]":
|
|
165
|
+
return cls(ok=value)
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def Err(cls, error: TErr) -> "Result[TOk, TErr]":
|
|
169
|
+
return cls(err=error)
|
|
170
|
+
|
|
171
|
+
def is_ok(self) -> bool:
|
|
172
|
+
return self._ok is not None
|
|
173
|
+
|
|
174
|
+
def is_err(self) -> bool:
|
|
175
|
+
return self._err is not None
|
|
176
|
+
|
|
177
|
+
def ok(self) -> TOk | None:
|
|
178
|
+
return self._ok
|
|
179
|
+
|
|
180
|
+
def err(self) -> TErr | None:
|
|
181
|
+
return self._err
|
|
182
|
+
|
|
183
|
+
def unwrap(self) -> TOk:
|
|
184
|
+
if self._ok is not None:
|
|
185
|
+
return self._ok
|
|
186
|
+
raise ValueError(f"Called unwrap on Err: {self._err}")
|
|
187
|
+
|
|
188
|
+
def unwrap_err(self) -> TErr:
|
|
189
|
+
if self._err is not None:
|
|
190
|
+
return self._err
|
|
191
|
+
raise ValueError(f"Called unwrap_err on Ok: {self._ok}")
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
def __repr__(self):
|
|
195
|
+
if self.is_ok():
|
|
196
|
+
return f"Ok({self._ok!r})"
|
|
197
|
+
else:
|
|
198
|
+
return f"Err({self._err!r})"
|
lythonic/logic.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @final
|
|
2
|
+
# class Logic:
|
|
3
|
+
# def __init__(self, config: dict[str, Any], default_ref: str | GlobalRef | None = None) -> None:
|
|
4
|
+
# config = dict(config)
|
|
5
|
+
# try:
|
|
6
|
+
# if default_ref is not None:
|
|
7
|
+
# ref = GlobalRef(config.pop("ref$", default_ref))
|
|
8
|
+
# else:
|
|
9
|
+
# ref = GlobalRef(config.pop("ref$"))
|
|
10
|
+
# self.async_call = ref.is_async()
|
|
11
|
+
# if ref.is_function():
|
|
12
|
+
# self.instance = None
|
|
13
|
+
# self.call = ref.get_instance()
|
|
14
|
+
# assert config == {}, f"Unexpected entries {config}"
|
|
15
|
+
# elif ref.is_class():
|
|
16
|
+
# cls = ref.get_instance()
|
|
17
|
+
# self.call = self.instance = cls(config)
|
|
18
|
+
# else:
|
|
19
|
+
# raise AssertionError(f"Invalid logic {ref} in config {config}") # pragma: no cover
|
|
20
|
+
# except BaseException:
|
|
21
|
+
# log.error(f"Error in {config}")
|
|
22
|
+
# raise
|
|
23
|
+
|
lythonic/misc.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def ensure_dir(dir: Path | str) -> Path:
|
|
6
|
+
dir = Path(dir)
|
|
7
|
+
dir.mkdir(parents=True, exist_ok=True)
|
|
8
|
+
return dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def tabula_rasa_path(p: Path | str) -> Path:
|
|
12
|
+
"""Make sure that path does not exists, but parent dir does"""
|
|
13
|
+
p = Path(p)
|
|
14
|
+
if p.exists():
|
|
15
|
+
if p.is_dir():
|
|
16
|
+
shutil.rmtree(p)
|
|
17
|
+
else:
|
|
18
|
+
p.unlink()
|
|
19
|
+
if not p.parent.is_dir():
|
|
20
|
+
ensure_dir(p.parent)
|
|
21
|
+
return p
|
lythonic/periodic.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import time as tt
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Annotated, Any, final
|
|
8
|
+
|
|
9
|
+
from pydantic import BeforeValidator, PlainSerializer, WithJsonSchema
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
|
|
12
|
+
from lythonic.types import str_or_none
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from datetime import UTC, date, datetime, timedelta
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
YEAR_IN_DAYS = 365.256
|
|
22
|
+
SECONDS_IN_DAY = 24 * 60 * 60
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
EPOCH_ZERO = datetime(1970, 1, 1, tzinfo=UTC)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def total_microseconds(d: timedelta) -> int:
|
|
29
|
+
return (d.days * SECONDS_IN_DAY + d.seconds) * 1_000_000 + d.microseconds
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def stamp_time() -> datetime:
|
|
33
|
+
"""return the current time in UTC
|
|
34
|
+
>>> stamp_time().tzinfo
|
|
35
|
+
datetime.timezone.utc
|
|
36
|
+
"""
|
|
37
|
+
return datetime.now(UTC)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dt_to_bytes(dt: datetime) -> bytes:
|
|
41
|
+
"""Convert datetime to bytes
|
|
42
|
+
>>> dt_to_bytes(datetime( 1900,1,1,0,0,0))
|
|
43
|
+
b'\\xff\\xf8&\\xef\\xb7C`\\x00'
|
|
44
|
+
>>> dt_to_bytes(datetime( 2000,1,1,0,0,0))
|
|
45
|
+
b'\\x00\\x03]\\x01;7\\xe0\\x00'
|
|
46
|
+
"""
|
|
47
|
+
if dt.tzinfo is None:
|
|
48
|
+
dt = dt.replace(tzinfo=UTC)
|
|
49
|
+
mics = total_microseconds(dt - EPOCH_ZERO)
|
|
50
|
+
return mics.to_bytes(8, "big", signed=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def dt_from_bytes(b: bytes) -> datetime:
|
|
54
|
+
"""Convert bytes to datetime
|
|
55
|
+
>>> dt_from_bytes(b'\\xff\\xf8&\\xef\\xb7C`\\x00')
|
|
56
|
+
datetime.datetime(1900, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
|
57
|
+
>>> dt_from_bytes(b'\\x00\\x03]\\x01;7\\xe0\\x00')
|
|
58
|
+
datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
|
59
|
+
"""
|
|
60
|
+
mics = int.from_bytes(b, "big", signed=True)
|
|
61
|
+
return EPOCH_ZERO + timedelta(microseconds=mics)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
DT_BYTES_LENGTH = len(dt_to_bytes(stamp_time()))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SimulatedTime:
|
|
68
|
+
"""
|
|
69
|
+
>>> st = SimulatedTime()
|
|
70
|
+
>>> st.get_datetime().tzinfo
|
|
71
|
+
datetime.timezone.utc
|
|
72
|
+
>>> cmp = lambda ss: abs((st.get_datetime()-stamp_time()).total_seconds()-ss) < 1e-3
|
|
73
|
+
>>> cmp(0)
|
|
74
|
+
True
|
|
75
|
+
>>> st.set_offset(timedelta(days=1))
|
|
76
|
+
>>> cmp(86400)
|
|
77
|
+
True
|
|
78
|
+
>>> st.set_offset(timedelta(days=1).total_seconds())
|
|
79
|
+
>>> cmp(86400)
|
|
80
|
+
True
|
|
81
|
+
>>> st.set_now(stamp_time() + timedelta(days=1))
|
|
82
|
+
>>> cmp(86400)
|
|
83
|
+
True
|
|
84
|
+
>>> st.set_now( (stamp_time() - timedelta(days=1)).timestamp() )
|
|
85
|
+
>>> cmp(-86400)
|
|
86
|
+
True
|
|
87
|
+
>>> st.is_real_time()
|
|
88
|
+
False
|
|
89
|
+
>>> st.reset()
|
|
90
|
+
>>> st.is_real_time()
|
|
91
|
+
True
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
offset: float
|
|
95
|
+
|
|
96
|
+
def __init__(self, offset: float = 0.0) -> None:
|
|
97
|
+
self.offset = offset
|
|
98
|
+
|
|
99
|
+
def time(self):
|
|
100
|
+
return tt.time() + self.offset
|
|
101
|
+
|
|
102
|
+
def set_offset(self, offset: timedelta | float):
|
|
103
|
+
if isinstance(offset, timedelta):
|
|
104
|
+
self.offset = offset.total_seconds()
|
|
105
|
+
else:
|
|
106
|
+
self.offset = offset
|
|
107
|
+
|
|
108
|
+
def set_now(self, dt: datetime | float):
|
|
109
|
+
if isinstance(dt, datetime):
|
|
110
|
+
epoch = dt.timestamp()
|
|
111
|
+
else:
|
|
112
|
+
epoch = dt
|
|
113
|
+
self.offset = epoch - tt.time()
|
|
114
|
+
|
|
115
|
+
def reset(self):
|
|
116
|
+
self.offset = 0.0
|
|
117
|
+
|
|
118
|
+
def is_real_time(self):
|
|
119
|
+
return self.offset == 0.0
|
|
120
|
+
|
|
121
|
+
def get_datetime(self) -> datetime:
|
|
122
|
+
return datetime.fromtimestamp(self.time(), tz=UTC)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
stime: SimulatedTime = SimulatedTime()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class IntervalUnit(Enum):
|
|
129
|
+
"""
|
|
130
|
+
>>> IntervalUnit.D
|
|
131
|
+
IntervalUnit.D
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
D = 1
|
|
135
|
+
W = 7
|
|
136
|
+
M = YEAR_IN_DAYS / 12
|
|
137
|
+
Q = YEAR_IN_DAYS / 4
|
|
138
|
+
Y = YEAR_IN_DAYS
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_string(cls, n: str) -> "IntervalUnit":
|
|
142
|
+
return cls[n.upper()]
|
|
143
|
+
|
|
144
|
+
def timedelta(self) -> timedelta:
|
|
145
|
+
return timedelta(days=self.value)
|
|
146
|
+
|
|
147
|
+
@override
|
|
148
|
+
def __str__(self) -> str:
|
|
149
|
+
return self.name
|
|
150
|
+
|
|
151
|
+
@override
|
|
152
|
+
def __repr__(self) -> str:
|
|
153
|
+
return f"IntervalUnit.{str(self)}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@final
|
|
157
|
+
class Interval:
|
|
158
|
+
_P = "".join(p.name for p in IntervalUnit)
|
|
159
|
+
FREQ_RE = re.compile(r"(\d+)([" + _P + _P.lower() + "])")
|
|
160
|
+
|
|
161
|
+
multiplier: int
|
|
162
|
+
period: IntervalUnit
|
|
163
|
+
|
|
164
|
+
def __init__(self, multiplier: int, period: IntervalUnit) -> None:
|
|
165
|
+
self.multiplier = multiplier
|
|
166
|
+
self.period = period
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_string_safe(cls, s: "Interval|str|None") -> "Interval | None":
|
|
170
|
+
if s is None:
|
|
171
|
+
return None
|
|
172
|
+
if isinstance(s, Interval):
|
|
173
|
+
return s
|
|
174
|
+
return cls.from_string(s)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_string(cls, s: str) -> "Interval":
|
|
178
|
+
m = cls.matcher(s)
|
|
179
|
+
if m:
|
|
180
|
+
n, p = m.groups()
|
|
181
|
+
return cls(int(n), IntervalUnit.from_string(p))
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError("Invalid frequency string", s)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def matcher(cls, s: str) -> re.Match[str] | None:
|
|
187
|
+
return re.match(cls.FREQ_RE, s)
|
|
188
|
+
|
|
189
|
+
def timedelta(self) -> timedelta:
|
|
190
|
+
return self.multiplier * self.period.timedelta()
|
|
191
|
+
|
|
192
|
+
def match(self, d: date, as_of: date) -> bool:
|
|
193
|
+
return d <= as_of and d + self.timedelta() > as_of
|
|
194
|
+
|
|
195
|
+
def find_file(
|
|
196
|
+
self,
|
|
197
|
+
path: Path,
|
|
198
|
+
as_of: date | datetime,
|
|
199
|
+
suffix: str = ".csv",
|
|
200
|
+
) -> Path | None:
|
|
201
|
+
ff = list(
|
|
202
|
+
reversed(
|
|
203
|
+
sorted(
|
|
204
|
+
(date_from_name(f.name), f)
|
|
205
|
+
for f in path.glob(f"*{suffix}")
|
|
206
|
+
if re.match(r"^\d{8}", f.name[:8])
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
for d, f in ff:
|
|
211
|
+
if d <= as_of:
|
|
212
|
+
if self.match(d, as_of):
|
|
213
|
+
return f
|
|
214
|
+
else:
|
|
215
|
+
break
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
@override
|
|
219
|
+
def __str__(self) -> str:
|
|
220
|
+
return f"{self.multiplier}{self.period}"
|
|
221
|
+
|
|
222
|
+
@override
|
|
223
|
+
def __repr__(self) -> str:
|
|
224
|
+
return f"Interval({self.multiplier}, {self.period!r})"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
IntervalSafe = Annotated[
|
|
228
|
+
Interval | str | None,
|
|
229
|
+
BeforeValidator(Interval.from_string_safe),
|
|
230
|
+
PlainSerializer(str_or_none, return_type=str),
|
|
231
|
+
WithJsonSchema({"anyOf": [{"type": "string"}, {"type": "null"}]}),
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def date_from_name(s: str) -> date:
|
|
236
|
+
return date(int(s[:4]), int(s[4:6]), int(s[6:8]))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Moment:
|
|
240
|
+
"""
|
|
241
|
+
>>> m = Moment.start()
|
|
242
|
+
>>> m = m.capture("instant")
|
|
243
|
+
>>> tt.sleep(1)
|
|
244
|
+
>>> m = m.capture("a second")
|
|
245
|
+
>>> s = m.chain()
|
|
246
|
+
>>> s.startswith('[start] 0.0'), 's-> [instant] 1.' in s , s.endswith('s-> [a second]')
|
|
247
|
+
(True, True, True)
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
time: float
|
|
251
|
+
name: str
|
|
252
|
+
prev: "Moment | None"
|
|
253
|
+
|
|
254
|
+
def __init__(self, name: str, prev: "Moment | None" = None) -> None:
|
|
255
|
+
self.time = tt.time()
|
|
256
|
+
self.name = name
|
|
257
|
+
self.prev = prev
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def start():
|
|
261
|
+
"""capture the starting moment"""
|
|
262
|
+
return Moment("start")
|
|
263
|
+
|
|
264
|
+
def capture(self, name: str):
|
|
265
|
+
"""capture the named moment relative to this one"""
|
|
266
|
+
return Moment(name, self)
|
|
267
|
+
|
|
268
|
+
def elapsed(self):
|
|
269
|
+
"""return time in seconds since previous moment"""
|
|
270
|
+
if self.prev is None:
|
|
271
|
+
return 0
|
|
272
|
+
return self.time - self.prev.time
|
|
273
|
+
|
|
274
|
+
@override
|
|
275
|
+
def __str__(self):
|
|
276
|
+
return (
|
|
277
|
+
f" {self.elapsed():.3f}s-> [{self.name}]" if self.prev is not None else f"[{self.name}]"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def chain(self) -> str:
|
|
281
|
+
return str(self) if self.prev is None else self.prev.chain() + str(self)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class PeriodicTask:
|
|
285
|
+
freq: int
|
|
286
|
+
logic: Callable[[], Any]
|
|
287
|
+
last_run: float | None = None
|
|
288
|
+
|
|
289
|
+
def __init__(self, freq: int, logic: Callable[[], Any]) -> None:
|
|
290
|
+
self.freq = freq
|
|
291
|
+
self.logic = logic
|
|
292
|
+
|
|
293
|
+
def is_due(self):
|
|
294
|
+
return self.last_run is None or stime.time() - self.last_run > self.freq
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def gcd_pair(a: int, b: int) -> int:
|
|
298
|
+
"""
|
|
299
|
+
>>> gcd_pair(4, 6)
|
|
300
|
+
2
|
|
301
|
+
>>> gcd_pair(6*15, 6*7)
|
|
302
|
+
6
|
|
303
|
+
>>> gcd_pair(6,35)
|
|
304
|
+
1
|
|
305
|
+
"""
|
|
306
|
+
return abs(a) if b == 0 else gcd_pair(b, a % b)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def gcd(*nn: int) -> int:
|
|
310
|
+
"""
|
|
311
|
+
>>> gcd(4)
|
|
312
|
+
4
|
|
313
|
+
>>> gcd(4, 6)
|
|
314
|
+
2
|
|
315
|
+
>>> gcd(6*15, 6*7)
|
|
316
|
+
6
|
|
317
|
+
>>> gcd(6,35)
|
|
318
|
+
1
|
|
319
|
+
>>> gcd(6*15, 6*7, 6*5)
|
|
320
|
+
6
|
|
321
|
+
>>> gcd(6*15, 6*7, 10)
|
|
322
|
+
2
|
|
323
|
+
>>> gcd(6*15, 6*7, 35)
|
|
324
|
+
1
|
|
325
|
+
>>> gcd()
|
|
326
|
+
Traceback (most recent call last):
|
|
327
|
+
...
|
|
328
|
+
IndexError: tuple index out of range
|
|
329
|
+
"""
|
|
330
|
+
r = nn[0]
|
|
331
|
+
for i in range(1, len(nn)):
|
|
332
|
+
r = gcd_pair(r, nn[i])
|
|
333
|
+
return r
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _collect_nothing(n: str, x: Any): # pyright: ignore [reportUnusedParameter]
|
|
337
|
+
pass # pragma: no cover
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
async def run_all(
|
|
341
|
+
*tasks: PeriodicTask,
|
|
342
|
+
shutdown_event: asyncio.Event | None = None,
|
|
343
|
+
collect_results: Callable[[str, Any], None] = _collect_nothing,
|
|
344
|
+
):
|
|
345
|
+
if shutdown_event is None:
|
|
346
|
+
shutdown_event = asyncio.Event()
|
|
347
|
+
if len(tasks) == 0:
|
|
348
|
+
log.warning("No tasks to run")
|
|
349
|
+
return
|
|
350
|
+
tick = gcd(*[t.freq for t in tasks])
|
|
351
|
+
loop = asyncio.get_running_loop()
|
|
352
|
+
|
|
353
|
+
while True:
|
|
354
|
+
start = stime.time()
|
|
355
|
+
for t in tasks:
|
|
356
|
+
if t.is_due():
|
|
357
|
+
t.last_run = start
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
if inspect.iscoroutinefunction(t.logic):
|
|
361
|
+
r = await t.logic()
|
|
362
|
+
else:
|
|
363
|
+
r = await loop.run_in_executor(None, t.logic)
|
|
364
|
+
except (Exception, asyncio.CancelledError) as _:
|
|
365
|
+
r = sys.exc_info()
|
|
366
|
+
collect_results(t.logic.__name__, r)
|
|
367
|
+
if shutdown_event.is_set():
|
|
368
|
+
return
|
|
369
|
+
elapsed = stime.time() - start
|
|
370
|
+
await asyncio.sleep(tick - elapsed if elapsed < tick else 0)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def adjust_as_of_date(as_of_date: date | None) -> date:
|
|
374
|
+
"""
|
|
375
|
+
>>> adjust_as_of_date(None) == date.today()
|
|
376
|
+
True
|
|
377
|
+
>>> adjust_as_of_date(date(2021, 1, 1)) == date(2021, 1, 1)
|
|
378
|
+
True
|
|
379
|
+
"""
|
|
380
|
+
return date.today() if as_of_date is None else as_of_date
|
lythonic/py.typed
ADDED
|
File without changes
|