omlish 0.0.0.dev229__py3-none-any.whl → 0.0.0.dev231__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.
- omlish/__about__.py +2 -2
- omlish/collections/__init__.py +15 -15
- omlish/collections/frozen.py +0 -2
- omlish/collections/identity.py +0 -3
- omlish/collections/indexed.py +0 -2
- omlish/collections/mappings.py +0 -3
- omlish/collections/ordered.py +0 -2
- omlish/collections/persistent/__init__.py +0 -0
- omlish/collections/sorted/__init__.py +0 -0
- omlish/collections/{skiplist.py → sorted/skiplist.py} +0 -1
- omlish/collections/{sorted.py → sorted/sorted.py} +2 -5
- omlish/collections/unmodifiable.py +0 -2
- omlish/daemons/__init__.py +0 -0
- omlish/daemons/daemon.py +200 -0
- omlish/daemons/reparent.py +16 -0
- omlish/daemons/spawning.py +166 -0
- omlish/daemons/targets.py +89 -0
- omlish/daemons/waiting.py +95 -0
- omlish/dispatch/dispatch.py +4 -1
- omlish/dispatch/methods.py +4 -0
- omlish/formats/json/__init__.py +5 -0
- omlish/io/compress/brotli.py +3 -3
- omlish/io/fdio/__init__.py +3 -0
- omlish/lang/__init__.py +0 -2
- omlish/lang/contextmanagers.py +15 -10
- omlish/libc.py +2 -4
- omlish/lite/timeouts.py +1 -1
- omlish/marshal/__init__.py +10 -10
- omlish/marshal/base.py +1 -1
- omlish/marshal/standard.py +2 -2
- omlish/marshal/trivial/__init__.py +0 -0
- omlish/marshal/{forbidden.py → trivial/forbidden.py} +7 -7
- omlish/marshal/{nop.py → trivial/nop.py} +5 -5
- omlish/os/deathpacts/__init__.py +15 -0
- omlish/os/deathpacts/base.py +76 -0
- omlish/os/deathpacts/heartbeatfile.py +85 -0
- omlish/os/{death.py → deathpacts/pipe.py} +20 -90
- omlish/os/forkhooks.py +55 -31
- omlish/os/pidfiles/manager.py +11 -44
- omlish/os/pidfiles/pidfile.py +18 -1
- omlish/reflect/__init__.py +1 -0
- omlish/reflect/inspect.py +43 -0
- omlish/sql/queries/__init__.py +4 -4
- omlish/sql/queries/rendering2.py +248 -0
- omlish/text/parts.py +26 -23
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/RECORD +57 -45
- omlish/formats/json/delimted.py +0 -4
- /omlish/collections/{persistent.py → persistent/persistent.py} +0 -0
- /omlish/collections/{treap.py → persistent/treap.py} +0 -0
- /omlish/collections/{treapmap.py → persistent/treapmap.py} +0 -0
- /omlish/marshal/{utils.py → proxy.py} +0 -0
- /omlish/marshal/{singular → trivial}/any.py +0 -0
- /omlish/sql/queries/{building.py → std.py} +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
import abc
|
2
|
+
import functools
|
3
|
+
import socket
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from .. import dataclasses as dc
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
|
11
|
+
|
12
|
+
class Wait(dc.Case):
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class Waiter(abc.ABC):
|
17
|
+
@abc.abstractmethod
|
18
|
+
def do_wait(self) -> bool:
|
19
|
+
raise NotImplementedError
|
20
|
+
|
21
|
+
|
22
|
+
@functools.singledispatch
|
23
|
+
def waiter_for(wait: Wait) -> Waiter:
|
24
|
+
raise TypeError(wait)
|
25
|
+
|
26
|
+
|
27
|
+
##
|
28
|
+
|
29
|
+
|
30
|
+
class SequentialWait(Wait):
|
31
|
+
waits: ta.Sequence[Wait]
|
32
|
+
|
33
|
+
|
34
|
+
class SequentialWaiter(Waiter):
|
35
|
+
def __init__(self, waiters: ta.Sequence[Waiter]) -> None:
|
36
|
+
super().__init__()
|
37
|
+
|
38
|
+
self._waiters = waiters
|
39
|
+
self._idx = 0
|
40
|
+
|
41
|
+
def do_wait(self) -> bool:
|
42
|
+
while self._idx < len(self._waiters):
|
43
|
+
if not self._waiters[self._idx].do_wait():
|
44
|
+
return False
|
45
|
+
self._idx += 1
|
46
|
+
return True
|
47
|
+
|
48
|
+
|
49
|
+
@waiter_for.register
|
50
|
+
def _(wait: SequentialWait) -> SequentialWaiter:
|
51
|
+
return SequentialWaiter([waiter_for(c) for c in wait.waits])
|
52
|
+
|
53
|
+
|
54
|
+
##
|
55
|
+
|
56
|
+
|
57
|
+
class FnWait(Wait):
|
58
|
+
fn: ta.Callable[[], bool]
|
59
|
+
|
60
|
+
|
61
|
+
class FnWaiter(Waiter, dc.Frozen):
|
62
|
+
wait: FnWait
|
63
|
+
|
64
|
+
def do_wait(self) -> bool:
|
65
|
+
return self.wait.fn()
|
66
|
+
|
67
|
+
|
68
|
+
@waiter_for.register
|
69
|
+
def _(wait: FnWait) -> FnWaiter:
|
70
|
+
return FnWaiter(wait)
|
71
|
+
|
72
|
+
|
73
|
+
##
|
74
|
+
|
75
|
+
|
76
|
+
class ConnectWait(Wait):
|
77
|
+
address: ta.Any
|
78
|
+
|
79
|
+
|
80
|
+
class ConnectWaiter(Waiter, dc.Frozen):
|
81
|
+
wait: ConnectWait
|
82
|
+
|
83
|
+
def do_wait(self) -> bool:
|
84
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
85
|
+
try:
|
86
|
+
s.connect(self.wait.address)
|
87
|
+
except ConnectionRefusedError:
|
88
|
+
return False
|
89
|
+
else:
|
90
|
+
return True
|
91
|
+
|
92
|
+
|
93
|
+
@waiter_for.register
|
94
|
+
def _(wait: ConnectWait) -> ConnectWaiter:
|
95
|
+
return ConnectWaiter(wait)
|
omlish/dispatch/dispatch.py
CHANGED
@@ -31,7 +31,10 @@ def get_impl_func_cls_set(func: ta.Callable) -> frozenset[type]:
|
|
31
31
|
else:
|
32
32
|
return check.isinstance(a, type)
|
33
33
|
|
34
|
-
|
34
|
+
# Exclude 'return' to support difficult to handle return types - they are unimportant.
|
35
|
+
# TODO: only get hints for first arg - requires inspection, which requires chopping off `self`, which can be tricky.
|
36
|
+
_, cls = next(iter(rfl.get_filtered_type_hints(func, exclude=['return']).items()))
|
37
|
+
|
35
38
|
rty = rfl.type_(cls)
|
36
39
|
if isinstance(rty, rfl.Union):
|
37
40
|
ret = frozenset(erase(arg) for arg in rty.args)
|
omlish/dispatch/methods.py
CHANGED
@@ -81,6 +81,10 @@ class Method:
|
|
81
81
|
mro_dct = lang.build_mro_dict(instance_cls, owner_cls)
|
82
82
|
seen: ta.Mapping[ta.Any, str] = {}
|
83
83
|
for nam, att in mro_dct.items():
|
84
|
+
try:
|
85
|
+
hash(att)
|
86
|
+
except TypeError:
|
87
|
+
continue
|
84
88
|
if att in self._impls:
|
85
89
|
try:
|
86
90
|
ex_nam = seen[att]
|
omlish/formats/json/__init__.py
CHANGED
omlish/io/compress/brotli.py
CHANGED
@@ -27,9 +27,9 @@ class BrotliCompression(Compression):
|
|
27
27
|
return brotli.compress(
|
28
28
|
d,
|
29
29
|
**(dict(mode=self.mode) if self.mode is not None else {}),
|
30
|
-
**(dict(
|
31
|
-
**(dict(
|
32
|
-
**(dict(
|
30
|
+
**(dict(quality=self.quality) if self.quality is not None else {}),
|
31
|
+
**(dict(lgwin=self.lgwin) if self.lgwin is not None else {}),
|
32
|
+
**(dict(lgblock=self.lgblock) if self.lgblock is not None else {}),
|
33
33
|
)
|
34
34
|
|
35
35
|
def decompress(self, d: bytes) -> bytes:
|
omlish/io/fdio/__init__.py
CHANGED
omlish/lang/__init__.py
CHANGED
omlish/lang/contextmanagers.py
CHANGED
@@ -18,6 +18,11 @@ T = ta.TypeVar('T')
|
|
18
18
|
##
|
19
19
|
|
20
20
|
|
21
|
+
class _NOT_SET: # noqa
|
22
|
+
def __new__(cls, *args, **kwargs): # noqa
|
23
|
+
raise TypeError
|
24
|
+
|
25
|
+
|
21
26
|
class ContextManaged:
|
22
27
|
def __enter__(self) -> ta.Self:
|
23
28
|
return self
|
@@ -31,21 +36,21 @@ class ContextManaged:
|
|
31
36
|
return None
|
32
37
|
|
33
38
|
|
34
|
-
class
|
35
|
-
def
|
36
|
-
|
37
|
-
|
39
|
+
class NopContextManager(ContextManaged):
|
40
|
+
def __init__(self, /, value: ta.Any = _NOT_SET) -> None:
|
41
|
+
super().__init__()
|
38
42
|
|
39
|
-
|
43
|
+
self._value = value
|
40
44
|
|
45
|
+
def __enter__(self):
|
46
|
+
if (value := self._value) is _NOT_SET:
|
47
|
+
return self
|
48
|
+
else:
|
49
|
+
return value
|
41
50
|
|
42
|
-
class NopContextManager:
|
43
51
|
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
44
52
|
raise TypeError
|
45
53
|
|
46
|
-
def __call__(self, *args, **kwargs):
|
47
|
-
return NOP_CONTEXT_MANAGED
|
48
|
-
|
49
54
|
|
50
55
|
NOP_CONTEXT_MANAGER = NopContextManager()
|
51
56
|
|
@@ -350,7 +355,7 @@ def default_lock(value: DefaultLockable, default: DefaultLockable = None) -> Loc
|
|
350
355
|
return lambda: lock
|
351
356
|
|
352
357
|
elif value is False or value is None:
|
353
|
-
return NOP_CONTEXT_MANAGER
|
358
|
+
return lambda: NOP_CONTEXT_MANAGER
|
354
359
|
|
355
360
|
elif callable(value):
|
356
361
|
return value
|
omlish/libc.py
CHANGED
@@ -62,7 +62,6 @@ libc.free.argtypes = [ct.c_void_p]
|
|
62
62
|
|
63
63
|
|
64
64
|
class Malloc:
|
65
|
-
|
66
65
|
def __init__(self, sz: int) -> None:
|
67
66
|
super().__init__()
|
68
67
|
|
@@ -174,7 +173,6 @@ if LINUX:
|
|
174
173
|
|
175
174
|
|
176
175
|
class Mmap:
|
177
|
-
|
178
176
|
def __init__(
|
179
177
|
self,
|
180
178
|
length: int,
|
@@ -537,14 +535,14 @@ elif DARWIN:
|
|
537
535
|
|
538
536
|
|
539
537
|
if LINUX:
|
540
|
-
def gettid():
|
538
|
+
def gettid() -> int:
|
541
539
|
syscalls = {
|
542
540
|
'i386': 224, # unistd_32.h: #define __NR_gettid 224
|
543
541
|
'x86_64': 186, # unistd_64.h: #define __NR_gettid 186
|
544
542
|
'aarch64': 178, # asm-generic/unistd.h: #define __NR_gettid 178
|
545
543
|
}
|
546
544
|
try:
|
547
|
-
tid =
|
545
|
+
tid = libc.syscall(syscalls[platform.machine()])
|
548
546
|
except Exception: # noqa
|
549
547
|
tid = -1
|
550
548
|
return tid
|
omlish/lite/timeouts.py
CHANGED
@@ -8,7 +8,7 @@ import time
|
|
8
8
|
import typing as ta
|
9
9
|
|
10
10
|
|
11
|
-
TimeoutLike = ta.Union['Timeout', 'Timeout.Default', ta.Iterable['TimeoutLike'], float] # ta.TypeAlias
|
11
|
+
TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.Default'], ta.Iterable['TimeoutLike'], float] # ta.TypeAlias
|
12
12
|
|
13
13
|
|
14
14
|
##
|
omlish/marshal/__init__.py
CHANGED
@@ -34,11 +34,6 @@ from .exceptions import ( # noqa
|
|
34
34
|
UnhandledTypeError,
|
35
35
|
)
|
36
36
|
|
37
|
-
from .forbidden import ( # noqa
|
38
|
-
ForbiddenTypeMarshalerFactory,
|
39
|
-
ForbiddenTypeUnmarshalerFactory,
|
40
|
-
)
|
41
|
-
|
42
37
|
from .global_ import ( # noqa
|
43
38
|
GLOBAL_REGISTRY,
|
44
39
|
|
@@ -54,11 +49,6 @@ from .naming import ( # noqa
|
|
54
49
|
translate_name,
|
55
50
|
)
|
56
51
|
|
57
|
-
from .nop import ( # noqa
|
58
|
-
NOP_MARSHALER_UNMARSHALER,
|
59
|
-
NopMarshalerUnmarshaler,
|
60
|
-
)
|
61
|
-
|
62
52
|
from .objects.helpers import ( # noqa
|
63
53
|
update_field_metadata,
|
64
54
|
update_fields_metadata,
|
@@ -127,6 +117,16 @@ from .standard import ( # noqa
|
|
127
117
|
new_standard_unmarshaler_factory,
|
128
118
|
)
|
129
119
|
|
120
|
+
from .trivial.forbidden import ( # noqa
|
121
|
+
ForbiddenTypeMarshalerFactory,
|
122
|
+
ForbiddenTypeUnmarshalerFactory,
|
123
|
+
)
|
124
|
+
|
125
|
+
from .trivial.nop import ( # noqa
|
126
|
+
NOP_MARSHALER_UNMARSHALER,
|
127
|
+
NopMarshalerUnmarshaler,
|
128
|
+
)
|
129
|
+
|
130
130
|
from .values import ( # noqa
|
131
131
|
Value,
|
132
132
|
)
|
omlish/marshal/base.py
CHANGED
@@ -95,9 +95,9 @@ from .exceptions import UnhandledTypeError
|
|
95
95
|
from .factories import RecursiveTypeFactory
|
96
96
|
from .factories import TypeCacheFactory
|
97
97
|
from .factories import TypeMapFactory
|
98
|
+
from .proxy import _Proxy
|
98
99
|
from .registries import Registry
|
99
100
|
from .registries import RegistryItem
|
100
|
-
from .utils import _Proxy
|
101
101
|
from .values import Value
|
102
102
|
|
103
103
|
|
omlish/marshal/standard.py
CHANGED
@@ -23,8 +23,6 @@ from .objects.namedtuples import NamedtupleMarshalerFactory
|
|
23
23
|
from .objects.namedtuples import NamedtupleUnmarshalerFactory
|
24
24
|
from .polymorphism.unions import PrimitiveUnionMarshalerFactory
|
25
25
|
from .polymorphism.unions import PrimitiveUnionUnmarshalerFactory
|
26
|
-
from .singular.any import ANY_MARSHALER_FACTORY
|
27
|
-
from .singular.any import ANY_UNMARSHALER_FACTORY
|
28
26
|
from .singular.base64 import BASE64_MARSHALER_FACTORY
|
29
27
|
from .singular.base64 import BASE64_UNMARSHALER_FACTORY
|
30
28
|
from .singular.datetimes import DATETIME_MARSHALER_FACTORY
|
@@ -37,6 +35,8 @@ from .singular.primitives import PRIMITIVE_MARSHALER_FACTORY
|
|
37
35
|
from .singular.primitives import PRIMITIVE_UNMARSHALER_FACTORY
|
38
36
|
from .singular.uuids import UUID_MARSHALER_FACTORY
|
39
37
|
from .singular.uuids import UUID_UNMARSHALER_FACTORY
|
38
|
+
from .trivial.any import ANY_MARSHALER_FACTORY
|
39
|
+
from .trivial.any import ANY_UNMARSHALER_FACTORY
|
40
40
|
|
41
41
|
|
42
42
|
##
|
File without changes
|
@@ -1,13 +1,13 @@
|
|
1
1
|
import dataclasses as dc
|
2
2
|
import typing as ta
|
3
3
|
|
4
|
-
from
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
4
|
+
from ... import reflect as rfl
|
5
|
+
from ...funcs import match as mfs
|
6
|
+
from ..base import MarshalContext
|
7
|
+
from ..base import Marshaler
|
8
|
+
from ..base import UnmarshalContext
|
9
|
+
from ..base import Unmarshaler
|
10
|
+
from ..exceptions import ForbiddenTypeError
|
11
11
|
|
12
12
|
|
13
13
|
C = ta.TypeVar('C')
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import typing as ta
|
2
2
|
|
3
|
-
from
|
4
|
-
from
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
3
|
+
from ..base import MarshalContext
|
4
|
+
from ..base import Marshaler
|
5
|
+
from ..base import UnmarshalContext
|
6
|
+
from ..base import Unmarshaler
|
7
|
+
from ..values import Value
|
8
8
|
|
9
9
|
|
10
10
|
class NopMarshalerUnmarshaler(Marshaler, Unmarshaler):
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import abc
|
2
|
+
import os
|
3
|
+
import signal
|
4
|
+
import sys
|
5
|
+
import time
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
|
11
|
+
|
12
|
+
class Deathpact(abc.ABC):
|
13
|
+
@abc.abstractmethod
|
14
|
+
def poll(self) -> None:
|
15
|
+
raise NotImplementedError
|
16
|
+
|
17
|
+
|
18
|
+
class NopDeathpact(Deathpact):
|
19
|
+
def poll(self) -> None:
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
|
25
|
+
|
26
|
+
class BaseDeathpact(Deathpact, abc.ABC):
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
*,
|
30
|
+
interval_s: float = .5,
|
31
|
+
signal: int | None = signal.SIGTERM, # noqa
|
32
|
+
output: ta.Literal['stdout', 'stderr'] | None = 'stderr',
|
33
|
+
on_die: ta.Callable[[], None] | None = None,
|
34
|
+
) -> None:
|
35
|
+
super().__init__()
|
36
|
+
|
37
|
+
self._interval_s = interval_s
|
38
|
+
self._signal = signal
|
39
|
+
self._output = output
|
40
|
+
self._on_die = on_die
|
41
|
+
|
42
|
+
self._last_check_t: float | None = None
|
43
|
+
|
44
|
+
def _print(self, msg: str) -> None:
|
45
|
+
match self._output:
|
46
|
+
case 'stdout':
|
47
|
+
f = sys.stdout
|
48
|
+
case 'stderr':
|
49
|
+
f = sys.stderr
|
50
|
+
case _:
|
51
|
+
return
|
52
|
+
print(f'{self} pid={os.getpid()}: {msg}', file=f)
|
53
|
+
|
54
|
+
def die(self) -> None:
|
55
|
+
self._print('Triggered! Process terminating!')
|
56
|
+
|
57
|
+
if self._on_die is not None:
|
58
|
+
self._on_die()
|
59
|
+
|
60
|
+
if self._signal is not None:
|
61
|
+
os.kill(os.getpid(), self._signal)
|
62
|
+
|
63
|
+
sys.exit(1)
|
64
|
+
|
65
|
+
@abc.abstractmethod
|
66
|
+
def should_die(self) -> bool:
|
67
|
+
raise NotImplementedError
|
68
|
+
|
69
|
+
def maybe_die(self) -> None:
|
70
|
+
if self.should_die():
|
71
|
+
self.die()
|
72
|
+
|
73
|
+
def poll(self) -> None:
|
74
|
+
if self._last_check_t is None or (time.monotonic() - self._last_check_t) >= self._interval_s:
|
75
|
+
self.maybe_die()
|
76
|
+
self._last_check_t = time.monotonic()
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- chaining
|
4
|
+
"""
|
5
|
+
import os
|
6
|
+
import time
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from ... import check
|
10
|
+
from ..forkhooks import ProcessOriginTracker
|
11
|
+
from ..temp import make_temp_file
|
12
|
+
from .base import BaseDeathpact
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
class HeartbeatFileDeathpact(BaseDeathpact):
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
path: str,
|
22
|
+
ttl_s: float = 10.,
|
23
|
+
**kwargs: ta.Any,
|
24
|
+
) -> None:
|
25
|
+
super().__init__(**kwargs)
|
26
|
+
|
27
|
+
self._path = path
|
28
|
+
self._ttl_s = ttl_s
|
29
|
+
|
30
|
+
self._process_origin = ProcessOriginTracker()
|
31
|
+
|
32
|
+
@property
|
33
|
+
def path(self) -> str:
|
34
|
+
return self._path
|
35
|
+
|
36
|
+
def is_parent(self) -> bool:
|
37
|
+
return self._process_origin.is_in_origin_process()
|
38
|
+
|
39
|
+
#
|
40
|
+
|
41
|
+
def __enter__(self) -> ta.Self:
|
42
|
+
check.state(self.is_parent())
|
43
|
+
|
44
|
+
self.update()
|
45
|
+
|
46
|
+
return self
|
47
|
+
|
48
|
+
def close(self) -> None:
|
49
|
+
if self.is_parent():
|
50
|
+
try:
|
51
|
+
os.unlink(self._path)
|
52
|
+
except FileNotFoundError:
|
53
|
+
pass
|
54
|
+
|
55
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
56
|
+
self.close()
|
57
|
+
|
58
|
+
#
|
59
|
+
|
60
|
+
@classmethod
|
61
|
+
def _now(cls) -> float:
|
62
|
+
return time.monotonic()
|
63
|
+
|
64
|
+
def update(self) -> None:
|
65
|
+
check.state(self.is_parent())
|
66
|
+
|
67
|
+
new = make_temp_file()
|
68
|
+
with open(new, 'w') as f:
|
69
|
+
f.write(str(self._now()))
|
70
|
+
|
71
|
+
# FIXME: same filesystem
|
72
|
+
os.replace(new, self._path)
|
73
|
+
|
74
|
+
def read(self) -> float:
|
75
|
+
try:
|
76
|
+
with open(self._path) as f:
|
77
|
+
return float(f.read())
|
78
|
+
except FileNotFoundError:
|
79
|
+
return float('-inf')
|
80
|
+
|
81
|
+
def age(self) -> float:
|
82
|
+
return self._now() - self.read()
|
83
|
+
|
84
|
+
def should_die(self) -> bool:
|
85
|
+
return self.age() >= self._ttl_s
|