ominfra 0.0.0.dev166__py3-none-any.whl → 0.0.0.dev168__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.
@@ -0,0 +1,216 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - run/{.pid,.sock}
5
+ - logs/...
6
+ - current symlink
7
+ - conf/{nginx,supervisor}
8
+ - env/?
9
+ - apps/<app>/shared
10
+ """
11
+ import abc
12
+ import dataclasses as dc
13
+ import itertools
14
+ import typing as ta
15
+
16
+ from omlish.lite.check import check
17
+ from omlish.lite.strings import split_keep_delimiter
18
+
19
+ from ..types import DeployPathKind
20
+ from ..types import DeployPathPlaceholder
21
+
22
+
23
+ ##
24
+
25
+
26
+ DEPLOY_PATH_PLACEHOLDER_SIGIL = '@'
27
+ DEPLOY_PATH_PLACEHOLDER_SEPARATOR = '--'
28
+
29
+ DEPLOY_PATH_PLACEHOLDER_DELIMITERS: ta.AbstractSet[str] = frozenset([
30
+ DEPLOY_PATH_PLACEHOLDER_SEPARATOR,
31
+ '.',
32
+ ])
33
+
34
+ DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
35
+ 'app',
36
+ 'tag',
37
+ 'conf',
38
+ ])
39
+
40
+
41
+ class DeployPathError(Exception):
42
+ pass
43
+
44
+
45
+ class DeployPathRenderable(abc.ABC):
46
+ @abc.abstractmethod
47
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
48
+ raise NotImplementedError
49
+
50
+
51
+ ##
52
+
53
+
54
+ class DeployPathNamePart(DeployPathRenderable, abc.ABC):
55
+ @classmethod
56
+ def parse(cls, s: str) -> 'DeployPathNamePart':
57
+ check.non_empty_str(s)
58
+ if s.startswith(DEPLOY_PATH_PLACEHOLDER_SIGIL):
59
+ return PlaceholderDeployPathNamePart(s[1:])
60
+ elif s in DEPLOY_PATH_PLACEHOLDER_DELIMITERS:
61
+ return DelimiterDeployPathNamePart(s)
62
+ else:
63
+ return ConstDeployPathNamePart(s)
64
+
65
+
66
+ @dc.dataclass(frozen=True)
67
+ class PlaceholderDeployPathNamePart(DeployPathNamePart):
68
+ placeholder: str # DeployPathPlaceholder
69
+
70
+ def __post_init__(self) -> None:
71
+ check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
72
+
73
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
74
+ if placeholders is not None:
75
+ return placeholders[self.placeholder] # type: ignore
76
+ else:
77
+ return DEPLOY_PATH_PLACEHOLDER_SIGIL + self.placeholder
78
+
79
+
80
+ @dc.dataclass(frozen=True)
81
+ class DelimiterDeployPathNamePart(DeployPathNamePart):
82
+ delimiter: str
83
+
84
+ def __post_init__(self) -> None:
85
+ check.in_(self.delimiter, DEPLOY_PATH_PLACEHOLDER_DELIMITERS)
86
+
87
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
88
+ return self.delimiter
89
+
90
+
91
+ @dc.dataclass(frozen=True)
92
+ class ConstDeployPathNamePart(DeployPathNamePart):
93
+ const: str
94
+
95
+ def __post_init__(self) -> None:
96
+ check.non_empty_str(self.const)
97
+ for c in [*DEPLOY_PATH_PLACEHOLDER_DELIMITERS, DEPLOY_PATH_PLACEHOLDER_SIGIL, '/']:
98
+ check.not_in(c, self.const)
99
+
100
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
101
+ return self.const
102
+
103
+
104
+ @dc.dataclass(frozen=True)
105
+ class DeployPathName(DeployPathRenderable):
106
+ parts: ta.Sequence[DeployPathNamePart]
107
+
108
+ def __post_init__(self) -> None:
109
+ hash(self)
110
+ check.not_empty(self.parts)
111
+ for k, g in itertools.groupby(self.parts, type):
112
+ if len(gl := list(g)) > 1:
113
+ raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
114
+
115
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
116
+ return ''.join(p.render(placeholders) for p in self.parts)
117
+
118
+ @classmethod
119
+ def parse(cls, s: str) -> 'DeployPathName':
120
+ check.non_empty_str(s)
121
+ check.not_in('/', s)
122
+
123
+ i = 0
124
+ ps = []
125
+ while i < len(s):
126
+ ns = [(n, d) for d in DEPLOY_PATH_PLACEHOLDER_DELIMITERS if (n := s.find(d, i)) >= 0]
127
+ if not ns:
128
+ ps.append(s[i:])
129
+ break
130
+ n, d = min(ns)
131
+ ps.append(check.non_empty_str(s[i:n]))
132
+ ps.append(s[n:n + len(d)])
133
+ i = n + len(d)
134
+
135
+ return cls(tuple(DeployPathNamePart.parse(p) for p in ps))
136
+
137
+
138
+ ##
139
+
140
+
141
+ @dc.dataclass(frozen=True)
142
+ class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
143
+ name: DeployPathName
144
+
145
+ @property
146
+ @abc.abstractmethod
147
+ def kind(self) -> DeployPathKind:
148
+ raise NotImplementedError
149
+
150
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
151
+ return self.name.render(placeholders) + ('/' if self.kind == 'dir' else '')
152
+
153
+ @classmethod
154
+ def parse(cls, s: str) -> 'DeployPathPart':
155
+ if (is_dir := s.endswith('/')):
156
+ s = s[:-1]
157
+ check.non_empty_str(s)
158
+ check.not_in('/', s)
159
+
160
+ n = DeployPathName.parse(s)
161
+ if is_dir:
162
+ return DirDeployPathPart(n)
163
+ else:
164
+ return FileDeployPathPart(n)
165
+
166
+
167
+ class DirDeployPathPart(DeployPathPart):
168
+ @property
169
+ def kind(self) -> DeployPathKind:
170
+ return 'dir'
171
+
172
+
173
+ class FileDeployPathPart(DeployPathPart):
174
+ @property
175
+ def kind(self) -> DeployPathKind:
176
+ return 'file'
177
+
178
+
179
+ #
180
+
181
+
182
+ @dc.dataclass(frozen=True)
183
+ class DeployPath:
184
+ parts: ta.Sequence[DeployPathPart]
185
+
186
+ @property
187
+ def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
188
+ for p in self.parts:
189
+ yield from p.name.parts
190
+
191
+ def __post_init__(self) -> None:
192
+ hash(self)
193
+ check.not_empty(self.parts)
194
+ for p in self.parts[:-1]:
195
+ check.equal(p.kind, 'dir')
196
+
197
+ pd: ta.Dict[DeployPathPlaceholder, ta.List[int]] = {}
198
+ for i, np in enumerate(self.name_parts):
199
+ if isinstance(np, PlaceholderDeployPathNamePart):
200
+ pd.setdefault(ta.cast(DeployPathPlaceholder, np.placeholder), []).append(i)
201
+
202
+ # if 'tag' in pd and 'app' not in pd:
203
+ # raise DeployPathError('Tag placeholder in path without app', self)
204
+
205
+ @property
206
+ def kind(self) -> ta.Literal['file', 'dir']:
207
+ return self.parts[-1].kind
208
+
209
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
210
+ return ''.join([p.render(placeholders) for p in self.parts])
211
+
212
+ @classmethod
213
+ def parse(cls, s: str) -> 'DeployPath':
214
+ check.non_empty_str(s)
215
+ ps = split_keep_delimiter(s, '/')
216
+ return cls(tuple(DeployPathPart.parse(p) for p in ps))
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ import abc
2
3
  import dataclasses as dc
3
4
  import hashlib
4
5
  import typing as ta
@@ -14,6 +15,17 @@ from .types import DeployRev
14
15
  ##
15
16
 
16
17
 
18
+ def check_valid_deploy_spec_path(s: str) -> str:
19
+ check.non_empty_str(s)
20
+ for c in ['..', '//']:
21
+ check.not_in(c, s)
22
+ check.arg(not s.startswith('/'))
23
+ return s
24
+
25
+
26
+ ##
27
+
28
+
17
29
  @dc.dataclass(frozen=True)
18
30
  class DeployGitRepo:
19
31
  host: ta.Optional[str] = None
@@ -26,14 +38,13 @@ class DeployGitRepo:
26
38
 
27
39
 
28
40
  @dc.dataclass(frozen=True)
29
- class DeployGitCheckout:
41
+ class DeployGitSpec:
30
42
  repo: DeployGitRepo
31
43
  rev: DeployRev
32
44
 
33
45
  subtrees: ta.Optional[ta.Sequence[str]] = None
34
46
 
35
47
  def __post_init__(self) -> None:
36
- hash(self)
37
48
  check.non_empty_str(self.rev)
38
49
  if self.subtrees is not None:
39
50
  for st in self.subtrees:
@@ -52,8 +63,62 @@ class DeployVenvSpec:
52
63
 
53
64
  use_uv: bool = False
54
65
 
66
+
67
+ ##
68
+
69
+
70
+ @dc.dataclass(frozen=True)
71
+ class DeployConfFile:
72
+ path: str
73
+ body: str
74
+
55
75
  def __post_init__(self) -> None:
56
- hash(self)
76
+ check_valid_deploy_spec_path(self.path)
77
+
78
+
79
+ #
80
+
81
+
82
+ @dc.dataclass(frozen=True)
83
+ class DeployConfLink(abc.ABC): # noqa
84
+ """
85
+ May be either:
86
+ - @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
87
+ - @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
88
+ - @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
89
+ """
90
+
91
+ src: str
92
+
93
+ def __post_init__(self) -> None:
94
+ check_valid_deploy_spec_path(self.src)
95
+ if '/' in self.src:
96
+ check.equal(self.src.count('/'), 1)
97
+
98
+
99
+ class AppDeployConfLink(DeployConfLink):
100
+ pass
101
+
102
+
103
+ class TagDeployConfLink(DeployConfLink):
104
+ pass
105
+
106
+
107
+ #
108
+
109
+
110
+ @dc.dataclass(frozen=True)
111
+ class DeployConfSpec:
112
+ files: ta.Optional[ta.Sequence[DeployConfFile]] = None
113
+
114
+ links: ta.Optional[ta.Sequence[DeployConfLink]] = None
115
+
116
+ def __post_init__(self) -> None:
117
+ if self.files:
118
+ seen: ta.Set[str] = set()
119
+ for f in self.files:
120
+ check.not_in(f.path, seen)
121
+ seen.add(f.path)
57
122
 
58
123
 
59
124
  ##
@@ -62,12 +127,12 @@ class DeployVenvSpec:
62
127
  @dc.dataclass(frozen=True)
63
128
  class DeploySpec:
64
129
  app: DeployApp
65
- checkout: DeployGitCheckout
130
+
131
+ git: DeployGitSpec
66
132
 
67
133
  venv: ta.Optional[DeployVenvSpec] = None
68
134
 
69
- def __post_init__(self) -> None:
70
- hash(self)
135
+ conf: ta.Optional[DeployConfSpec] = None
71
136
 
72
137
  @cached_nullary
73
138
  def key(self) -> DeployKey:
@@ -8,7 +8,7 @@ from omlish.os.atomics import AtomicPathSwapKind
8
8
  from omlish.os.atomics import AtomicPathSwapping
9
9
  from omlish.os.atomics import TempDirAtomicPathSwapping
10
10
 
11
- from .paths import SingleDirDeployPathOwner
11
+ from .paths.owners import SingleDirDeployPathOwner
12
12
  from .types import DeployHome
13
13
 
14
14
 
@@ -1,5 +1,15 @@
1
+ import dataclasses as dc
1
2
  import typing as ta
2
3
 
4
+ from omlish.lite.check import check
5
+
6
+
7
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
8
+ DeployPathPlaceholder = ta.Literal['app', 'tag', 'conf'] # ta.TypeAlias
9
+
10
+
11
+ ##
12
+
3
13
 
4
14
  DeployHome = ta.NewType('DeployHome', str)
5
15
 
@@ -9,6 +19,21 @@ DeployRev = ta.NewType('DeployRev', str)
9
19
  DeployKey = ta.NewType('DeployKey', str)
10
20
 
11
21
 
12
- class DeployAppTag(ta.NamedTuple):
22
+ ##
23
+
24
+
25
+ @dc.dataclass(frozen=True)
26
+ class DeployAppTag:
13
27
  app: DeployApp
14
28
  tag: DeployTag
29
+
30
+ def __post_init__(self) -> None:
31
+ for s in [self.app, self.tag]:
32
+ check.non_empty_str(s)
33
+ check.equal(s, s.strip())
34
+
35
+ def placeholders(self) -> ta.Mapping[DeployPathPlaceholder, str]:
36
+ return {
37
+ 'app': self.app,
38
+ 'tag': self.tag,
39
+ }
@@ -5,46 +5,28 @@ TODO:
5
5
  - share more code with pyproject?
6
6
  """
7
7
  import os.path
8
- import typing as ta
9
8
 
10
9
  from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
11
- from omlish.lite.cached import cached_nullary
12
- from omlish.lite.check import check
13
10
  from omlish.os.atomics import AtomicPathSwapping
14
11
 
15
- from .paths import DeployPath
16
- from .paths import DeployPathOwner
17
12
  from .specs import DeployVenvSpec
18
- from .types import DeployAppTag
19
- from .types import DeployHome
20
13
 
21
14
 
22
- class DeployVenvManager(DeployPathOwner):
15
+ class DeployVenvManager:
23
16
  def __init__(
24
17
  self,
25
18
  *,
26
- deploy_home: ta.Optional[DeployHome] = None,
27
19
  atomics: AtomicPathSwapping,
28
20
  ) -> None:
29
21
  super().__init__()
30
22
 
31
- self._deploy_home = deploy_home
32
23
  self._atomics = atomics
33
24
 
34
- @cached_nullary
35
- def _dir(self) -> str:
36
- return os.path.join(check.non_empty_str(self._deploy_home), 'venvs')
37
-
38
- def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
39
- return {
40
- DeployPath.parse('venvs/@app/@tag/'),
41
- }
42
-
43
25
  async def setup_venv(
44
26
  self,
45
- app_dir: str,
46
- venv_dir: str,
47
27
  spec: DeployVenvSpec,
28
+ git_dir: str,
29
+ venv_dir: str,
48
30
  ) -> None:
49
31
  sys_exe = 'python3'
50
32
 
@@ -58,7 +40,7 @@ class DeployVenvManager(DeployPathOwner):
58
40
 
59
41
  #
60
42
 
61
- reqs_txt = os.path.join(app_dir, 'requirements.txt')
43
+ reqs_txt = os.path.join(git_dir, 'requirements.txt')
62
44
 
63
45
  if os.path.isfile(reqs_txt):
64
46
  if spec.use_uv:
@@ -68,14 +50,3 @@ class DeployVenvManager(DeployPathOwner):
68
50
  pip_cmd = ['-m', 'pip']
69
51
 
70
52
  await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
71
-
72
- async def setup_app_venv(
73
- self,
74
- app_tag: DeployAppTag,
75
- spec: DeployVenvSpec,
76
- ) -> None:
77
- await self.setup_venv(
78
- os.path.join(check.non_empty_str(self._deploy_home), 'apps', app_tag.app, app_tag.tag),
79
- os.path.join(self._dir(), app_tag.app, app_tag.tag),
80
- spec,
81
- )
@@ -961,6 +961,8 @@ def async_cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
961
961
  """
962
962
  TODO:
963
963
  - def maybe(v: lang.Maybe[T])
964
+ - def not_ ?
965
+ - ** class @dataclass Raise - user message should be able to be an exception type or instance or factory
964
966
  """
965
967
 
966
968
 
@@ -1527,6 +1529,37 @@ def snake_case(name: str) -> str:
1527
1529
  ##
1528
1530
 
1529
1531
 
1532
+ def strip_with_newline(s: str) -> str:
1533
+ if not s:
1534
+ return ''
1535
+ return s.strip() + '\n'
1536
+
1537
+
1538
+ @ta.overload
1539
+ def split_keep_delimiter(s: str, d: str) -> str:
1540
+ ...
1541
+
1542
+
1543
+ @ta.overload
1544
+ def split_keep_delimiter(s: bytes, d: bytes) -> bytes:
1545
+ ...
1546
+
1547
+
1548
+ def split_keep_delimiter(s, d):
1549
+ ps = []
1550
+ i = 0
1551
+ while i < len(s):
1552
+ if (n := s.find(d, i)) < i:
1553
+ ps.append(s[i:])
1554
+ break
1555
+ ps.append(s[i:n + 1])
1556
+ i = n + 1
1557
+ return ps
1558
+
1559
+
1560
+ ##
1561
+
1562
+
1530
1563
  def is_dunder(name: str) -> bool:
1531
1564
  return (
1532
1565
  name[:2] == name[-2:] == '__' and