ominfra 0.0.0.dev7__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.
Files changed (44) hide show
  1. ominfra/__about__.py +27 -0
  2. ominfra/__init__.py +0 -0
  3. ominfra/bootstrap/__init__.py +0 -0
  4. ominfra/bootstrap/bootstrap.py +8 -0
  5. ominfra/cmds.py +83 -0
  6. ominfra/deploy/__init__.py +0 -0
  7. ominfra/deploy/_executor.py +1036 -0
  8. ominfra/deploy/configs.py +19 -0
  9. ominfra/deploy/executor/__init__.py +1 -0
  10. ominfra/deploy/executor/base.py +115 -0
  11. ominfra/deploy/executor/concerns/__init__.py +0 -0
  12. ominfra/deploy/executor/concerns/dirs.py +28 -0
  13. ominfra/deploy/executor/concerns/nginx.py +47 -0
  14. ominfra/deploy/executor/concerns/repo.py +17 -0
  15. ominfra/deploy/executor/concerns/supervisor.py +46 -0
  16. ominfra/deploy/executor/concerns/systemd.py +88 -0
  17. ominfra/deploy/executor/concerns/user.py +25 -0
  18. ominfra/deploy/executor/concerns/venv.py +22 -0
  19. ominfra/deploy/executor/main.py +119 -0
  20. ominfra/deploy/poly/__init__.py +1 -0
  21. ominfra/deploy/poly/_main.py +725 -0
  22. ominfra/deploy/poly/base.py +179 -0
  23. ominfra/deploy/poly/configs.py +38 -0
  24. ominfra/deploy/poly/deploy.py +25 -0
  25. ominfra/deploy/poly/main.py +18 -0
  26. ominfra/deploy/poly/nginx.py +60 -0
  27. ominfra/deploy/poly/repo.py +41 -0
  28. ominfra/deploy/poly/runtime.py +39 -0
  29. ominfra/deploy/poly/site.py +11 -0
  30. ominfra/deploy/poly/supervisor.py +64 -0
  31. ominfra/deploy/poly/venv.py +52 -0
  32. ominfra/deploy/remote.py +91 -0
  33. ominfra/pyremote/__init__.py +0 -0
  34. ominfra/pyremote/_runcommands.py +824 -0
  35. ominfra/pyremote/bootstrap.py +149 -0
  36. ominfra/pyremote/runcommands.py +56 -0
  37. ominfra/ssh.py +191 -0
  38. ominfra/tools/__init__.py +0 -0
  39. ominfra/tools/listresources.py +256 -0
  40. ominfra-0.0.0.dev7.dist-info/LICENSE +21 -0
  41. ominfra-0.0.0.dev7.dist-info/METADATA +19 -0
  42. ominfra-0.0.0.dev7.dist-info/RECORD +44 -0
  43. ominfra-0.0.0.dev7.dist-info/WHEEL +5 -0
  44. ominfra-0.0.0.dev7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,725 @@
1
+ #!/usr/bin/env python3
2
+ # noinspection DuplicatedCode
3
+ # @omdev-amalg-output main.py
4
+ import abc
5
+ import dataclasses as dc
6
+ import functools
7
+ import inspect
8
+ import json
9
+ import logging
10
+ import os
11
+ import os.path
12
+ import shlex
13
+ import stat
14
+ import subprocess
15
+ import sys
16
+ import textwrap
17
+ import typing as ta
18
+
19
+
20
+ T = ta.TypeVar('T')
21
+ ConcernT = ta.TypeVar('ConcernT')
22
+ ConfigT = ta.TypeVar('ConfigT')
23
+ SiteConcernConfigT = ta.TypeVar('SiteConcernConfigT', bound='SiteConcernConfig')
24
+ DeployConcernConfigT = ta.TypeVar('DeployConcernConfigT', bound='DeployConcernConfig')
25
+
26
+
27
+ ########################################
28
+ # ../configs.py
29
+ # ruff: noqa: UP006
30
+
31
+
32
+ ##
33
+
34
+
35
+ @dc.dataclass(frozen=True)
36
+ class SiteConcernConfig(abc.ABC): # noqa
37
+ pass
38
+
39
+
40
+ @dc.dataclass(frozen=True)
41
+ class SiteConfig:
42
+ user = 'omlish'
43
+
44
+ root_dir: str = '~/deploy'
45
+
46
+ concerns: ta.List[SiteConcernConfig] = dc.field(default_factory=list)
47
+
48
+
49
+ ##
50
+
51
+
52
+ @dc.dataclass(frozen=True)
53
+ class DeployConcernConfig(abc.ABC): # noqa
54
+ pass
55
+
56
+
57
+ @dc.dataclass(frozen=True)
58
+ class DeployConfig:
59
+ site: SiteConfig
60
+
61
+ name: str
62
+
63
+ concerns: ta.List[DeployConcernConfig] = dc.field(default_factory=list)
64
+
65
+
66
+ ########################################
67
+ # ../../../../omlish/lite/cached.py
68
+
69
+
70
+ class cached_nullary: # noqa
71
+ def __init__(self, fn):
72
+ super().__init__()
73
+ self._fn = fn
74
+ self._value = self._missing = object()
75
+ functools.update_wrapper(self, fn)
76
+
77
+ def __call__(self, *args, **kwargs): # noqa
78
+ if self._value is self._missing:
79
+ self._value = self._fn()
80
+ return self._value
81
+
82
+ def __get__(self, instance, owner): # noqa
83
+ bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
84
+ return bound
85
+
86
+
87
+ ########################################
88
+ # ../../../../omlish/lite/json.py
89
+
90
+
91
+ ##
92
+
93
+
94
+ JSON_PRETTY_INDENT = 2
95
+
96
+ JSON_PRETTY_KWARGS: ta.Mapping[str, ta.Any] = dict(
97
+ indent=JSON_PRETTY_INDENT,
98
+ )
99
+
100
+ json_dump_pretty: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON_PRETTY_KWARGS) # type: ignore
101
+ json_dumps_pretty: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_PRETTY_KWARGS)
102
+
103
+
104
+ ##
105
+
106
+
107
+ JSON_COMPACT_SEPARATORS = (',', ':')
108
+
109
+ JSON_COMPACT_KWARGS: ta.Mapping[str, ta.Any] = dict(
110
+ indent=None,
111
+ separators=JSON_COMPACT_SEPARATORS,
112
+ )
113
+
114
+ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON_COMPACT_KWARGS) # type: ignore
115
+ json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
116
+
117
+
118
+ ########################################
119
+ # ../base.py
120
+ # ruff: noqa: UP006 UP007
121
+
122
+
123
+ ##
124
+
125
+
126
+ @dc.dataclass(frozen=True)
127
+ class FsItem(abc.ABC):
128
+ path: str
129
+
130
+ @property
131
+ @abc.abstractmethod
132
+ def is_dir(self) -> bool:
133
+ raise NotImplementedError
134
+
135
+
136
+ @dc.dataclass(frozen=True)
137
+ class FsFile(FsItem):
138
+ @property
139
+ def is_dir(self) -> bool:
140
+ return False
141
+
142
+
143
+ @dc.dataclass(frozen=True)
144
+ class FsDir(FsItem):
145
+ @property
146
+ def is_dir(self) -> bool:
147
+ return True
148
+
149
+
150
+ ##
151
+
152
+
153
+ class Runtime(abc.ABC):
154
+ class Stat(ta.NamedTuple):
155
+ path: str
156
+ is_dir: bool
157
+
158
+ @abc.abstractmethod
159
+ def stat(self, p: str) -> ta.Optional[Stat]:
160
+ raise NotImplementedError
161
+
162
+ @abc.abstractmethod
163
+ def make_dirs(self, p: str, exist_ok: bool = False) -> None:
164
+ raise NotImplementedError
165
+
166
+ @abc.abstractmethod
167
+ def write_file(self, p: str, c: ta.Union[str, bytes]) -> None:
168
+ raise NotImplementedError
169
+
170
+ @abc.abstractmethod
171
+ def sh(self, *ss: str) -> None:
172
+ raise NotImplementedError
173
+
174
+
175
+ ##
176
+
177
+
178
+ class ConcernsContainer(abc.ABC, ta.Generic[ConcernT, ConfigT]):
179
+ concern_cls: ta.ClassVar[type]
180
+
181
+ def __init__(
182
+ self,
183
+ config: ConfigT,
184
+ ) -> None:
185
+ super().__init__()
186
+ self._config = config
187
+
188
+ concern_cls_dct = self._concern_cls_by_config_cls()
189
+ self._concerns = [
190
+ concern_cls_dct[type(c)](c, self) # type: ignore
191
+ for c in config.concerns # type: ignore
192
+ ]
193
+ self._concerns_by_cls: ta.Dict[ta.Type[ConcernT], ConcernT] = {}
194
+ for c in self._concerns:
195
+ if type(c) in self._concerns_by_cls:
196
+ raise TypeError(f'Duplicate concern type: {c}')
197
+ self._concerns_by_cls[type(c)] = c
198
+
199
+ @classmethod
200
+ def _concern_cls_by_config_cls(cls) -> ta.Mapping[type, ta.Type[ConcernT]]:
201
+ return { # noqa
202
+ c.Config: c # type: ignore
203
+ for c in cls.concern_cls.__subclasses__()
204
+ }
205
+
206
+ @property
207
+ def config(self) -> ConfigT:
208
+ return self._config
209
+
210
+ @property
211
+ def concerns(self) -> ta.List[ConcernT]:
212
+ return self._concerns
213
+
214
+ def concern(self, cls: ta.Type[T]) -> T:
215
+ return self._concerns_by_cls[cls] # type: ignore
216
+
217
+
218
+ ##
219
+
220
+
221
+ SiteConcernT = ta.TypeVar('SiteConcernT', bound='SiteConcern')
222
+
223
+
224
+ class SiteConcern(abc.ABC, ta.Generic[SiteConcernConfigT]):
225
+ def __init__(self, config: SiteConcernConfigT, site: 'Site') -> None:
226
+ super().__init__()
227
+ self._config = config
228
+ self._site = site
229
+
230
+ @property
231
+ def config(self) -> SiteConcernConfigT:
232
+ return self._config
233
+
234
+ @abc.abstractmethod
235
+ def run(self, runtime: Runtime) -> None:
236
+ raise NotImplementedError
237
+
238
+
239
+ ##
240
+
241
+
242
+ class Site(ConcernsContainer[SiteConcern, SiteConfig]):
243
+ @abc.abstractmethod
244
+ def run(self, runtime: Runtime) -> None:
245
+ raise NotImplementedError
246
+
247
+
248
+ ##
249
+
250
+
251
+ DeployConcernT = ta.TypeVar('DeployConcernT', bound='DeployConcern')
252
+
253
+
254
+ class DeployConcern(abc.ABC, ta.Generic[DeployConcernConfigT]):
255
+ def __init__(self, config: DeployConcernConfigT, deploy: 'Deploy') -> None:
256
+ super().__init__()
257
+ self._config = config
258
+ self._deploy = deploy
259
+
260
+ @property
261
+ def config(self) -> DeployConcernConfigT:
262
+ return self._config
263
+
264
+ def fs_items(self) -> ta.Sequence[FsItem]:
265
+ return []
266
+
267
+ @abc.abstractmethod
268
+ def run(self, runtime: Runtime) -> None:
269
+ raise NotImplementedError
270
+
271
+
272
+ ##
273
+
274
+
275
+ class Deploy(ConcernsContainer[DeployConcern, DeployConfig]):
276
+ @property
277
+ @abc.abstractmethod
278
+ def site(self) -> Site:
279
+ raise NotImplementedError
280
+
281
+ @abc.abstractmethod
282
+ def run(self, runtime: Runtime) -> None:
283
+ raise NotImplementedError
284
+
285
+
286
+ ########################################
287
+ # ../../../../omlish/lite/logs.py
288
+ """
289
+ TODO:
290
+ - debug
291
+ """
292
+ # ruff: noqa: UP007
293
+
294
+
295
+ log = logging.getLogger(__name__)
296
+
297
+
298
+ class JsonLogFormatter(logging.Formatter):
299
+
300
+ KEYS: ta.Mapping[str, bool] = {
301
+ 'name': False,
302
+ 'msg': False,
303
+ 'args': False,
304
+ 'levelname': False,
305
+ 'levelno': False,
306
+ 'pathname': False,
307
+ 'filename': False,
308
+ 'module': False,
309
+ 'exc_info': True,
310
+ 'exc_text': True,
311
+ 'stack_info': True,
312
+ 'lineno': False,
313
+ 'funcName': False,
314
+ 'created': False,
315
+ 'msecs': False,
316
+ 'relativeCreated': False,
317
+ 'thread': False,
318
+ 'threadName': False,
319
+ 'processName': False,
320
+ 'process': False,
321
+ }
322
+
323
+ def format(self, record: logging.LogRecord) -> str:
324
+ dct = {
325
+ k: v
326
+ for k, o in self.KEYS.items()
327
+ for v in [getattr(record, k)]
328
+ if not (o and v is None)
329
+ }
330
+ return json_dumps_compact(dct)
331
+
332
+
333
+ def configure_standard_logging(level: ta.Union[int, str] = logging.INFO) -> None:
334
+ logging.root.addHandler(logging.StreamHandler())
335
+ logging.root.setLevel(level)
336
+
337
+
338
+ ########################################
339
+ # ../../../../omlish/lite/runtime.py
340
+
341
+
342
+ @cached_nullary
343
+ def is_debugger_attached() -> bool:
344
+ return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
345
+
346
+
347
+ REQUIRED_PYTHON_VERSION = (3, 8)
348
+
349
+
350
+ def check_runtime_version() -> None:
351
+ if sys.version_info < REQUIRED_PYTHON_VERSION:
352
+ raise OSError(
353
+ f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
354
+
355
+
356
+ ########################################
357
+ # ../deploy.py
358
+
359
+
360
+ class DeployImpl(Deploy):
361
+ concern_cls = DeployConcern
362
+
363
+ def __init__(
364
+ self,
365
+ config: DeployConfig,
366
+ site: Site,
367
+ ) -> None:
368
+ super().__init__(config)
369
+ self._site = site
370
+
371
+ @property
372
+ def site(self) -> Site:
373
+ return self._site
374
+
375
+ def run(self, runtime: Runtime) -> None:
376
+ for c in self._concerns:
377
+ c.run(runtime)
378
+
379
+
380
+ ########################################
381
+ # ../nginx.py
382
+
383
+
384
+ class NginxSiteConcern(SiteConcern['NginxSiteConcern.Config']):
385
+ @dc.dataclass(frozen=True)
386
+ class Config(SiteConcernConfig):
387
+ global_conf_file: str = '/etc/nginx/sites-enabled/omlish.conf'
388
+
389
+ @cached_nullary
390
+ def confs_dir(self) -> str:
391
+ return os.path.join(self._site.config.root_dir, 'conf', 'nginx')
392
+
393
+ def run(self, runtime: Runtime) -> None:
394
+ if runtime.stat(self._config.global_conf_file) is None:
395
+ runtime.write_file(
396
+ self._config.global_conf_file,
397
+ f'include {self.confs_dir()}/*.conf;\n',
398
+ )
399
+
400
+
401
+ class NginxDeployConcern(DeployConcern['NginxDeployConcern.Config']):
402
+ @dc.dataclass(frozen=True)
403
+ class Config(DeployConcernConfig):
404
+ listen_port: int = 80
405
+ proxy_port: int = 8000
406
+
407
+ @cached_nullary
408
+ def conf_file(self) -> str:
409
+ return os.path.join(self._deploy.site.concern(NginxSiteConcern).confs_dir(), self._deploy.config.name + '.conf')
410
+
411
+ @cached_nullary
412
+ def fs_items(self) -> ta.Sequence[FsItem]:
413
+ return [FsFile(self.conf_file())]
414
+
415
+ def run(self, runtime: Runtime) -> None:
416
+ runtime.make_dirs(os.path.dirname(self.conf_file()))
417
+
418
+ conf = textwrap.dedent(f"""
419
+ server {{
420
+ listen {self._config.listen_port};
421
+ location / {{
422
+ proxy_pass http://127.0.0.1:{self._config.proxy_port}/;
423
+ }}
424
+ }}
425
+ """)
426
+
427
+ runtime.write_file(self.conf_file(), conf)
428
+
429
+
430
+ ########################################
431
+ # ../repo.py
432
+
433
+
434
+ class RepoDeployConcern(DeployConcern['RepoDeployConcern.Config']):
435
+ @dc.dataclass(frozen=True)
436
+ class Config(DeployConcernConfig):
437
+ url: str
438
+ revision: str = 'master'
439
+ init_submodules: bool = False
440
+
441
+ @cached_nullary
442
+ def repo_dir(self) -> str:
443
+ return os.path.join(self._deploy.site.config.root_dir, 'repos', self._deploy.config.name)
444
+
445
+ @cached_nullary
446
+ def fs_items(self) -> ta.Sequence[FsItem]:
447
+ return [FsDir(self.repo_dir())]
448
+
449
+ def run(self, runtime: Runtime) -> None:
450
+ runtime.make_dirs(self.repo_dir())
451
+
452
+ runtime.sh(
453
+ f'cd {self.repo_dir()}',
454
+ 'git init',
455
+ f'git remote add origin {self._config.url}',
456
+ f'git fetch --depth 1 origin {self._config.revision}',
457
+ 'git checkout FETCH_HEAD',
458
+ *([
459
+ 'git submodule update --init',
460
+ ] if self._config.init_submodules else []),
461
+ )
462
+
463
+
464
+ ########################################
465
+ # ../site.py
466
+
467
+
468
+ class SiteImpl(Site):
469
+ concern_cls = SiteConcern
470
+
471
+ def run(self, runtime: Runtime) -> None:
472
+ for c in self._concerns:
473
+ c.run(runtime)
474
+
475
+
476
+ ########################################
477
+ # ../../../../omlish/lite/subprocesses.py
478
+ # ruff: noqa: UP006 UP007
479
+
480
+
481
+ ##
482
+
483
+
484
+ _SUBPROCESS_SHELL_WRAP_EXECS = False
485
+
486
+
487
+ def subprocess_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
488
+ return ('sh', '-c', ' '.join(map(shlex.quote, args)))
489
+
490
+
491
+ def subprocess_maybe_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
492
+ if _SUBPROCESS_SHELL_WRAP_EXECS or is_debugger_attached():
493
+ return subprocess_shell_wrap_exec(*args)
494
+ else:
495
+ return args
496
+
497
+
498
+ def _prepare_subprocess_invocation(
499
+ *args: str,
500
+ env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
501
+ extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
502
+ quiet: bool = False,
503
+ shell: bool = False,
504
+ **kwargs: ta.Any,
505
+ ) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
506
+ log.debug(args)
507
+ if extra_env:
508
+ log.debug(extra_env)
509
+
510
+ if extra_env:
511
+ env = {**(env if env is not None else os.environ), **extra_env}
512
+
513
+ if quiet and 'stderr' not in kwargs:
514
+ if not log.isEnabledFor(logging.DEBUG):
515
+ kwargs['stderr'] = subprocess.DEVNULL
516
+
517
+ if not shell:
518
+ args = subprocess_maybe_shell_wrap_exec(*args)
519
+
520
+ return args, dict(
521
+ env=env,
522
+ shell=shell,
523
+ **kwargs,
524
+ )
525
+
526
+
527
+ def subprocess_check_call(*args: str, stdout=sys.stderr, **kwargs: ta.Any) -> None:
528
+ args, kwargs = _prepare_subprocess_invocation(*args, stdout=stdout, **kwargs)
529
+ return subprocess.check_call(args, **kwargs) # type: ignore
530
+
531
+
532
+ def subprocess_check_output(*args: str, **kwargs: ta.Any) -> bytes:
533
+ args, kwargs = _prepare_subprocess_invocation(*args, **kwargs)
534
+ return subprocess.check_output(args, **kwargs)
535
+
536
+
537
+ def subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
538
+ return subprocess_check_output(*args, **kwargs).decode().strip()
539
+
540
+
541
+ ##
542
+
543
+
544
+ DEFAULT_SUBPROCESS_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
545
+ FileNotFoundError,
546
+ subprocess.CalledProcessError,
547
+ )
548
+
549
+
550
+ def subprocess_try_call(
551
+ *args: str,
552
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
553
+ **kwargs: ta.Any,
554
+ ) -> bool:
555
+ try:
556
+ subprocess_check_call(*args, **kwargs)
557
+ except try_exceptions as e: # noqa
558
+ if log.isEnabledFor(logging.DEBUG):
559
+ log.exception('command failed')
560
+ return False
561
+ else:
562
+ return True
563
+
564
+
565
+ def subprocess_try_output(
566
+ *args: str,
567
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
568
+ **kwargs: ta.Any,
569
+ ) -> ta.Optional[bytes]:
570
+ try:
571
+ return subprocess_check_output(*args, **kwargs)
572
+ except try_exceptions as e: # noqa
573
+ if log.isEnabledFor(logging.DEBUG):
574
+ log.exception('command failed')
575
+ return None
576
+
577
+
578
+ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
579
+ out = subprocess_try_output(*args, **kwargs)
580
+ return out.decode().strip() if out is not None else None
581
+
582
+
583
+ ########################################
584
+ # ../runtime.py
585
+ # ruff: noqa: UP007
586
+
587
+
588
+ class RuntimeImpl(Runtime):
589
+ def __init__(self) -> None:
590
+ super().__init__()
591
+
592
+ def stat(self, p: str) -> ta.Optional[Runtime.Stat]:
593
+ try:
594
+ st = os.stat(p)
595
+ except FileNotFoundError:
596
+ return None
597
+ else:
598
+ return Runtime.Stat(
599
+ path=p,
600
+ is_dir=bool(st.st_mode & stat.S_IFDIR),
601
+ )
602
+
603
+ def make_dirs(self, p: str, exist_ok: bool = False) -> None:
604
+ os.makedirs(p, exist_ok=exist_ok)
605
+
606
+ def write_file(self, p: str, c: ta.Union[str, bytes]) -> None:
607
+ if os.path.exists(p):
608
+ raise RuntimeError(f'Path exists: {p}')
609
+ with open(p, 'w' if isinstance(c, str) else 'wb') as f:
610
+ f.write(c)
611
+
612
+ def sh(self, *ss: str) -> None:
613
+ s = ' && '.join(ss)
614
+ log.info('Executing: %s', s)
615
+ subprocess_check_call(s, shell=True)
616
+
617
+
618
+ ########################################
619
+ # ../venv.py
620
+
621
+
622
+ class VenvDeployConcern(DeployConcern['VenvDeployConcern.Config']):
623
+ @dc.dataclass(frozen=True)
624
+ class Config(DeployConcernConfig):
625
+ interp_version: str
626
+ requirements_txt: str = 'requirements.txt'
627
+
628
+ @cached_nullary
629
+ def venv_dir(self) -> str:
630
+ return os.path.join(self._deploy.site.config.root_dir, 'venvs', self._deploy.config.name)
631
+
632
+ @cached_nullary
633
+ def fs_items(self) -> ta.Sequence[FsItem]:
634
+ return [FsDir(self.venv_dir())]
635
+
636
+ @cached_nullary
637
+ def exe(self) -> str:
638
+ return os.path.join(self.venv_dir(), 'bin', 'python')
639
+
640
+ def run(self, runtime: Runtime) -> None:
641
+ runtime.make_dirs(self.venv_dir())
642
+
643
+ rd = self._deploy.concern(RepoDeployConcern).repo_dir()
644
+
645
+ l, r = os.path.split(self.venv_dir())
646
+
647
+ # FIXME: lol
648
+ py_exe = 'python3'
649
+
650
+ runtime.sh(
651
+ f'cd {l}',
652
+ f'{py_exe} -mvenv {r}',
653
+
654
+ # https://stackoverflow.com/questions/77364550/attributeerror-module-pkgutil-has-no-attribute-impimporter-did-you-mean
655
+ f'{self.exe()} -m ensurepip',
656
+ f'{self.exe()} -mpip install --upgrade setuptools pip',
657
+
658
+ f'{self.exe()} -mpip install -r {rd}/{self._config.requirements_txt}', # noqa
659
+ )
660
+
661
+
662
+ ########################################
663
+ # ../supervisor.py
664
+
665
+
666
+ # class SupervisorSiteConcern(SiteConcern['SupervisorSiteConcern.Config']):
667
+ # @dc.dataclass(frozen=True)
668
+ # class Config(DeployConcern.Config):
669
+ # global_conf_file: str = '/etc/supervisor/conf.d/supervisord.conf'
670
+ #
671
+ # def run(self) -> None:
672
+ # sup_conf_dir = os.path.join(self._d.home_dir(), 'conf/supervisor')
673
+ # with open(self._d.host_cfg.global_supervisor_conf_file_path) as f:
674
+ # glo_sup_conf = f.read()
675
+ # if sup_conf_dir not in glo_sup_conf:
676
+ # log.info('Updating global supervisor conf at %s', self._d.host_cfg.global_supervisor_conf_file_path) # noqa
677
+ # glo_sup_conf += textwrap.dedent(f"""
678
+ # [include]
679
+ # files = {self._d.home_dir()}/conf/supervisor/*.conf
680
+ # """)
681
+ # with open(self._d.host_cfg.global_supervisor_conf_file_path, 'w') as f:
682
+ # f.write(glo_sup_conf)
683
+
684
+
685
+ class SupervisorDeployConcern(DeployConcern['SupervisorDeployConcern.Config']):
686
+ @dc.dataclass(frozen=True)
687
+ class Config(DeployConcernConfig):
688
+ entrypoint: str
689
+
690
+ @cached_nullary
691
+ def conf_file(self) -> str:
692
+ return os.path.join(self._deploy.site.config.root_dir, 'conf', 'supervisor', self._deploy.config.name + '.conf')
693
+
694
+ @cached_nullary
695
+ def fs_items(self) -> ta.Sequence[FsItem]:
696
+ return [FsFile(self.conf_file())]
697
+
698
+ def run(self, runtime: Runtime) -> None:
699
+ runtime.make_dirs(os.path.dirname(self.conf_file()))
700
+
701
+ rd = self._deploy.concern(RepoDeployConcern).repo_dir()
702
+ vx = self._deploy.concern(VenvDeployConcern).exe()
703
+
704
+ conf = textwrap.dedent(f"""
705
+ [program:{self._deploy.config.name}]
706
+ command={vx} -m {self._config.entrypoint}
707
+ directory={rd}
708
+ user={self._deploy.site.config.user}
709
+ autostart=true
710
+ autorestart=true
711
+ """)
712
+
713
+ runtime.write_file(self.conf_file(), conf)
714
+
715
+
716
+ ########################################
717
+ # main.py
718
+
719
+
720
+ def _main() -> None:
721
+ pass
722
+
723
+
724
+ if __name__ == '__main__':
725
+ _main()