omdev 0.0.0.dev28__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.

@@ -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
+ )