comparison libervia/backend/plugins/plugin_misc_app_manager/__init__.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 4aa62767f501
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
43 43
44 try: 44 try:
45 import yaml 45 import yaml
46 except ImportError: 46 except ImportError:
47 raise exceptions.MissingModule( 47 raise exceptions.MissingModule(
48 'Missing module PyYAML, please download/install it. You can use ' 48 "Missing module PyYAML, please download/install it. You can use "
49 '"pip install pyyaml"' 49 '"pip install pyyaml"'
50 ) 50 )
51 51
52 try: 52 try:
53 from yaml import CLoader as Loader, CDumper as Dumper 53 from yaml import CLoader as Loader, CDumper as Dumper
55 log.warning( 55 log.warning(
56 "Can't use LibYAML binding (is libyaml installed?), pure Python version will be " 56 "Can't use LibYAML binding (is libyaml installed?), pure Python version will be "
57 "used, but it is slower" 57 "used, but it is slower"
58 ) 58 )
59 from yaml import Loader, Dumper 59 from yaml import Loader, Dumper
60
61
62 60
63 61
64 PLUGIN_INFO = { 62 PLUGIN_INFO = {
65 C.PI_NAME: "Applications Manager", 63 C.PI_NAME: "Applications Manager",
66 C.PI_IMPORT_NAME: "APP_MANAGER", 64 C.PI_IMPORT_NAME: "APP_MANAGER",
71 C.PI_DESCRIPTION: _( 69 C.PI_DESCRIPTION: _(
72 """Applications Manager 70 """Applications Manager
73 71
74 Manage external applications using packagers, OS virtualization/containers or other 72 Manage external applications using packagers, OS virtualization/containers or other
75 software management tools. 73 software management tools.
76 """), 74 """
75 ),
77 } 76 }
78 77
79 APP_FILE_PREFIX = "libervia_app_" 78 APP_FILE_PREFIX = "libervia_app_"
80 79
81 80
90 self._apps = {} 89 self._apps = {}
91 self._started = {} 90 self._started = {}
92 # instance id to app data map 91 # instance id to app data map
93 self._instances = {} 92 self._instances = {}
94 93
95
96 self.persistent_data = persistent.LazyPersistentBinaryDict("app_manager") 94 self.persistent_data = persistent.LazyPersistentBinaryDict("app_manager")
97 95
98 host.bridge.add_method( 96 host.bridge.add_method(
99 "applications_list", 97 "applications_list",
100 ".plugin", 98 ".plugin",
126 method=self._get_exposed, 124 method=self._get_exposed,
127 async_=True, 125 async_=True,
128 ) 126 )
129 # application has been started succeesfully, 127 # application has been started succeesfully,
130 # args: name, instance_id, extra 128 # args: name, instance_id, extra
131 host.bridge.add_signal( 129 host.bridge.add_signal("application_started", ".plugin", signature="sss")
132 "application_started", ".plugin", signature="sss"
133 )
134 # application went wrong with the application 130 # application went wrong with the application
135 # args: name, instance_id, extra 131 # args: name, instance_id, extra
136 host.bridge.add_signal( 132 host.bridge.add_signal("application_error", ".plugin", signature="sss")
137 "application_error", ".plugin", signature="sss" 133 yaml.add_constructor("!libervia_conf", self._libervia_conf_constr, Loader=Loader)
138 )
139 yaml.add_constructor( 134 yaml.add_constructor(
140 "!libervia_conf", self._libervia_conf_constr, Loader=Loader) 135 "!libervia_generate_pwd", self._libervia_generate_pwd_constr, Loader=Loader
136 )
141 yaml.add_constructor( 137 yaml.add_constructor(
142 "!libervia_generate_pwd", self._libervia_generate_pwd_constr, Loader=Loader) 138 "!libervia_param", self._libervia_param_constr, Loader=Loader
143 yaml.add_constructor( 139 )
144 "!libervia_param", self._libervia_param_constr, Loader=Loader)
145 140
146 def unload(self): 141 def unload(self):
147 log.debug("unloading applications manager") 142 log.debug("unloading applications manager")
148 for instances in self._started.values(): 143 for instances in self._started.values():
149 for instance in instances: 144 for instance in instances:
150 data = instance['data'] 145 data = instance["data"]
151 if not data['single_instance']: 146 if not data["single_instance"]:
152 log.debug( 147 log.debug(
153 f"cleaning temporary directory at {data['_instance_dir_path']}") 148 f"cleaning temporary directory at {data['_instance_dir_path']}"
154 data['_instance_dir_obj'].cleanup() 149 )
150 data["_instance_dir_obj"].cleanup()
155 151
156 def _libervia_conf_constr(self, loader, node) -> str: 152 def _libervia_conf_constr(self, loader, node) -> str:
157 """Get a value from Libervia configuration 153 """Get a value from Libervia configuration
158 154
159 A list is expected with either "name" of a config parameter, a one or more of 155 A list is expected with either "name" of a config parameter, a one or more of
181 "is expected" 177 "is expected"
182 ) 178 )
183 179
184 value = self.host.memory.config_get(section, name, default) 180 value = self.host.memory.config_get(section, name, default)
185 # FIXME: "public_url" is used only here and doesn't take multi-sites into account 181 # FIXME: "public_url" is used only here and doesn't take multi-sites into account
186 if name == "public_url" and (not value or value.startswith('http')): 182 if name == "public_url" and (not value or value.startswith("http")):
187 if not value: 183 if not value:
188 log.warning(_( 184 log.warning(
189 'No value found for "public_url", using "example.org" for ' 185 _(
190 'now, please set the proper value in libervia.conf')) 186 'No value found for "public_url", using "example.org" for '
187 "now, please set the proper value in libervia.conf"
188 )
189 )
191 else: 190 else:
192 log.warning(_( 191 log.warning(
193 'invalid value for "public_url" ({value}), it musts not start with ' 192 _(
194 'schema ("http"), ignoring it and using "example.org" ' 193 'invalid value for "public_url" ({value}), it musts not start with '
195 'instead') 194 'schema ("http"), ignoring it and using "example.org" '
196 .format(value=value)) 195 "instead"
196 ).format(value=value)
197 )
197 value = "example.org" 198 value = "example.org"
198 199
199 if filter_ is None: 200 if filter_ is None:
200 pass 201 pass
201 elif filter_ == 'first': 202 elif filter_ == "first":
202 value = value[0] 203 value = value[0]
203 elif filter_ == "not": 204 elif filter_ == "not":
204 value = C.bool(value) 205 value = C.bool(value)
205 value = C.bool_const(not value).lower() 206 value = C.bool_const(not value).lower()
206 else: 207 else:
230 try: 231 try:
231 key = self._app_persistent_data[pwd_data_key] 232 key = self._app_persistent_data[pwd_data_key]
232 except KeyError: 233 except KeyError:
233 alphabet = string.ascii_letters + string.digits 234 alphabet = string.ascii_letters + string.digits
234 key_size = int(kwargs.get("size", 30)) 235 key_size = int(kwargs.get("size", 30))
235 key = ''.join(secrets.choice(alphabet) for __ in range(key_size)) 236 key = "".join(secrets.choice(alphabet) for __ in range(key_size))
236 self._app_persistent_data[pwd_data_key] = key 237 self._app_persistent_data[pwd_data_key] = key
237 else: 238 else:
238 log.debug(f"Re-using existing key for {name!r} password.") 239 log.debug(f"Re-using existing key for {name!r} password.")
239 return key 240 return key
240 241
253 254
254 def register(self, manager): 255 def register(self, manager):
255 name = manager.name 256 name = manager.name
256 if name in self._managers: 257 if name in self._managers:
257 raise exceptions.ConflictError( 258 raise exceptions.ConflictError(
258 f"There is already a manager with the name {name}") 259 f"There is already a manager with the name {name}"
260 )
259 self._managers[manager.name] = manager 261 self._managers[manager.name] = manager
260 if manager.discover_path is not None: 262 if manager.discover_path is not None:
261 self.discover(manager.discover_path, manager) 263 self.discover(manager.discover_path, manager)
262 264
263 def get_manager(self, app_data: dict) -> AppManagerBackend: 265 def get_manager(self, app_data: dict) -> AppManagerBackend:
267 @raise exceptions.NotFound: manager is not registered 269 @raise exceptions.NotFound: manager is not registered
268 """ 270 """
269 try: 271 try:
270 app_type = app_data["type"] 272 app_type = app_data["type"]
271 except KeyError: 273 except KeyError:
272 raise exceptions.DataError( 274 raise exceptions.DataError('app file doesn\'t have the mandatory "type" key')
273 "app file doesn't have the mandatory \"type\" key"
274 )
275 if not isinstance(app_type, str): 275 if not isinstance(app_type, str):
276 raise exceptions.DataError( 276 raise exceptions.DataError(f"invalid app data type: {app_type!r}")
277 f"invalid app data type: {app_type!r}"
278 )
279 app_type = app_type.strip() 277 app_type = app_type.strip()
280 try: 278 try:
281 return self._managers[app_type] 279 return self._managers[app_type]
282 except KeyError: 280 except KeyError:
283 raise exceptions.NotFound( 281 raise exceptions.NotFound(
284 f"No manager found to manage app of type {app_type!r}") 282 f"No manager found to manage app of type {app_type!r}"
285 283 )
286 def get_app_data( 284
287 self, 285 def get_app_data(self, id_type: Optional[str], identifier: str) -> dict:
288 id_type: Optional[str],
289 identifier: str
290 ) -> dict:
291 """Retrieve instance's app_data from identifier 286 """Retrieve instance's app_data from identifier
292 287
293 @param id_type: type of the identifier, can be: 288 @param id_type: type of the identifier, can be:
294 - "name": identifier is a canonical application name 289 - "name": identifier is a canonical application name
295 the first found instance of this application is returned 290 the first found instance of this application is returned
298 @return: instance application data 293 @return: instance application data
299 @raise exceptions.NotFound: no instance with this id can be found 294 @raise exceptions.NotFound: no instance with this id can be found
300 @raise ValueError: id_type is invalid 295 @raise ValueError: id_type is invalid
301 """ 296 """
302 if not id_type: 297 if not id_type:
303 id_type = 'name' 298 id_type = "name"
304 if id_type == 'name': 299 if id_type == "name":
305 identifier = identifier.lower().strip() 300 identifier = identifier.lower().strip()
306 try: 301 try:
307 return next(iter(self._started[identifier])) 302 return next(iter(self._started[identifier]))
308 except (KeyError, StopIteration): 303 except (KeyError, StopIteration):
309 raise exceptions.NotFound( 304 raise exceptions.NotFound(
310 f"No instance of {identifier!r} is currently running" 305 f"No instance of {identifier!r} is currently running"
311 ) 306 )
312 elif id_type == 'instance': 307 elif id_type == "instance":
313 instance_id = identifier 308 instance_id = identifier
314 try: 309 try:
315 return self._instances[instance_id] 310 return self._instances[instance_id]
316 except KeyError: 311 except KeyError:
317 raise exceptions.NotFound( 312 raise exceptions.NotFound(
318 f"There is no application instance running with id {instance_id!r}" 313 f"There is no application instance running with id {instance_id!r}"
319 ) 314 )
320 else: 315 else:
321 raise ValueError(f"invalid id_type: {id_type!r}") 316 raise ValueError(f"invalid id_type: {id_type!r}")
322 317
323 def discover( 318 def discover(self, dir_path: Path, manager: AppManagerBackend | None = None) -> None:
324 self,
325 dir_path: Path,
326 manager: AppManagerBackend|None = None
327 ) -> None:
328 """Search for app configuration file. 319 """Search for app configuration file.
329 320
330 App configuration files must start with [APP_FILE_PREFIX] and have a ``.yaml`` 321 App configuration files must start with [APP_FILE_PREFIX] and have a ``.yaml``
331 extension. 322 extension.
332 """ 323 """
334 if manager is None: 325 if manager is None:
335 try: 326 try:
336 app_data = self.parse(file_path) 327 app_data = self.parse(file_path)
337 manager = self.get_manager(app_data) 328 manager = self.get_manager(app_data)
338 except (exceptions.DataError, exceptions.NotFound) as e: 329 except (exceptions.DataError, exceptions.NotFound) as e:
339 log.warning( 330 log.warning(f"Can't parse {file_path}, skipping: {e}")
340 f"Can't parse {file_path}, skipping: {e}")
341 continue 331 continue
342 app_name = file_path.stem[len(APP_FILE_PREFIX):].strip().lower() 332 app_name = file_path.stem[len(APP_FILE_PREFIX) :].strip().lower()
343 if not app_name: 333 if not app_name:
344 log.warning(f"invalid app file name at {file_path}") 334 log.warning(f"invalid app file name at {file_path}")
345 continue 335 continue
346 app_dict = self._apps.setdefault(app_name, {}) 336 app_dict = self._apps.setdefault(app_name, {})
347 manager_set = app_dict.setdefault(manager, set()) 337 manager_set = app_dict.setdefault(manager, set())
348 manager_set.add(file_path) 338 manager_set.add(file_path)
349 log.debug( 339 log.debug(f"{app_name!r} {manager.name} application found")
350 f"{app_name!r} {manager.name} application found"
351 )
352 340
353 def parse(self, file_path: Path, params: Optional[dict] = None) -> dict: 341 def parse(self, file_path: Path, params: Optional[dict] = None) -> dict:
354 """Parse Libervia application file 342 """Parse Libervia application file
355 343
356 @param params: parameters for running this instance 344 @param params: parameters for running this instance
365 app_data = self.load(f) 353 app_data = self.load(f)
366 self._params = None 354 self._params = None
367 if "name" not in app_data: 355 if "name" not in app_data:
368 # note that we don't use lower() here as we want human readable name and 356 # note that we don't use lower() here as we want human readable name and
369 # uppercase may be set on purpose 357 # uppercase may be set on purpose
370 app_data['name'] = file_path.stem[len(APP_FILE_PREFIX):].strip() 358 app_data["name"] = file_path.stem[len(APP_FILE_PREFIX) :].strip()
371 single_instance = app_data.setdefault("single_instance", True) 359 single_instance = app_data.setdefault("single_instance", True)
372 if not isinstance(single_instance, bool): 360 if not isinstance(single_instance, bool):
373 raise ValueError( 361 raise ValueError(
374 f'"single_instance" must be a boolean, but it is {type(single_instance)}' 362 f'"single_instance" must be a boolean, but it is {type(single_instance)}'
375 ) 363 )
427 # to be defined in app_data allows explicitly it. 415 # to be defined in app_data allows explicitly it.
428 app_name = app_name.lower().strip() 416 app_name = app_name.lower().strip()
429 try: 417 try:
430 app_file_path = next(iter(next(iter(self._apps[app_name].values())))) 418 app_file_path = next(iter(next(iter(self._apps[app_name].values()))))
431 except KeyError: 419 except KeyError:
432 raise exceptions.NotFound( 420 raise exceptions.NotFound(f"No application found with the name {app_name!r}")
433 f"No application found with the name {app_name!r}"
434 )
435 log.info(f"starting {app_name!r}") 421 log.info(f"starting {app_name!r}")
436 self._app_persistent_data = await self.persistent_data.get(app_name) or {} 422 self._app_persistent_data = await self.persistent_data.get(app_name) or {}
437 self._app_persistent_data["last_started"] = time.time() 423 self._app_persistent_data["last_started"] = time.time()
438 started_data = self._started.setdefault(app_name, []) 424 started_data = self._started.setdefault(app_name, [])
439 app_data = self.parse(app_file_path, extra) 425 app_data = self.parse(app_file_path, extra)
440 await self.persistent_data.aset(app_name, self._app_persistent_data) 426 await self.persistent_data.aset(app_name, self._app_persistent_data)
441 app_data["_started"] = False 427 app_data["_started"] = False
442 app_data['_file_path'] = app_file_path 428 app_data["_file_path"] = app_file_path
443 app_data['_name_canonical'] = app_name 429 app_data["_name_canonical"] = app_name
444 single_instance = app_data['single_instance'] 430 single_instance = app_data["single_instance"]
445 ret_data = { 431 ret_data = {"name": app_name, "started": False}
446 "name": app_name,
447 "started": False
448 }
449 if single_instance: 432 if single_instance:
450 if started_data: 433 if started_data:
451 instance_data = started_data[0] 434 instance_data = started_data[0]
452 instance_id = instance_data["_instance_id"] 435 instance_id = instance_data["_instance_id"]
453 ret_data["instance"] = instance_id 436 ret_data["instance"] = instance_id
460 else: 443 else:
461 cache_path = self.host.memory.get_cache_path( 444 cache_path = self.host.memory.get_cache_path(
462 PLUGIN_INFO[C.PI_IMPORT_NAME], app_name 445 PLUGIN_INFO[C.PI_IMPORT_NAME], app_name
463 ) 446 )
464 cache_path.mkdir(0o700, parents=True, exist_ok=True) 447 cache_path.mkdir(0o700, parents=True, exist_ok=True)
465 app_data['_instance_dir_path'] = cache_path 448 app_data["_instance_dir_path"] = cache_path
466 else: 449 else:
467 dest_dir_obj = tempfile.TemporaryDirectory(prefix="libervia_app_") 450 dest_dir_obj = tempfile.TemporaryDirectory(prefix="libervia_app_")
468 app_data['_instance_dir_obj'] = dest_dir_obj 451 app_data["_instance_dir_obj"] = dest_dir_obj
469 app_data['_instance_dir_path'] = Path(dest_dir_obj.name) 452 app_data["_instance_dir_path"] = Path(dest_dir_obj.name)
470 instance_id = ret_data["instance"] = app_data['_instance_id'] = shortuuid.uuid() 453 instance_id = ret_data["instance"] = app_data["_instance_id"] = shortuuid.uuid()
471 manager = self.get_manager(app_data) 454 manager = self.get_manager(app_data)
472 app_data['_manager'] = manager 455 app_data["_manager"] = manager
473 started_data.append(app_data) 456 started_data.append(app_data)
474 self._instances[instance_id] = app_data 457 self._instances[instance_id] = app_data
475 # we retrieve exposed data such as url_prefix which can be useful computed exposed 458 # we retrieve exposed data such as url_prefix which can be useful computed exposed
476 # data must wait for the app to be started, so we skip them for now 459 # data must wait for the app to be started, so we skip them for now
477 ret_data["expose"] = await self.get_exposed( 460 ret_data["expose"] = await self.get_exposed(
480 463
481 try: 464 try:
482 start = manager.start 465 start = manager.start
483 except AttributeError: 466 except AttributeError:
484 raise exceptions.InternalError( 467 raise exceptions.InternalError(
485 f"{manager.name} doesn't have the mandatory \"start\" method" 468 f'{manager.name} doesn\'t have the mandatory "start" method'
486 ) 469 )
487 else: 470 else:
488 defer.ensureDeferred(self.start_app(start, app_data)) 471 defer.ensureDeferred(self.start_app(start, app_data))
489 return ret_data 472 return ret_data
490 473
496 except Exception as e: 479 except Exception as e:
497 log.exception(f"Can't start libervia app {app_name!r}") 480 log.exception(f"Can't start libervia app {app_name!r}")
498 self.host.bridge.application_error( 481 self.host.bridge.application_error(
499 app_name, 482 app_name,
500 instance_id, 483 instance_id,
501 data_format.serialise( 484 data_format.serialise({"class": str(type(e)), "msg": str(e)}),
502 { 485 )
503 "class": str(type(e)),
504 "msg": str(e)
505 }
506 ))
507 else: 486 else:
508 app_data["_started"] = True 487 app_data["_started"] = True
509 self.host.bridge.application_started(app_name, instance_id, "") 488 self.host.bridge.application_started(app_name, instance_id, "")
510 log.info(f"{app_name!r} started") 489 log.info(f"{app_name!r} started")
511 490
512 def _stop(self, identifier, id_type, extra): 491 def _stop(self, identifier, id_type, extra):
513 extra = data_format.deserialise(extra) 492 extra = data_format.deserialise(extra)
514 return defer.ensureDeferred( 493 return defer.ensureDeferred(
515 self.stop(str(identifier), str(id_type) or None, extra)) 494 self.stop(str(identifier), str(id_type) or None, extra)
495 )
516 496
517 async def stop( 497 async def stop(
518 self, 498 self,
519 identifier: str, 499 identifier: str,
520 id_type: Optional[str] = None, 500 id_type: Optional[str] = None,
525 505
526 app_data = self.get_app_data(id_type, identifier) 506 app_data = self.get_app_data(id_type, identifier)
527 507
528 log.info(f"stopping {app_data['name']!r}") 508 log.info(f"stopping {app_data['name']!r}")
529 509
530 app_name = app_data['_name_canonical'] 510 app_name = app_data["_name_canonical"]
531 instance_id = app_data['_instance_id'] 511 instance_id = app_data["_instance_id"]
532 manager = app_data['_manager'] 512 manager = app_data["_manager"]
533 513
534 try: 514 try:
535 stop = manager.stop 515 stop = manager.stop
536 except AttributeError: 516 except AttributeError:
537 raise exceptions.InternalError( 517 raise exceptions.InternalError(
538 f"{manager.name} doesn't have the mandatory \"stop\" method" 518 f'{manager.name} doesn\'t have the mandatory "stop" method'
539 ) 519 )
540 else: 520 else:
541 try: 521 try:
542 await stop(app_data) 522 await stop(app_data)
543 except Exception as e: 523 except Exception as e:
549 529
550 try: 530 try:
551 del self._instances[instance_id] 531 del self._instances[instance_id]
552 except KeyError: 532 except KeyError:
553 log.error( 533 log.error(
554 f"INTERNAL ERROR: {instance_id!r} is not present in self._instances") 534 f"INTERNAL ERROR: {instance_id!r} is not present in self._instances"
535 )
555 536
556 try: 537 try:
557 self._started[app_name].remove(app_data) 538 self._started[app_name].remove(app_data)
558 except ValueError: 539 except ValueError:
559 log.error( 540 log.error(
588 569
589 The manager's "compute_expose" method will be called if it exists. It can be used 570 The manager's "compute_expose" method will be called if it exists. It can be used
590 to handle manager specific conventions. 571 to handle manager specific conventions.
591 """ 572 """
592 app_data = self.get_app_data(id_type, identifier) 573 app_data = self.get_app_data(id_type, identifier)
593 if app_data.get('_exposed_computed', False): 574 if app_data.get("_exposed_computed", False):
594 return app_data['expose'] 575 return app_data["expose"]
595 if extra is None: 576 if extra is None:
596 extra = {} 577 extra = {}
597 expose = app_data.setdefault("expose", {}) 578 expose = app_data.setdefault("expose", {})
598 if "passwords" in expose: 579 if "passwords" in expose:
599 passwords = expose['passwords'] 580 passwords = expose["passwords"]
600 for name, value in list(passwords.items()): 581 for name, value in list(passwords.items()):
601 if isinstance(value, list): 582 if isinstance(value, list):
602 # if we have a list, is the sequence of keys leading to the value 583 # if we have a list, is the sequence of keys leading to the value
603 # to expose. 584 # to expose.
604 try: 585 try:
605 passwords[name] = self.get_app_data_value(value, app_data) 586 passwords[name] = self.get_app_data_value(value, app_data)
606 except KeyError: 587 except KeyError:
607 log.warning( 588 log.warning(
608 f"Can't retrieve exposed value for password {name!r}: {e}") 589 f"Can't retrieve exposed value for password {name!r}: {e}"
590 )
609 del passwords[name] 591 del passwords[name]
610 592
611 for key in ("url_prefix", "front_url"): 593 for key in ("url_prefix", "front_url"):
612 value = expose.get(key) 594 value = expose.get(key)
613 if isinstance(value, list): 595 if isinstance(value, list):
614 try: 596 try:
615 expose[key] = self.get_app_data_value(value, app_data) 597 expose[key] = self.get_app_data_value(value, app_data)
616 except KeyError: 598 except KeyError:
617 log.warning( 599 log.warning(f"Can't retrieve exposed value for {key!r} at {value}")
618 f"Can't retrieve exposed value for {key!r} at {value}"
619 )
620 del expose[key] 600 del expose[key]
621 601
622 front_url = expose.get("front_url") 602 front_url = expose.get("front_url")
623 if front_url: 603 if front_url:
624 # we want to be sure that a scheme is defined, defaulting to ``https`` 604 # we want to be sure that a scheme is defined, defaulting to ``https``
626 if not parsed_url.scheme: 606 if not parsed_url.scheme:
627 if not parsed_url.netloc: 607 if not parsed_url.netloc:
628 path_elt = parsed_url.path.split("/", 1) 608 path_elt = parsed_url.path.split("/", 1)
629 parsed_url = parsed_url._replace( 609 parsed_url = parsed_url._replace(
630 netloc=path_elt[0], 610 netloc=path_elt[0],
631 path=f"/{path_elt[1]}" if len(path_elt) > 1 else "" 611 path=f"/{path_elt[1]}" if len(path_elt) > 1 else "",
632 ) 612 )
633 parsed_url = parsed_url._replace(scheme='https') 613 parsed_url = parsed_url._replace(scheme="https")
634 expose["front_url"] = urlunparse(parsed_url) 614 expose["front_url"] = urlunparse(parsed_url)
635 615
636 if extra.get("skip_compute", False): 616 if extra.get("skip_compute", False):
637 return expose 617 return expose
638 618
639 try: 619 try:
640 compute_expose = app_data['_manager'].compute_expose 620 compute_expose = app_data["_manager"].compute_expose
641 except AttributeError: 621 except AttributeError:
642 pass 622 pass
643 else: 623 else:
644 await compute_expose(app_data) 624 await compute_expose(app_data)
645 625
646 app_data['_exposed_computed'] = True 626 app_data["_exposed_computed"] = True
647 return expose 627 return expose
648 628
649 async def _do_prepare( 629 async def _do_prepare(
650 self, 630 self,
651 app_data: dict, 631 app_data: dict,
652 ) -> None: 632 ) -> None:
653 name = app_data['name'] 633 name = app_data["name"]
654 dest_path = app_data['_instance_dir_path'] 634 dest_path = app_data["_instance_dir_path"]
655 if next(dest_path.iterdir(), None) != None: 635 if next(dest_path.iterdir(), None) != None:
656 log.debug(f"There is already a prepared dir at {dest_path}, nothing to do") 636 log.debug(f"There is already a prepared dir at {dest_path}, nothing to do")
657 return 637 return
658 try: 638 try:
659 prepare = app_data['prepare'].copy() 639 prepare = app_data["prepare"].copy()
660 except KeyError: 640 except KeyError:
661 prepare = {} 641 prepare = {}
662 642
663 if not prepare: 643 if not prepare:
664 log.debug(f"Nothing to prepare for {name!r}") 644 log.debug(f"Nothing to prepare for {name!r}")
666 646
667 for action, value in list(prepare.items()): 647 for action, value in list(prepare.items()):
668 log.debug(f"[{name}] [prepare] running {action!r} action") 648 log.debug(f"[{name}] [prepare] running {action!r} action")
669 if action == "git": 649 if action == "git":
670 try: 650 try:
671 git_path = which('git')[0] 651 git_path = which("git")[0]
672 except IndexError: 652 except IndexError:
673 raise exceptions.NotFound( 653 raise exceptions.NotFound(
674 "Can't find \"git\" executable, {name} can't be started without it" 654 "Can't find \"git\" executable, {name} can't be started without it"
675 ) 655 )
676 await async_process.run(git_path, "clone", value, str(dest_path)) 656 await async_process.run(git_path, "clone", value, str(dest_path))
686 666
687 async def _do_create_files( 667 async def _do_create_files(
688 self, 668 self,
689 app_data: dict, 669 app_data: dict,
690 ) -> None: 670 ) -> None:
691 dest_path = app_data['_instance_dir_path'] 671 dest_path = app_data["_instance_dir_path"]
692 files = app_data.get('files') 672 files = app_data.get("files")
693 if not files: 673 if not files:
694 return 674 return
695 if not isinstance(files, dict): 675 if not isinstance(files, dict):
696 raise ValueError('"files" must be a dictionary') 676 raise ValueError('"files" must be a dictionary')
697 for filename, data in files.items(): 677 for filename, data in files.items():