omlish 0.0.0.dev236__py3-none-any.whl → 0.0.0.dev238__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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev236'
2
- __revision__ = '492e9f7254561d4c5576a6df3dfb94fdf94d9f7a'
1
+ __version__ = '0.0.0.dev238'
2
+ __revision__ = '0ab60b1d75e5b0b298707ba55797958e528c69b5'
3
3
 
4
4
 
5
5
  #
@@ -57,11 +57,6 @@ from .identity import ( # noqa
57
57
  IdentityWeakSet,
58
58
  )
59
59
 
60
- from .indexed import ( # noqa
61
- IndexedSeq,
62
- IndexedSetSeq,
63
- )
64
-
65
60
  from .mappings import ( # noqa
66
61
  MissingDict,
67
62
  TypeMap,
@@ -91,6 +86,11 @@ else:
91
86
  'new_treap_map',
92
87
  ])
93
88
 
89
+ from .ranked import ( # noqa
90
+ RankedSeq,
91
+ RankedSetSeq,
92
+ )
93
+
94
94
  if _ta.TYPE_CHECKING:
95
95
  from .sorted.skiplist import ( # noqa
96
96
  SkipList,
@@ -0,0 +1,79 @@
1
+ import typing as ta
2
+
3
+ from .identity import IdentityKeyDict
4
+ from .identity import IdentitySet
5
+
6
+
7
+ T = ta.TypeVar('T')
8
+
9
+
10
+ ##
11
+
12
+
13
+ class RankedSeq(ta.Sequence[T]):
14
+ def __init__(self, it: ta.Iterable[T], *, identity: bool = False) -> None:
15
+ super().__init__()
16
+
17
+ self._lst = list(it)
18
+ self._ranks: ta.Mapping[T, int] = (IdentityKeyDict if identity else dict)((e, i) for i, e in enumerate(self._lst)) # noqa
19
+ if len(self._ranks) != len(self._lst):
20
+ raise ValueError(f'{len(self._ranks)} != {len(self._lst)}')
21
+
22
+ @property
23
+ def debug(self) -> ta.Sequence[T]:
24
+ return self._lst
25
+
26
+ def __iter__(self) -> ta.Iterator[T]:
27
+ return iter(self._lst)
28
+
29
+ def __getitem__(self, rank: int) -> T: # type: ignore
30
+ return self._lst[rank]
31
+
32
+ def __len__(self) -> int:
33
+ return len(self._lst)
34
+
35
+ def __contains__(self, obj: T) -> bool: # type: ignore
36
+ return obj in self._ranks
37
+
38
+ @property
39
+ def ranks(self) -> ta.Mapping[T, int]:
40
+ return self._ranks
41
+
42
+ def rank(self, obj: T) -> int:
43
+ return self._ranks[obj]
44
+
45
+
46
+ ##
47
+
48
+
49
+ class RankedSetSeq(ta.Sequence[ta.AbstractSet[T]]):
50
+ def __init__(self, it: ta.Iterable[ta.Iterable[T]], *, identity: bool = False) -> None:
51
+ super().__init__()
52
+
53
+ self._lst = [(IdentitySet if identity else set)(e) for e in it]
54
+ self._ranks: ta.Mapping[T, int] = (IdentityKeyDict if identity else dict)((e, i) for i, es in enumerate(self._lst) for e in es) # noqa
55
+ if len(self._ranks) != sum(map(len, self._lst)):
56
+ raise ValueError(f'{len(self._ranks)} != {sum(map(len, self._lst))}')
57
+
58
+ @property
59
+ def debug(self) -> ta.Sequence[ta.AbstractSet[T]]:
60
+ return self._lst
61
+
62
+ def __iter__(self) -> ta.Iterator[ta.AbstractSet[T]]:
63
+ return iter(self._lst)
64
+
65
+ def __getitem__(self, rank: int) -> ta.AbstractSet[T]: # type: ignore
66
+ return self._lst[rank]
67
+
68
+ def __len__(self) -> int:
69
+ return len(self._lst)
70
+
71
+ def __contains__(self, obj: T) -> bool: # type: ignore
72
+ return obj in self._ranks
73
+
74
+ @property
75
+ def ranks(self) -> ta.Mapping[T, int]:
76
+ return self._ranks
77
+
78
+ def rank(self, obj: T) -> int:
79
+ return self._ranks[obj]
omlish/configs/classes.py CHANGED
@@ -1,53 +1,35 @@
1
1
  import typing as ta
2
- import weakref
3
2
 
4
3
  from .. import check
5
4
  from .. import dataclasses as dc
6
5
  from .. import lang
7
6
 
8
7
 
9
- class Config(
10
- dc.Data,
11
- lang.Abstract,
12
- frozen=True,
13
- reorder=True,
14
- confer=frozenset([
15
- 'frozen',
16
- 'reorder',
17
- 'confer',
18
- ]),
19
- ):
20
- pass
8
+ ConfigurableConfigT = ta.TypeVar('ConfigurableConfigT', bound='Configurable.Config')
21
9
 
22
10
 
23
- ConfigT = ta.TypeVar('ConfigT', bound='Config')
11
+ class Configurable(ta.Generic[ConfigurableConfigT], lang.Abstract):
12
+ @dc.dataclass(frozen=True, kw_only=True)
13
+ class Config:
14
+ """Does not use any dc metaclasses to preserve typechecking."""
24
15
 
25
- _CONFIG_CLS_MAP: ta.MutableMapping[type[Config], type['Configurable']] = weakref.WeakValueDictionary()
26
-
27
-
28
- class Configurable(ta.Generic[ConfigT], lang.Abstract):
29
-
30
- # FIXME: https://github.com/python/mypy/issues/5144
31
- Config: ta.ClassVar[type[ConfigT]] # type: ignore # noqa
16
+ configurable_cls: ta.ClassVar[type['Configurable']]
32
17
 
33
18
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
34
19
  super().__init_subclass__(**kwargs)
35
20
 
36
- cfg_cls = check.issubclass(cls.__dict__['Config'], Config)
37
- check.not_in(cfg_cls, _CONFIG_CLS_MAP)
38
- _CONFIG_CLS_MAP[cfg_cls] = cls
21
+ if not lang.is_abstract_class(cls):
22
+ check.in_('Config', cls.__dict__)
23
+ cfg_cls = check.issubclass(cls.Config, Configurable.Config)
24
+ check.not_in('configurable_cls', cfg_cls.__dict__)
25
+ check.state(dc.is_immediate_dataclass(cfg_cls))
26
+ cfg_cls.configurable_cls = cls
39
27
 
40
- def __init__(self, config: ConfigT) -> None:
28
+ def __init__(self, config: ConfigurableConfigT) -> None:
41
29
  super().__init__()
42
30
 
43
- self._config: ConfigT = check.isinstance(config, self.Config)
44
-
31
+ self._config: ConfigurableConfigT = check.isinstance(config, self.Config) # type: ignore[assignment]
45
32
 
46
- def get_impl(cfg: type[Config] | Config) -> type[Configurable]:
47
- if isinstance(cfg, type):
48
- cfg_cls = check.issubclass(cfg, Config) # noqa
49
- elif isinstance(cfg, Config):
50
- cfg_cls = type(cfg)
51
- else:
52
- raise TypeError(cfg)
53
- return _CONFIG_CLS_MAP[cfg_cls]
33
+ @property
34
+ def config(self) -> ConfigurableConfigT:
35
+ return self._config
@@ -13,6 +13,10 @@ from .matching import ( # noqa
13
13
  matched_config_rewrite as matched_rewrite,
14
14
  )
15
15
 
16
+ from .merging import ( # noqa
17
+ merge_configs as merge,
18
+ )
19
+
16
20
  from .names import ( # noqa
17
21
  build_config_named_children as build_named_children,
18
22
  )
@@ -0,0 +1,33 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - smarter merging than just dumb dict-squashing
6
+ """
7
+ import typing as ta
8
+
9
+ from ..types import ConfigMap
10
+
11
+
12
+ ##
13
+
14
+
15
+ def merge_configs(*ms: ConfigMap) -> ConfigMap:
16
+ def rec(o, i):
17
+ for k, v in i.items():
18
+ try:
19
+ e = o[k]
20
+ except KeyError:
21
+ o[k] = v
22
+ else:
23
+ if isinstance(e, ta.Mapping) and isinstance(v, ta.Mapping):
24
+ rec(e, v) # noqa
25
+ else:
26
+ if isinstance(v, ta.Mapping):
27
+ v = dict(v)
28
+ o[k] = v
29
+
30
+ o: dict = {}
31
+ for i in ms:
32
+ rec(o, i)
33
+ return o
@@ -0,0 +1,92 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - cascading?
6
+ - caching?
7
+ - purging?
8
+ - wishlist: piecewise config parts, unmarshal, registered via manifests
9
+ """
10
+ import abc
11
+ import os.path
12
+ import typing as ta
13
+
14
+ from ..lite.check import check
15
+ from ..os.mangle import mangle_path
16
+ from .formats import DEFAULT_CONFIG_LOADERS
17
+ from .formats import ConfigLoader
18
+
19
+
20
+ ShadowConfig = ta.Mapping[str, ta.Any] # ta.TypeAlias
21
+
22
+
23
+ ##
24
+
25
+
26
+ class ShadowConfigs(abc.ABC):
27
+ @abc.abstractmethod
28
+ def get_shadow_config(self, path: str) -> ta.Optional[ShadowConfig]:
29
+ raise NotImplementedError
30
+
31
+
32
+ class FileShadowConfigs(ShadowConfigs, abc.ABC):
33
+ @abc.abstractmethod
34
+ def get_shadow_config_file_path(self, path: str) -> str:
35
+ raise NotImplementedError
36
+
37
+
38
+ ##
39
+
40
+
41
+ class NopShadowConfigs(ShadowConfigs):
42
+ def get_shadow_config(self, path: str) -> ta.Optional[ShadowConfig]:
43
+ return None
44
+
45
+
46
+ ##
47
+
48
+
49
+ class MangledFilesShadowConfigs(FileShadowConfigs):
50
+ def __init__(
51
+ self,
52
+ path: str,
53
+ *,
54
+ create: bool = False,
55
+ ext: str = 'yml',
56
+ loader: ta.Optional[ConfigLoader] = None,
57
+ ) -> None: # noqa
58
+ super().__init__()
59
+
60
+ check.arg(not ext.startswith('.'))
61
+ check.non_empty_str(ext)
62
+
63
+ if loader is None:
64
+ for dl in DEFAULT_CONFIG_LOADERS:
65
+ if dl.match_file(f'.{ext}'):
66
+ loader = dl
67
+ break
68
+ else:
69
+ raise KeyError(ext)
70
+
71
+ self._path = path
72
+ self._create = create
73
+ self._ext = ext
74
+ self._loader = loader
75
+
76
+ def get_shadow_config_file_path(self, path: str) -> str:
77
+ mangled = '.'.join([mangle_path(os.path.abspath(path)), self._ext])
78
+ return os.path.abspath(os.path.join(self._path, mangled))
79
+
80
+ def get_shadow_config(self, path: str) -> ta.Optional[ShadowConfig]:
81
+ file_path = self.get_shadow_config_file_path(path)
82
+
83
+ if self._create:
84
+ os.makedirs(self._path, exist_ok=True)
85
+
86
+ try:
87
+ with open(file_path) as f:
88
+ content = f.read()
89
+ except FileNotFoundError:
90
+ return None
91
+
92
+ return self._loader.load_str(content).as_map()
omlish/daemons/daemon.py CHANGED
@@ -38,14 +38,9 @@ log = logging.getLogger(__name__)
38
38
 
39
39
 
40
40
  class Daemon:
41
- """
42
- Instances of these should most likely be effectively singletons, but that's up to the user.
43
- """
44
-
45
41
  @dc.dataclass(frozen=True, kw_only=True)
46
42
  class Config:
47
- target: Target
48
- spawning: Spawning
43
+ spawning: Spawning | None = None
49
44
 
50
45
  #
51
46
 
@@ -68,11 +63,20 @@ class Daemon:
68
63
  def __post_init__(self) -> None:
69
64
  check.isinstance(self.pid_file, (str, None))
70
65
 
71
- def __init__(self, config: Config) -> None:
66
+ def __init__(
67
+ self,
68
+ target: Target,
69
+ config: Config = Config(),
70
+ ) -> None:
72
71
  super().__init__()
73
72
 
73
+ self._target = target
74
74
  self._config = config
75
75
 
76
+ @property
77
+ def target(self) -> Target:
78
+ return self._target
79
+
76
80
  @property
77
81
  def config(self) -> Config:
78
82
  return self._config
@@ -124,8 +128,8 @@ class Daemon:
124
128
 
125
129
  def launch_no_wait(self) -> None:
126
130
  launcher = Launcher(
127
- target=self._config.target,
128
- spawning=self._config.spawning,
131
+ target=self._target,
132
+ spawning=check.not_none(self._config.spawning),
129
133
 
130
134
  pid_file=self._config.pid_file,
131
135
  reparent_process=self._config.reparent_process,
@@ -0,0 +1,108 @@
1
+ import abc
2
+ import typing as ta
3
+
4
+ from .. import cached
5
+ from .. import check
6
+ from .. import dataclasses as dc
7
+ from .. import lang
8
+ from ..configs.classes import Configurable
9
+ from .daemon import Daemon
10
+ from .targets import Target
11
+ from .targets import TargetRunner
12
+ from .targets import target_runner_for
13
+
14
+
15
+ ServiceConfigT = ta.TypeVar('ServiceConfigT', bound='Service.Config')
16
+
17
+
18
+ ##
19
+
20
+
21
+ class Service(Configurable[ServiceConfigT], lang.Abstract):
22
+ class Config(Configurable.Config, lang.Abstract):
23
+ pass
24
+
25
+ @classmethod
26
+ def from_config(cls, config: Config) -> 'Service':
27
+ return check.isinstance(config.configurable_cls(config), cls)
28
+
29
+ #
30
+
31
+ @abc.abstractmethod
32
+ def _run(self) -> None:
33
+ raise NotImplementedError
34
+
35
+ def run(self) -> None:
36
+ self._run()
37
+
38
+ @classmethod
39
+ def run_config(cls, config: Config) -> None:
40
+ return cls.from_config(config).run()
41
+
42
+
43
+ ##
44
+
45
+
46
+ class ServiceTarget(Target):
47
+ svc: Service
48
+
49
+
50
+ class ServiceTargetRunner(TargetRunner, dc.Frozen):
51
+ target: ServiceTarget
52
+
53
+ def run(self) -> None:
54
+ self.target.svc.run()
55
+
56
+
57
+ @target_runner_for.register
58
+ def _(target: ServiceTarget) -> ServiceTargetRunner:
59
+ return ServiceTargetRunner(target)
60
+
61
+
62
+ #
63
+
64
+
65
+ class ServiceConfigTarget(Target):
66
+ cfg: Service.Config
67
+
68
+
69
+ class ServiceConfigTargetRunner(TargetRunner, dc.Frozen):
70
+ target: ServiceConfigTarget
71
+
72
+ def run(self) -> None:
73
+ Service.run_config(self.target.cfg)
74
+
75
+
76
+ @target_runner_for.register
77
+ def _(target: ServiceConfigTarget) -> ServiceConfigTargetRunner:
78
+ return ServiceConfigTargetRunner(target)
79
+
80
+
81
+ ##
82
+
83
+
84
+ @dc.dataclass(frozen=True)
85
+ class ServiceDaemon(lang.Final):
86
+ service: Service | Service.Config
87
+
88
+ @cached.function
89
+ def service_(self) -> Service:
90
+ if isinstance(self.service, Service):
91
+ return self.service
92
+ elif isinstance(self.service, Service.Config):
93
+ return Service.from_config(self.service)
94
+ else:
95
+ raise TypeError(self.service)
96
+
97
+ #
98
+
99
+ daemon: Daemon | Daemon.Config = Daemon.Config()
100
+
101
+ @cached.function
102
+ def daemon_(self) -> Daemon:
103
+ if isinstance(self.daemon, Daemon):
104
+ return self.daemon
105
+ elif isinstance(self.daemon, Daemon.Config):
106
+ return Daemon(Target.of(self.service_()), self.daemon)
107
+ else:
108
+ raise TypeError(self.daemon)
omlish/daemons/targets.py CHANGED
@@ -10,15 +10,38 @@ from .. import lang
10
10
 
11
11
  if ta.TYPE_CHECKING:
12
12
  import runpy
13
+
14
+ from . import services
15
+
13
16
  else:
14
17
  runpy = lang.proxy_import('runpy')
15
18
 
19
+ services = lang.proxy_import('.services', __package__)
20
+
16
21
 
17
22
  ##
18
23
 
19
24
 
20
25
  class Target(dc.Case):
21
- pass
26
+ @classmethod
27
+ def of(cls, obj: ta.Any) -> 'Target':
28
+ if isinstance(obj, Target):
29
+ return obj
30
+
31
+ elif isinstance(obj, str):
32
+ return NameTarget(obj)
33
+
34
+ elif callable(obj):
35
+ return FnTarget(obj)
36
+
37
+ elif isinstance(obj, services.Service):
38
+ return services.ServiceTarget(obj)
39
+
40
+ elif isinstance(obj, services.Service.Config):
41
+ return services.ServiceConfigTarget(obj)
42
+
43
+ else:
44
+ raise TypeError(obj)
22
45
 
23
46
 
24
47
  class TargetRunner(abc.ABC):
@@ -36,14 +59,18 @@ def target_runner_for(target: Target) -> TargetRunner:
36
59
 
37
60
 
38
61
  class FnTarget(Target):
39
- fn: ta.Callable[[], None]
62
+ fn: ta.Callable[[], ta.Any]
40
63
 
41
64
 
42
65
  class FnTargetRunner(TargetRunner, dc.Frozen):
43
66
  target: FnTarget
44
67
 
45
68
  def run(self) -> None:
46
- self.target.fn()
69
+ obj = self.target.fn()
70
+ if obj is not None:
71
+ tgt = Target.of(obj)
72
+ tr = target_runner_for(tgt)
73
+ tr.run()
47
74
 
48
75
 
49
76
  @target_runner_for.register
@@ -57,6 +84,23 @@ def _(target: FnTarget) -> FnTargetRunner:
57
84
  class NameTarget(Target):
58
85
  name: str
59
86
 
87
+ @classmethod
88
+ def for_obj(
89
+ cls,
90
+ obj: ta.Any,
91
+ *,
92
+ globals: ta.Mapping[str, ta.Any] | None = None, # noqa
93
+ no_module_name_lookup: bool = False,
94
+ ) -> 'NameTarget':
95
+ if globals is None:
96
+ globals = obj.__globals__ # noqa
97
+ if not no_module_name_lookup:
98
+ mn = lang.get_real_module_name(globals)
99
+ else:
100
+ mn = globals['__name__']
101
+ qn = obj.__qualname__
102
+ return NameTarget('.'.join([mn, qn]))
103
+
60
104
 
61
105
  class NameTargetRunner(TargetRunner, dc.Frozen):
62
106
  target: NameTarget
@@ -67,7 +111,14 @@ class NameTargetRunner(TargetRunner, dc.Frozen):
67
111
  runpy._run_module_as_main(name) # type: ignore # noqa
68
112
  else:
69
113
  obj = lang.import_attr(self.target.name)
70
- obj()
114
+ tgt = Target.of(obj)
115
+ tr = target_runner_for(tgt)
116
+ tr.run()
117
+
118
+
119
+ @target_runner_for.register
120
+ def _(target: NameTarget) -> NameTargetRunner:
121
+ return NameTargetRunner(target)
71
122
 
72
123
 
73
124
  ##
@@ -76,11 +127,16 @@ class NameTargetRunner(TargetRunner, dc.Frozen):
76
127
  class ExecTarget(Target):
77
128
  cmd: ta.Sequence[str] = dc.xfield(coerce=check.of_not_isinstance(str))
78
129
 
130
+ cwd: str | None = None
131
+
79
132
 
80
133
  class ExecTargetRunner(TargetRunner, dc.Frozen):
81
134
  target: ExecTarget
82
135
 
83
136
  def run(self) -> None:
137
+ if (cwd := self.target.cwd) is not None:
138
+ os.chdir(os.path.expanduser(cwd))
139
+
84
140
  os.execl(*self.target.cmd)
85
141
 
86
142
 
@@ -91,9 +91,11 @@ from .impl.reflect import ( # noqa
91
91
  reflect,
92
92
  )
93
93
 
94
- from .utils import ( # noqa
95
- is_immediate_dataclass,
94
+ from .static import ( # noqa
95
+ Static,
96
+ )
96
97
 
98
+ from .utils import ( # noqa
97
99
  opt_repr,
98
100
  truthy_repr,
99
101
 
@@ -116,5 +118,7 @@ from .utils import ( # noqa
116
118
  ##
117
119
 
118
120
  from ..lite.dataclasses import ( # noqa
121
+ is_immediate_dataclass,
122
+
119
123
  dataclass_maybe_post_init as maybe_post_init,
120
124
  )