ominfra 0.0.0.dev190__py3-none-any.whl → 0.0.0.dev191__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import dataclasses as dc
3
+ import json
3
4
  import os.path
4
5
  import typing as ta
5
6
 
@@ -13,6 +14,7 @@ from .git import DeployGitManager
13
14
  from .paths.owners import DeployPathOwner
14
15
  from .paths.paths import DeployPath
15
16
  from .specs import DeployAppSpec
17
+ from .tags import DeployAppRev
16
18
  from .tags import DeployTagMap
17
19
  from .types import DeployHome
18
20
  from .venvs import DeployVenvManager
@@ -57,9 +59,21 @@ class DeployAppManager(DeployPathOwner):
57
59
 
58
60
  #
59
61
 
62
+ def _make_tags(self, spec: DeployAppSpec) -> DeployTagMap:
63
+ return DeployTagMap(
64
+ spec.app,
65
+ spec.key(),
66
+ DeployAppRev(spec.git.rev),
67
+ )
68
+
69
+ #
70
+
60
71
  @dc.dataclass(frozen=True)
61
72
  class PreparedApp:
62
- app_dir: str
73
+ spec: DeployAppSpec
74
+ tags: DeployTagMap
75
+
76
+ dir: str
63
77
 
64
78
  git_dir: ta.Optional[str] = None
65
79
  venv_dir: ta.Optional[str] = None
@@ -75,16 +89,23 @@ class DeployAppManager(DeployPathOwner):
75
89
 
76
90
  #
77
91
 
92
+ app_tags = tags.add(*self._make_tags(spec))
93
+
94
+ #
95
+
78
96
  check.non_empty_str(home)
79
97
 
80
- app_dir = os.path.join(home, self.APP_DIR.render(tags))
98
+ app_dir = os.path.join(home, self.APP_DIR.render(app_tags))
81
99
 
82
100
  os.makedirs(app_dir, exist_ok=True)
83
101
 
84
102
  #
85
103
 
86
104
  rkw: ta.Dict[str, ta.Any] = dict(
87
- app_dir=app_dir,
105
+ spec=spec,
106
+ tags=app_tags,
107
+
108
+ dir=app_dir,
88
109
  )
89
110
 
90
111
  #
@@ -127,3 +148,51 @@ class DeployAppManager(DeployPathOwner):
127
148
  #
128
149
 
129
150
  return DeployAppManager.PreparedApp(**rkw)
151
+
152
+ async def prepare_app_link(
153
+ self,
154
+ tags: DeployTagMap,
155
+ app_dir: str,
156
+ ) -> PreparedApp:
157
+ spec_file = os.path.join(app_dir, 'spec.json')
158
+ with open(spec_file) as f: # noqa
159
+ spec_json = f.read()
160
+
161
+ spec: DeployAppSpec = self._msh.unmarshal_obj(json.loads(spec_json), DeployAppSpec)
162
+
163
+ #
164
+
165
+ app_tags = tags.add(*self._make_tags(spec))
166
+
167
+ #
168
+
169
+ rkw: ta.Dict[str, ta.Any] = dict(
170
+ spec=spec,
171
+ tags=app_tags,
172
+
173
+ dir=app_dir,
174
+ )
175
+
176
+ #
177
+
178
+ git_dir = os.path.join(app_dir, 'git')
179
+ check.state(os.path.isdir(git_dir))
180
+ rkw.update(git_dir=git_dir)
181
+
182
+ #
183
+
184
+ if spec.venv is not None:
185
+ venv_dir = os.path.join(app_dir, 'venv')
186
+ check.state(os.path.isdir(venv_dir))
187
+ rkw.update(venv_dir=venv_dir)
188
+
189
+ #
190
+
191
+ if spec.conf is not None:
192
+ conf_dir = os.path.join(app_dir, 'conf')
193
+ check.state(os.path.isdir(conf_dir))
194
+ rkw.update(conf_dir=conf_dir)
195
+
196
+ #
197
+
198
+ return DeployAppManager.PreparedApp(**rkw)
@@ -16,6 +16,8 @@ TODO:
16
16
  - some things (venvs) cannot be moved, thus the /deploy/venvs dir
17
17
  - ** ensure (enforce) equivalent relpath nesting
18
18
  """
19
+ import collections.abc
20
+ import functools
19
21
  import os.path
20
22
  import typing as ta
21
23
 
@@ -43,20 +45,66 @@ from .specs import NginxDeployAppConfContent
43
45
  from .specs import RawDeployAppConfContent
44
46
 
45
47
 
48
+ T = ta.TypeVar('T')
49
+
50
+
51
+ ##
52
+
53
+
46
54
  class DeployConfManager:
47
- def _render_app_conf_content(self, ac: DeployAppConfContent) -> str:
55
+ def _process_conf_content(
56
+ self,
57
+ content: T,
58
+ *,
59
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
60
+ ) -> T:
61
+ def rec(o):
62
+ if isinstance(o, str):
63
+ if str_processor is not None:
64
+ return type(o)(str_processor(o))
65
+
66
+ elif isinstance(o, collections.abc.Mapping):
67
+ return type(o)([ # type: ignore
68
+ (rec(k), rec(v))
69
+ for k, v in o.items()
70
+ ])
71
+
72
+ elif isinstance(o, collections.abc.Iterable):
73
+ return type(o)([ # type: ignore
74
+ rec(e) for e in o
75
+ ])
76
+
77
+ return o
78
+
79
+ return rec(content)
80
+
81
+ #
82
+
83
+ def _render_app_conf_content(
84
+ self,
85
+ ac: DeployAppConfContent,
86
+ *,
87
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
88
+ ) -> str:
89
+ pcc = functools.partial(
90
+ self._process_conf_content,
91
+ str_processor=str_processor,
92
+ )
93
+
48
94
  if isinstance(ac, RawDeployAppConfContent):
49
- return ac.body
95
+ return pcc(ac.body)
50
96
 
51
97
  elif isinstance(ac, JsonDeployAppConfContent):
52
- return strip_with_newline(json_dumps_pretty(ac.obj))
98
+ json_obj = pcc(ac.obj)
99
+ return strip_with_newline(json_dumps_pretty(json_obj))
53
100
 
54
101
  elif isinstance(ac, IniDeployAppConfContent):
55
- return strip_with_newline(render_ini_config(ac.sections))
102
+ ini_sections = pcc(ac.sections)
103
+ return strip_with_newline(render_ini_config(ini_sections))
56
104
 
57
105
  elif isinstance(ac, NginxDeployAppConfContent):
58
- ni = NginxConfigItems.of(ac.items)
59
- return strip_with_newline(render_nginx_config_str(ni))
106
+ nginx_items = NginxConfigItems.of(pcc(ac.items))
107
+ return strip_with_newline(render_nginx_config_str(nginx_items))
60
108
 
61
109
  else:
62
110
  raise TypeError(ac)
@@ -65,11 +113,16 @@ class DeployConfManager:
65
113
  self,
66
114
  acf: DeployAppConfFile,
67
115
  app_conf_dir: str,
116
+ *,
117
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
68
118
  ) -> None:
69
119
  conf_file = os.path.join(app_conf_dir, acf.path)
70
120
  check.arg(is_path_in_dir(app_conf_dir, conf_file))
71
121
 
72
- body = self._render_app_conf_content(acf.content)
122
+ body = self._render_app_conf_content(
123
+ acf.content,
124
+ str_processor=str_processor,
125
+ )
73
126
 
74
127
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
75
128
 
@@ -81,10 +134,14 @@ class DeployConfManager:
81
134
  spec: DeployAppConfSpec,
82
135
  app_conf_dir: str,
83
136
  ) -> None:
137
+ def process_str(s: str) -> str:
138
+ return s
139
+
84
140
  for acf in spec.files or []:
85
141
  await self._write_app_conf_file(
86
142
  acf,
87
143
  app_conf_dir,
144
+ str_processor=process_str,
88
145
  )
89
146
 
90
147
  #
@@ -9,6 +9,7 @@ from omlish.lite.json import json_dumps_pretty
9
9
  from omlish.lite.marshal import ObjMarshalerManager
10
10
  from omlish.lite.typing import Func0
11
11
  from omlish.lite.typing import Func1
12
+ from omlish.os.paths import abs_real_path
12
13
  from omlish.os.paths import relative_symlink
13
14
 
14
15
  from .apps import DeployAppManager
@@ -19,9 +20,10 @@ from .paths.paths import DeployPath
19
20
  from .specs import DeployAppSpec
20
21
  from .specs import DeploySpec
21
22
  from .systemd import DeploySystemdManager
22
- from .tags import DeployAppRev
23
+ from .tags import DeployApp
23
24
  from .tags import DeployTagMap
24
25
  from .tags import DeployTime
26
+ from .tmp import DeployHomeAtomics
25
27
  from .types import DeployHome
26
28
 
27
29
 
@@ -38,16 +40,24 @@ class DeployManager(DeployPathOwner):
38
40
  def __init__(
39
41
  self,
40
42
  *,
43
+ atomics: DeployHomeAtomics,
44
+
41
45
  utc_clock: ta.Optional[DeployManagerUtcClock] = None,
42
46
  ):
43
47
  super().__init__()
44
48
 
49
+ self._atomics = atomics
50
+
45
51
  self._utc_clock = utc_clock
46
52
 
47
53
  #
48
54
 
55
+ # Home current link just points to CURRENT_DEPLOY_LINK, and is intended for user convenience.
56
+ HOME_CURRENT_LINK = DeployPath.parse('current')
57
+
49
58
  DEPLOYS_DIR = DeployPath.parse('deploys/')
50
59
 
60
+ # Authoritative current symlink is not in deploy-home, just to prevent accidental corruption.
51
61
  CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
52
62
  DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
53
63
 
@@ -80,6 +90,11 @@ class DeployManager(DeployPathOwner):
80
90
 
81
91
  #
82
92
 
93
+ def render_path(self, home: DeployHome, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
94
+ return os.path.join(check.non_empty_str(home), pth.render(tags))
95
+
96
+ #
97
+
83
98
  def _utc_now(self) -> datetime.datetime:
84
99
  if self._utc_clock is not None:
85
100
  return self._utc_clock() # noqa
@@ -89,6 +104,22 @@ class DeployManager(DeployPathOwner):
89
104
  def make_deploy_time(self) -> DeployTime:
90
105
  return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
91
106
 
107
+ #
108
+
109
+ def make_home_current_link(self, home: DeployHome) -> None:
110
+ home_current_link = os.path.join(check.non_empty_str(home), self.HOME_CURRENT_LINK.render())
111
+ current_deploy_link = os.path.join(check.non_empty_str(home), self.CURRENT_DEPLOY_LINK.render())
112
+ with self._atomics(home).begin_atomic_path_swap( # noqa
113
+ 'file',
114
+ home_current_link,
115
+ auto_commit=True,
116
+ ) as dst_swap:
117
+ os.unlink(dst_swap.tmp_path)
118
+ os.symlink(
119
+ os.path.relpath(current_deploy_link, os.path.dirname(dst_swap.dst_path)),
120
+ dst_swap.tmp_path,
121
+ )
122
+
92
123
 
93
124
  ##
94
125
 
@@ -130,18 +161,18 @@ class DeployDriver:
130
161
  #
131
162
 
132
163
  @property
133
- def deploy_tags(self) -> DeployTagMap:
164
+ def tags(self) -> DeployTagMap:
134
165
  return DeployTagMap(
135
166
  self._time,
136
167
  self._spec.key(),
137
168
  )
138
169
 
139
- def render_deploy_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
140
- return os.path.join(self._home, pth.render(tags if tags is not None else self.deploy_tags))
170
+ def render_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
171
+ return os.path.join(self._home, pth.render(tags if tags is not None else self.tags))
141
172
 
142
173
  @property
143
- def deploy_dir(self) -> str:
144
- return self.render_deploy_path(self._deploys.DEPLOY_DIR)
174
+ def dir(self) -> str:
175
+ return self.render_path(self._deploys.DEPLOY_DIR)
145
176
 
146
177
  #
147
178
 
@@ -150,25 +181,36 @@ class DeployDriver:
150
181
 
151
182
  #
152
183
 
184
+ das: ta.Set[DeployApp] = {a.app for a in self._spec.apps}
185
+ las: ta.Set[DeployApp] = set(self._spec.app_links.apps)
186
+ if (ras := das & las):
187
+ raise RuntimeError(f'Must not specify apps as both deploy and link: {sorted(a.s for a in ras)}')
188
+
189
+ #
190
+
153
191
  self._paths.validate_deploy_paths()
154
192
 
155
193
  #
156
194
 
157
- os.makedirs(self.deploy_dir)
195
+ os.makedirs(self.dir)
158
196
 
159
197
  #
160
198
 
161
- spec_file = self.render_deploy_path(self._deploys.DEPLOY_SPEC_FILE)
199
+ spec_file = self.render_path(self._deploys.DEPLOY_SPEC_FILE)
162
200
  with open(spec_file, 'w') as f: # noqa
163
201
  f.write(spec_json)
164
202
 
165
203
  #
166
204
 
167
- deploying_link = self.render_deploy_path(self._deploys.DEPLOYING_DEPLOY_LINK)
205
+ deploying_link = self.render_path(self._deploys.DEPLOYING_DEPLOY_LINK)
206
+ current_link = self.render_path(self._deploys.CURRENT_DEPLOY_LINK)
207
+
208
+ #
209
+
168
210
  if os.path.exists(deploying_link):
169
211
  os.unlink(deploying_link)
170
212
  relative_symlink(
171
- self.deploy_dir,
213
+ self.dir,
172
214
  deploying_link,
173
215
  target_is_directory=True,
174
216
  make_dirs=True,
@@ -180,16 +222,30 @@ class DeployDriver:
180
222
  self._deploys.APPS_DEPLOY_DIR,
181
223
  self._deploys.CONFS_DEPLOY_DIR,
182
224
  ]:
183
- os.makedirs(self.render_deploy_path(md))
225
+ os.makedirs(self.render_path(md))
184
226
 
185
227
  #
186
228
 
229
+ if not self._spec.app_links.exclude_unspecified:
230
+ cad = abs_real_path(os.path.join(current_link, 'apps'))
231
+ if os.path.exists(cad):
232
+ for d in os.listdir(cad):
233
+ if (da := DeployApp(d)) not in das:
234
+ las.add(da)
235
+
236
+ for la in self._spec.app_links.apps:
237
+ await self._drive_app_link(
238
+ la,
239
+ current_link,
240
+ )
241
+
187
242
  for app in self._spec.apps:
188
- await self.drive_app_deploy(app)
243
+ await self._drive_app_deploy(
244
+ app,
245
+ )
189
246
 
190
247
  #
191
248
 
192
- current_link = self.render_deploy_path(self._deploys.CURRENT_DEPLOY_LINK)
193
249
  os.replace(deploying_link, current_link)
194
250
 
195
251
  #
@@ -197,31 +253,27 @@ class DeployDriver:
197
253
  await self._systemd.sync_systemd(
198
254
  self._spec.systemd,
199
255
  self._home,
200
- os.path.join(self.deploy_dir, 'conf', 'systemd'), # FIXME
256
+ os.path.join(self.dir, 'conf', 'systemd'), # FIXME
201
257
  )
202
258
 
203
- #
259
+ #
204
260
 
205
- async def drive_app_deploy(self, app: DeployAppSpec) -> None:
206
- app_tags = self.deploy_tags.add(
207
- app.app,
208
- app.key(),
209
- DeployAppRev(app.git.rev),
210
- )
261
+ self._deploys.make_home_current_link(self._home)
211
262
 
212
- #
263
+ #
213
264
 
214
- da = await self._apps.prepare_app(
265
+ async def _drive_app_deploy(self, app: DeployAppSpec) -> None:
266
+ pa = await self._apps.prepare_app(
215
267
  app,
216
268
  self._home,
217
- app_tags,
269
+ self.tags,
218
270
  )
219
271
 
220
272
  #
221
273
 
222
- app_link = self.render_deploy_path(self._deploys.APP_DEPLOY_LINK, app_tags)
274
+ app_link = self.render_path(self._deploys.APP_DEPLOY_LINK, pa.tags)
223
275
  relative_symlink(
224
- da.app_dir,
276
+ pa.dir,
225
277
  app_link,
226
278
  target_is_directory=True,
227
279
  make_dirs=True,
@@ -229,11 +281,47 @@ class DeployDriver:
229
281
 
230
282
  #
231
283
 
232
- deploy_conf_dir = self.render_deploy_path(self._deploys.CONFS_DEPLOY_DIR)
233
- if app.conf is not None:
284
+ await self._drive_app_configure(pa)
285
+
286
+ async def _drive_app_link(
287
+ self,
288
+ app: DeployApp,
289
+ current_link: str,
290
+ ) -> None:
291
+ app_link = os.path.join(abs_real_path(current_link), 'apps', app.s)
292
+ check.state(os.path.islink(app_link))
293
+
294
+ app_dir = abs_real_path(app_link)
295
+ check.state(os.path.isdir(app_dir))
296
+
297
+ #
298
+
299
+ pa = await self._apps.prepare_app_link(
300
+ self.tags,
301
+ app_dir,
302
+ )
303
+
304
+ #
305
+
306
+ relative_symlink(
307
+ app_dir,
308
+ os.path.join(self.dir, 'apps', app.s),
309
+ target_is_directory=True,
310
+ )
311
+
312
+ #
313
+
314
+ await self._drive_app_configure(pa)
315
+
316
+ async def _drive_app_configure(
317
+ self,
318
+ pa: DeployAppManager.PreparedApp,
319
+ ) -> None:
320
+ deploy_conf_dir = self.render_path(self._deploys.CONFS_DEPLOY_DIR)
321
+ if pa.spec.conf is not None:
234
322
  await self._conf.link_app_conf(
235
- app.conf,
236
- app_tags,
237
- check.non_empty_str(da.conf_dir),
323
+ pa.spec.conf,
324
+ pa.tags,
325
+ check.non_empty_str(pa.conf_dir),
238
326
  deploy_conf_dir,
239
327
  )
@@ -91,6 +91,13 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
91
91
  return DeployAppKey(self._key_str())
92
92
 
93
93
 
94
+ @dc.dataclass(frozen=True)
95
+ class DeployAppLinksSpec:
96
+ apps: ta.Sequence[DeployApp] = ()
97
+
98
+ exclude_unspecified: bool = False
99
+
100
+
94
101
  ##
95
102
 
96
103
 
@@ -109,6 +116,8 @@ class DeploySpec(DeploySpecKeyed[DeployKey]):
109
116
 
110
117
  apps: ta.Sequence[DeployAppSpec] = ()
111
118
 
119
+ app_links: DeployAppLinksSpec = DeployAppLinksSpec()
120
+
112
121
  systemd: ta.Optional[DeploySystemdSpec] = None
113
122
 
114
123
  def __post_init__(self) -> None:
@@ -1,34 +1,17 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  """
3
- ~/.config/systemd/user/
4
-
5
- verify - systemd-analyze
6
-
7
- sudo loginctl enable-linger "$USER"
8
-
9
- cat ~/.config/systemd/user/sleep-infinity.service
10
- [Unit]
11
- Description=User-specific service to run 'sleep infinity'
12
- After=default.target
13
-
14
- [Service]
15
- ExecStart=/bin/sleep infinity
16
- Restart=always
17
- RestartSec=5
18
-
19
- [Install]
20
- WantedBy=default.target
21
-
22
- systemctl --user daemon-reload
23
-
24
- systemctl --user enable sleep-infinity.service
25
- systemctl --user start sleep-infinity.service
26
-
27
- systemctl --user status sleep-infinity.service
3
+ TODO:
4
+ - verify - systemd-analyze
5
+ - sudo loginctl enable-linger "$USER"
6
+ - idemp kill services that shouldn't be running, start ones that should
7
+ - ideally only those defined by links to deploy home
8
+ - ominfra.systemd / x.sd_orphans
28
9
  """
29
10
  import os.path
11
+ import sys
30
12
  import typing as ta
31
13
 
14
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
32
15
  from omlish.lite.check import check
33
16
  from omlish.os.paths import abs_real_path
34
17
  from omlish.os.paths import is_path_in_dir
@@ -73,6 +56,8 @@ class DeploySystemdManager:
73
56
  if not spec:
74
57
  return
75
58
 
59
+ #
60
+
76
61
  if not (ud := spec.unit_dir):
77
62
  return
78
63
 
@@ -80,6 +65,8 @@ class DeploySystemdManager:
80
65
 
81
66
  os.makedirs(ud, exist_ok=True)
82
67
 
68
+ #
69
+
83
70
  uld = {
84
71
  n: p
85
72
  for n, p in self._scan_link_dir(ud).items()
@@ -91,8 +78,11 @@ class DeploySystemdManager:
91
78
  else:
92
79
  cld = {}
93
80
 
94
- for n in sorted(set(uld) | set(cld)):
95
- ul = uld.get(n) # noqa
81
+ #
82
+
83
+ ns = sorted(set(uld) | set(cld))
84
+
85
+ for n in ns:
96
86
  cl = cld.get(n)
97
87
  if cl is None:
98
88
  os.unlink(os.path.join(ud, n))
@@ -108,3 +98,34 @@ class DeploySystemdManager:
108
98
  os.path.relpath(cl, os.path.dirname(dst_swap.dst_path)),
109
99
  dst_swap.tmp_path,
110
100
  )
101
+
102
+ #
103
+
104
+ if sys.platform == 'linux':
105
+ async def reload() -> None:
106
+ await asyncio_subprocesses.check_call('systemctl', '--user', 'daemon-reload')
107
+
108
+ await reload()
109
+
110
+ num_deleted = 0
111
+ for n in ns:
112
+ if n.endswith('.service'):
113
+ cl = cld.get(n)
114
+ ul = uld.get(n)
115
+ if cl is not None:
116
+ if ul is None:
117
+ cs = ['enable', 'start']
118
+ else:
119
+ cs = ['restart']
120
+ else: # noqa
121
+ if ul is not None:
122
+ cs = ['stop']
123
+ num_deleted += 1
124
+ else:
125
+ cs = []
126
+
127
+ for c in cs:
128
+ await asyncio_subprocesses.check_call('systemctl', '--user', c, n)
129
+
130
+ if num_deleted:
131
+ await reload()
@@ -29,7 +29,7 @@ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
29
29
  ##
30
30
 
31
31
 
32
- @dc.dataclass(frozen=True)
32
+ @dc.dataclass(frozen=True, order=True)
33
33
  class DeployTag(abc.ABC): # noqa
34
34
  s: str
35
35
 
@@ -5,6 +5,7 @@ TODO:
5
5
  - share more code with pyproject?
6
6
  """
7
7
  import os.path
8
+ import shutil
8
9
 
9
10
  from omdev.interp.default import get_default_interp_resolver
10
11
  from omdev.interp.types import InterpSpecifier
@@ -44,9 +45,12 @@ class DeployVenvManager:
44
45
 
45
46
  if os.path.isfile(reqs_txt):
46
47
  if spec.use_uv:
47
- await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
48
- pip_cmd = ['-m', 'uv', 'pip']
48
+ if shutil.which('uv') is not None:
49
+ pip_cmd = ['uv', 'pip']
50
+ else:
51
+ await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
52
+ pip_cmd = [venv_exe, '-m', 'uv', 'pip']
49
53
  else:
50
- pip_cmd = ['-m', 'pip']
54
+ pip_cmd = [venv_exe, '-m', 'pip']
51
55
 
52
- await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
56
+ await asyncio_subprocesses.check_call(*pip_cmd, 'install', '-r', reqs_txt, cwd=venv_dir)