ominfra 0.0.0.dev190__py3-none-any.whl → 0.0.0.dev191__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.
@@ -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)