ominfra 0.0.0.dev166__py3-none-any.whl → 0.0.0.dev168__py3-none-any.whl

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