omlish 0.0.0.dev236__py3-none-any.whl → 0.0.0.dev238__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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
  )