ominfra 0.0.0.dev167__py3-none-any.whl → 0.0.0.dev169__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))
@@ -45,7 +45,6 @@ class DeployGitSpec:
45
45
  subtrees: ta.Optional[ta.Sequence[str]] = None
46
46
 
47
47
  def __post_init__(self) -> None:
48
- hash(self)
49
48
  check.non_empty_str(self.rev)
50
49
  if self.subtrees is not None:
51
50
  for st in self.subtrees:
@@ -85,7 +84,7 @@ class DeployConfLink(abc.ABC): # noqa
85
84
  """
86
85
  May be either:
87
86
  - @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
88
- - @conf/file - links a single file in a single subdir to conf/@conf/@dst-file
87
+ - @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
89
88
  - @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
90
89
  """
91
90
 
@@ -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
+ }
@@ -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
 
@@ -1533,6 +1535,28 @@ def strip_with_newline(s: str) -> str:
1533
1535
  return s.strip() + '\n'
1534
1536
 
1535
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
+
1536
1560
  ##
1537
1561
 
1538
1562