omdev 0.0.0.dev29__py3-none-any.whl → 0.0.0.dev30__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.
Potentially problematic release.
This version of omdev might be problematic. Click here for more details.
- omdev/cache/compute/cache.py +120 -0
- omdev/cache/compute/contexts.py +137 -0
- omdev/cache/compute/currents.py +78 -0
- omdev/cache/compute/fns.py +157 -0
- omdev/cache/compute/resolvers.py +23 -0
- omdev/cache/compute/storage.py +39 -0
- omdev/cache/compute/types.py +144 -0
- omdev/cache/data/__init__.py +9 -5
- omdev/cache/data/actions.py +8 -2
- omdev/cache/data/cache.py +66 -31
- omdev/cache/data/manifests.py +3 -3
- omdev/cache/data/specs.py +9 -6
- omdev/manifests.py +1 -1
- omdev/pyproject/cli.py +5 -1
- omdev/scripts/interp.py +20 -5
- omdev/scripts/pyproject.py +25 -6
- omdev/tools/piptools.py +24 -0
- {omdev-0.0.0.dev29.dist-info → omdev-0.0.0.dev30.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev29.dist-info → omdev-0.0.0.dev30.dist-info}/RECORD +23 -21
- omdev/cache/comp/cache.py +0 -137
- omdev/cache/comp/contexts.py +0 -136
- omdev/cache/comp/fns.py +0 -115
- omdev/cache/comp/resolvers.py +0 -23
- omdev/cache/comp/types.py +0 -92
- /omdev/cache/{comp → compute}/__init__.py +0 -0
- {omdev-0.0.0.dev29.dist-info → omdev-0.0.0.dev30.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev29.dist-info → omdev-0.0.0.dev30.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev29.dist-info → omdev-0.0.0.dev30.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- filesystem OPTIONAL
|
|
4
|
+
- also postgres + (s3?) blobstore
|
|
5
|
+
- locking
|
|
6
|
+
|
|
7
|
+
==
|
|
8
|
+
|
|
9
|
+
TODO (old):
|
|
10
|
+
- are pickles stable?
|
|
11
|
+
- ttl
|
|
12
|
+
- np mmap
|
|
13
|
+
- https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html freeze ndarray w writable=False
|
|
14
|
+
- compress?
|
|
15
|
+
- overlap w/ jobs/dags/batches/whatever
|
|
16
|
+
- joblib
|
|
17
|
+
- ** INPUTS **
|
|
18
|
+
- if underlying impl changes, bust
|
|
19
|
+
- kinda reacty/reffy/signally
|
|
20
|
+
- decorator unwrapping and shit
|
|
21
|
+
- proactive deep invalidate
|
|
22
|
+
----
|
|
23
|
+
- version can be anything - hashes, etc
|
|
24
|
+
- version generators - one for ast
|
|
25
|
+
- configurable serde - marshal vs pickle? marshal w/ override for ndarray to write to file?
|
|
26
|
+
- ok:
|
|
27
|
+
- @fn - version, passive=False, deps=[Objectable, …]
|
|
28
|
+
- it no version use ast - specifically {'ast': <md5>}
|
|
29
|
+
- but if present just use literal they gave, probably int
|
|
30
|
+
- idiom: Version can be a frozendict, conventionally of str -> ta.Hashable
|
|
31
|
+
- auto deps - fn can get containing Packages
|
|
32
|
+
- Module, Resource, …
|
|
33
|
+
- hrm.. LiteralVersion, MapVersion? + custom Marshal? need to deser as frozendict
|
|
34
|
+
- storage
|
|
35
|
+
- object table? w/ versions? strictly one row per object, evict objects with diff versions than those encountered
|
|
36
|
+
- nah Cache iface, SimpleCache, SqlCache
|
|
37
|
+
- dir structure: __package__/__qualname__/... ?
|
|
38
|
+
- next: Versions get squashed into VersionHash, store whole version in db but only pass and cmp md5
|
|
39
|
+
- thus VersionHashMap
|
|
40
|
+
|
|
41
|
+
manifest stuff
|
|
42
|
+
- serialization_version
|
|
43
|
+
- lib_version
|
|
44
|
+
- lib_revision
|
|
45
|
+
|
|
46
|
+
See:
|
|
47
|
+
- https://jax.readthedocs.io/en/latest/autodidax.html
|
|
48
|
+
- https://github.com/tinygrad/tinygrad/blob/78699d9924feb96dc0bac88c3646b5d4f9ecad23/tinygrad/engine/jit.py
|
|
49
|
+
- https://github.com/SeaOfNodes/Simple/tree/c7445ad142aeaece5b2b1059c193735ba7e509d9 (gvn)
|
|
50
|
+
- https://github.com/joblib/joblib/tree/bca1f4216a38cff82a85371c45dde79bed977d0e/joblib
|
|
51
|
+
- https://docs.python.org/3/library/pickle.html#pickle.Pickler.dispatch_table
|
|
52
|
+
|
|
53
|
+
Don't see:
|
|
54
|
+
- https://github.com/amakelov/mandala
|
|
55
|
+
"""
|
|
56
|
+
import copy
|
|
57
|
+
import typing as ta
|
|
58
|
+
|
|
59
|
+
from omlish import collections as col
|
|
60
|
+
|
|
61
|
+
from .storage import Storage
|
|
62
|
+
from .types import CacheEntry
|
|
63
|
+
from .types import CacheKey
|
|
64
|
+
from .types import CacheResult
|
|
65
|
+
from .types import CacheStats
|
|
66
|
+
from .types import Name
|
|
67
|
+
from .types import ObjectResolver
|
|
68
|
+
from .types import VersionMap
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Cache:
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
resolver: ObjectResolver,
|
|
75
|
+
storage: Storage,
|
|
76
|
+
) -> None:
|
|
77
|
+
super().__init__()
|
|
78
|
+
|
|
79
|
+
self._resolver = resolver
|
|
80
|
+
self._storage = storage
|
|
81
|
+
|
|
82
|
+
self._stats = CacheStats()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def stats(self) -> CacheStats:
|
|
86
|
+
return copy.deepcopy(self._stats)
|
|
87
|
+
|
|
88
|
+
def _build_version_map(self, names: ta.Iterable[Name]) -> VersionMap:
|
|
89
|
+
dct = {}
|
|
90
|
+
for n in names:
|
|
91
|
+
c = self._resolver.resolve(n)
|
|
92
|
+
dct[n] = c.version
|
|
93
|
+
return col.frozendict(dct)
|
|
94
|
+
|
|
95
|
+
def get(self, key: CacheKey) -> CacheResult | None:
|
|
96
|
+
entry = self._storage.get(key)
|
|
97
|
+
if entry is None:
|
|
98
|
+
self._stats.num_misses += 1
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
new_versions = self._build_version_map(entry.versions)
|
|
102
|
+
if entry.versions != new_versions:
|
|
103
|
+
self._storage.delete(key)
|
|
104
|
+
self._stats.num_invalidates += 1
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
self._stats.num_hits += 1
|
|
108
|
+
return CacheResult(
|
|
109
|
+
True,
|
|
110
|
+
entry.versions,
|
|
111
|
+
entry.value,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def put(self, key: CacheKey, versions: VersionMap, val: ta.Any) -> None:
|
|
115
|
+
self._storage.put(CacheEntry(
|
|
116
|
+
key,
|
|
117
|
+
versions,
|
|
118
|
+
val,
|
|
119
|
+
))
|
|
120
|
+
self._stats.num_puts += 1
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- weak children if not in debug?
|
|
4
|
+
"""
|
|
5
|
+
import abc
|
|
6
|
+
import typing as ta
|
|
7
|
+
|
|
8
|
+
from omlish import check
|
|
9
|
+
from omlish import collections as col
|
|
10
|
+
from omlish import lang
|
|
11
|
+
|
|
12
|
+
from .types import CacheKey
|
|
13
|
+
from .types import CacheResult
|
|
14
|
+
from .types import Object
|
|
15
|
+
from .types import VersionMap
|
|
16
|
+
from .types import merge_version_maps
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Context(lang.Abstract, lang.Sealed):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
obj: Object,
|
|
26
|
+
*,
|
|
27
|
+
dependencies: VersionMap = col.frozendict(),
|
|
28
|
+
parent: ta.Optional['Context'] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
self._obj = obj
|
|
33
|
+
self._dependencies = dependencies
|
|
34
|
+
self._parent = parent
|
|
35
|
+
|
|
36
|
+
check.equal(set(dependencies), set(obj.dependencies))
|
|
37
|
+
|
|
38
|
+
self._result: CacheResult | None = None
|
|
39
|
+
self._children: list[Context] = []
|
|
40
|
+
|
|
41
|
+
if parent is not None:
|
|
42
|
+
check.state(not parent.done)
|
|
43
|
+
parent._children.append(self) # noqa
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def object(self) -> Object:
|
|
49
|
+
return self._obj
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def dependencies(self) -> VersionMap:
|
|
53
|
+
return self._dependencies
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def parent(self) -> ta.Optional['Context']:
|
|
57
|
+
return self._parent
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def children(self) -> ta.Sequence['Context']:
|
|
61
|
+
return self._children
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
@abc.abstractmethod
|
|
65
|
+
def done(self) -> bool:
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
@lang.cached_function
|
|
69
|
+
@ta.final
|
|
70
|
+
def versions(self) -> VersionMap:
|
|
71
|
+
check.state(self.done)
|
|
72
|
+
return merge_version_maps(
|
|
73
|
+
self._obj.as_version_map,
|
|
74
|
+
self._dependencies,
|
|
75
|
+
self._impl_versions(),
|
|
76
|
+
*[c.versions() for c in self._children],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@abc.abstractmethod
|
|
80
|
+
def _impl_versions(self) -> VersionMap:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ActiveContext(Context, lang.Final):
|
|
85
|
+
def __init__(self, obj: Object, key: CacheKey, **kwargs: ta.Any) -> None:
|
|
86
|
+
check.arg(not obj.passive)
|
|
87
|
+
|
|
88
|
+
super().__init__(obj, **kwargs)
|
|
89
|
+
|
|
90
|
+
self._key = key
|
|
91
|
+
|
|
92
|
+
self._result: CacheResult | None = None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def key(self) -> CacheKey:
|
|
96
|
+
return self._key
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def done(self) -> bool:
|
|
100
|
+
return self._result is not None
|
|
101
|
+
|
|
102
|
+
def set_hit(self, result: CacheResult) -> None:
|
|
103
|
+
check.state(result.hit)
|
|
104
|
+
self._result = check.replacing_none(self._result, result)
|
|
105
|
+
self.versions()
|
|
106
|
+
|
|
107
|
+
def set_miss(self, val: ta.Any) -> None:
|
|
108
|
+
self._result = check.replacing_none(self._result, CacheResult(
|
|
109
|
+
False,
|
|
110
|
+
VersionMap(),
|
|
111
|
+
val,
|
|
112
|
+
))
|
|
113
|
+
self.versions()
|
|
114
|
+
|
|
115
|
+
def _impl_versions(self) -> VersionMap:
|
|
116
|
+
return check.not_none(self._result).versions
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PassiveContext(Context, lang.Final):
|
|
120
|
+
def __init__(self, obj: Object, **kwargs: ta.Any) -> None:
|
|
121
|
+
check.arg(obj.passive)
|
|
122
|
+
|
|
123
|
+
super().__init__(obj, **kwargs)
|
|
124
|
+
|
|
125
|
+
self._done = False
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def done(self) -> bool:
|
|
129
|
+
return self._done
|
|
130
|
+
|
|
131
|
+
def finish(self) -> None:
|
|
132
|
+
check.state(not self._done)
|
|
133
|
+
self._done = True
|
|
134
|
+
|
|
135
|
+
@lang.cached_function
|
|
136
|
+
def _impl_versions(self) -> VersionMap:
|
|
137
|
+
return col.frozendict()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- threadlocal / contextvar / whatever
|
|
4
|
+
"""
|
|
5
|
+
import contextlib
|
|
6
|
+
import typing as ta
|
|
7
|
+
|
|
8
|
+
from omlish import check
|
|
9
|
+
|
|
10
|
+
from .cache import Cache
|
|
11
|
+
from .contexts import ActiveContext
|
|
12
|
+
from .contexts import Context
|
|
13
|
+
from .contexts import PassiveContext
|
|
14
|
+
from .types import CacheKey
|
|
15
|
+
from .types import Object
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CacheT = ta.TypeVar('CacheT', bound='Cache')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_CURRENT_CACHE: Cache | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextlib.contextmanager
|
|
28
|
+
def setting_current_cache(cache: CacheT) -> ta.Iterator[CacheT]:
|
|
29
|
+
global _CURRENT_CACHE
|
|
30
|
+
prev = _CURRENT_CACHE
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
_CURRENT_CACHE = cache
|
|
34
|
+
yield cache
|
|
35
|
+
|
|
36
|
+
finally:
|
|
37
|
+
check.is_(_CURRENT_CACHE, cache)
|
|
38
|
+
_CURRENT_CACHE = prev
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_current_cache() -> Cache | None:
|
|
42
|
+
return _CURRENT_CACHE
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_CURRENT_CONTEXT: Context | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@contextlib.contextmanager
|
|
52
|
+
def setting_current_context(
|
|
53
|
+
obj: Object,
|
|
54
|
+
key: CacheKey | None = None,
|
|
55
|
+
**kwargs: ta.Any,
|
|
56
|
+
) -> ta.Iterator[Context]:
|
|
57
|
+
global _CURRENT_CONTEXT
|
|
58
|
+
prev = _CURRENT_CONTEXT
|
|
59
|
+
|
|
60
|
+
ctx_kw = dict(
|
|
61
|
+
parent=prev,
|
|
62
|
+
**kwargs,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
ctx: Context
|
|
66
|
+
if obj.passive:
|
|
67
|
+
check.none(key)
|
|
68
|
+
ctx = PassiveContext(obj, **ctx_kw)
|
|
69
|
+
else:
|
|
70
|
+
ctx = ActiveContext(obj, check.isinstance(key, CacheKey), **ctx_kw)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
_CURRENT_CONTEXT = ctx
|
|
74
|
+
yield ctx
|
|
75
|
+
|
|
76
|
+
finally:
|
|
77
|
+
check.is_(_CURRENT_CONTEXT, ctx)
|
|
78
|
+
_CURRENT_CONTEXT = prev
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- user specified key construction (skip args, default, transform, etc)
|
|
4
|
+
- option to not cache if too fast
|
|
5
|
+
- auto parent package chain per-module/package-ish CACHE_VERSION convention
|
|
6
|
+
- meditate on decos, descriptors, unwrapping, etc
|
|
7
|
+
- auto metadata:
|
|
8
|
+
- source
|
|
9
|
+
- qualname
|
|
10
|
+
- location
|
|
11
|
+
- ast? ast hash?
|
|
12
|
+
- keep src anyway, but just for warn
|
|
13
|
+
- strip comments?
|
|
14
|
+
- global tracking?
|
|
15
|
+
- *** or, at least, global constants? *** explicit Const obj, on access attach? hm...
|
|
16
|
+
"""
|
|
17
|
+
import functools
|
|
18
|
+
import importlib
|
|
19
|
+
import typing as ta
|
|
20
|
+
|
|
21
|
+
from omlish import check
|
|
22
|
+
from omlish import collections as col
|
|
23
|
+
from omlish import dataclasses as dc
|
|
24
|
+
from omlish import lang
|
|
25
|
+
|
|
26
|
+
from .contexts import ActiveContext
|
|
27
|
+
from .contexts import PassiveContext
|
|
28
|
+
from .currents import get_current_cache
|
|
29
|
+
from .currents import setting_current_context
|
|
30
|
+
from .types import CacheKey
|
|
31
|
+
from .types import Metadata
|
|
32
|
+
from .types import Name
|
|
33
|
+
from .types import Object
|
|
34
|
+
from .types import ObjectResolver
|
|
35
|
+
from .types import Version
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
P = ta.ParamSpec('P')
|
|
39
|
+
T = ta.TypeVar('T')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dc.dataclass(frozen=True)
|
|
46
|
+
class FnName(Name, lang.Final):
|
|
47
|
+
module: str
|
|
48
|
+
qualname: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dc.dataclass(frozen=True)
|
|
52
|
+
class FnObject(Object, lang.Final, ta.Generic[P, T]):
|
|
53
|
+
name: Name = dc.xfield(override=True)
|
|
54
|
+
fn: ta.Callable[P, T] # type: ignore
|
|
55
|
+
version: Version = dc.xfield(override=True)
|
|
56
|
+
|
|
57
|
+
_: dc.KW_ONLY
|
|
58
|
+
|
|
59
|
+
dependencies: ta.AbstractSet[Name] = dc.xfield(default=frozenset(), override=True)
|
|
60
|
+
passive: bool = dc.xfield(default=False, override=True)
|
|
61
|
+
metadata: Metadata = dc.xfield(default=col.frozendict(), override=True)
|
|
62
|
+
|
|
63
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
|
64
|
+
return self.fn(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FnObjectResolver(ObjectResolver):
|
|
68
|
+
def resolve(self, name: Name) -> FnObject:
|
|
69
|
+
fname = check.isinstance(name, FnName)
|
|
70
|
+
|
|
71
|
+
mod = importlib.import_module(fname.module)
|
|
72
|
+
obj = mod
|
|
73
|
+
for a in fname.qualname.split('.'):
|
|
74
|
+
obj = getattr(obj, a)
|
|
75
|
+
|
|
76
|
+
check.callable(obj)
|
|
77
|
+
fc = check.isinstance(obj.__cacheable__, FnObject)
|
|
78
|
+
|
|
79
|
+
return fc
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dc.dataclass(frozen=True)
|
|
83
|
+
class FnCacheKey(CacheKey[FnName], lang.Final):
|
|
84
|
+
args: tuple
|
|
85
|
+
kwargs: col.frozendict[str, ta.Any]
|
|
86
|
+
|
|
87
|
+
@dc.validate
|
|
88
|
+
def _check_fn_types(self) -> bool:
|
|
89
|
+
return (
|
|
90
|
+
isinstance(self.name, FnName) and
|
|
91
|
+
isinstance(self.args, tuple) and
|
|
92
|
+
isinstance(self.kwargs, col.frozendict)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def fn(
|
|
100
|
+
version: Version,
|
|
101
|
+
*,
|
|
102
|
+
passive: bool = False,
|
|
103
|
+
metadata: Metadata = col.frozendict(),
|
|
104
|
+
) -> ta.Callable[[T], T]:
|
|
105
|
+
def outer(fn):
|
|
106
|
+
@functools.wraps(fn)
|
|
107
|
+
def inner(*args, **kwargs):
|
|
108
|
+
# NOTE: just for testing :x allows updating
|
|
109
|
+
# TODO: proper wrapper obj probably (enforce name resolution)
|
|
110
|
+
obj = inner.__cacheable__ # type: ignore
|
|
111
|
+
|
|
112
|
+
if (cache := get_current_cache()) is None:
|
|
113
|
+
return fn(*args, **kwargs)
|
|
114
|
+
|
|
115
|
+
if obj.passive:
|
|
116
|
+
with setting_current_context(obj) as ctx:
|
|
117
|
+
pctx = check.isinstance(ctx, PassiveContext)
|
|
118
|
+
val = fn(*args, **kwargs)
|
|
119
|
+
pctx.finish()
|
|
120
|
+
return val
|
|
121
|
+
|
|
122
|
+
key = FnCacheKey(
|
|
123
|
+
obj.name,
|
|
124
|
+
args,
|
|
125
|
+
col.frozendict(kwargs),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
with setting_current_context(obj, key) as ctx:
|
|
129
|
+
actx = check.isinstance(ctx, ActiveContext)
|
|
130
|
+
|
|
131
|
+
if (hit := cache.get(key)) is not None:
|
|
132
|
+
actx.set_hit(hit)
|
|
133
|
+
return hit.value
|
|
134
|
+
|
|
135
|
+
val = fn(*args, **kwargs)
|
|
136
|
+
actx.set_miss(val)
|
|
137
|
+
cache.put(
|
|
138
|
+
key,
|
|
139
|
+
actx.versions(),
|
|
140
|
+
val,
|
|
141
|
+
)
|
|
142
|
+
return val
|
|
143
|
+
|
|
144
|
+
inner.__cacheable__ = FnObject( # type: ignore
|
|
145
|
+
FnName(
|
|
146
|
+
fn.__module__,
|
|
147
|
+
fn.__qualname__,
|
|
148
|
+
),
|
|
149
|
+
fn,
|
|
150
|
+
version,
|
|
151
|
+
passive=passive,
|
|
152
|
+
metadata=metadata,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return inner
|
|
156
|
+
|
|
157
|
+
return outer # noqa
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .types import Name
|
|
2
|
+
from .types import Object
|
|
3
|
+
from .types import ObjectResolver
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CachingObjectResolver(ObjectResolver):
|
|
7
|
+
def __init__(self, child: ObjectResolver) -> None:
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
self._child = child
|
|
11
|
+
self._dct: dict[Name, Object] = {}
|
|
12
|
+
|
|
13
|
+
def clear(self) -> None:
|
|
14
|
+
self._dct.clear()
|
|
15
|
+
|
|
16
|
+
def resolve(self, name: Name) -> Object:
|
|
17
|
+
try:
|
|
18
|
+
return self._dct[name]
|
|
19
|
+
except KeyError:
|
|
20
|
+
pass
|
|
21
|
+
ret = self._child.resolve(name)
|
|
22
|
+
self._dct[name] = ret
|
|
23
|
+
return ret
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from .types import CacheEntry
|
|
4
|
+
from .types import CacheKey
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Storage(abc.ABC):
|
|
8
|
+
@abc.abstractmethod
|
|
9
|
+
def get(self, key: CacheKey) -> CacheEntry | None:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
@abc.abstractmethod
|
|
13
|
+
def put(self, entry: CacheEntry) -> None:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
@abc.abstractmethod
|
|
17
|
+
def delete(self, key: CacheKey) -> None:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DictStorage(Storage):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
self._dct: dict[CacheKey, CacheEntry] = {}
|
|
26
|
+
|
|
27
|
+
def get(self, key: CacheKey) -> CacheEntry | None:
|
|
28
|
+
try:
|
|
29
|
+
return self._dct[key]
|
|
30
|
+
except KeyError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def put(self, entry: CacheEntry) -> None:
|
|
34
|
+
if entry.key in self._dct:
|
|
35
|
+
raise KeyError(entry.key)
|
|
36
|
+
self._dct[entry.key] = entry
|
|
37
|
+
|
|
38
|
+
def delete(self, key: CacheKey) -> None:
|
|
39
|
+
del self._dct[key]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omlish import cached
|
|
5
|
+
from omlish import collections as col
|
|
6
|
+
from omlish import dataclasses as dc
|
|
7
|
+
from omlish import lang
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = ta.TypeVar('T')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
NameT = ta.TypeVar('NameT', bound='Name')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Name(lang.Abstract):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Version: ta.TypeAlias = ta.Hashable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def version(**kwargs: Version) -> Version:
|
|
30
|
+
return col.frozendict(**kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
VersionMap: ta.TypeAlias = col.frozendict[Name, Version]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def merge_version_maps(
|
|
37
|
+
*dcts: ta.Mapping[Name, Version],
|
|
38
|
+
) -> VersionMap:
|
|
39
|
+
out: dict[Name, Version] = {}
|
|
40
|
+
for dct in dcts:
|
|
41
|
+
for name, version in dct.items():
|
|
42
|
+
try:
|
|
43
|
+
ex = out[name]
|
|
44
|
+
except KeyError:
|
|
45
|
+
out[name] = version
|
|
46
|
+
else:
|
|
47
|
+
if ex != version:
|
|
48
|
+
raise Exception(f'Version mismatch: {ex} {version}')
|
|
49
|
+
return col.frozendict(out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Metadata: ta.TypeAlias = ta.Mapping[str, ta.Any] # *not* hashed - advisory
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Object(lang.Abstract):
|
|
59
|
+
@property
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
def name(self) -> Name:
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
@abc.abstractmethod
|
|
66
|
+
def version(self) -> Version:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
@abc.abstractmethod
|
|
71
|
+
def dependencies(self) -> ta.AbstractSet[Name]:
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
def passive(self) -> bool:
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
@abc.abstractmethod
|
|
81
|
+
def metadata(self) -> Metadata:
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
|
|
84
|
+
@cached.property
|
|
85
|
+
def as_version_map(self) -> VersionMap:
|
|
86
|
+
return col.frozendict({self.name: self.version})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dc.dataclass(frozen=True)
|
|
93
|
+
@dc.extra_params(cache_hash=True)
|
|
94
|
+
class CacheKey(lang.Abstract, ta.Generic[NameT]):
|
|
95
|
+
name: NameT
|
|
96
|
+
|
|
97
|
+
@dc.validate
|
|
98
|
+
def _check_types(self) -> bool:
|
|
99
|
+
hash(self)
|
|
100
|
+
return isinstance(self.name, Name)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dc.dataclass(frozen=True)
|
|
104
|
+
class CacheResult(ta.Generic[T], lang.Final):
|
|
105
|
+
hit: bool
|
|
106
|
+
versions: VersionMap
|
|
107
|
+
value: T
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ObjectResolver(lang.Abstract):
|
|
114
|
+
@abc.abstractmethod
|
|
115
|
+
def resolve(self, name: Name) -> Object:
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dc.dataclass()
|
|
123
|
+
class CacheStats:
|
|
124
|
+
num_hits: int = 0
|
|
125
|
+
num_misses: int = 0
|
|
126
|
+
num_invalidates: int = 0
|
|
127
|
+
num_puts: int = 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
##
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dc.dataclass(frozen=True)
|
|
134
|
+
class CacheEntry:
|
|
135
|
+
key: CacheKey
|
|
136
|
+
versions: VersionMap
|
|
137
|
+
value: ta.Any
|
|
138
|
+
|
|
139
|
+
@dc.validate
|
|
140
|
+
def _check_types(self) -> bool:
|
|
141
|
+
return (
|
|
142
|
+
isinstance(self.key, CacheKey) and
|
|
143
|
+
isinstance(self.versions, col.frozendict)
|
|
144
|
+
)
|