ominfra 0.0.0.dev167__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))
@@ -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