mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
on demand mongo url input, use Path
This commit is contained in:
parent
a5c8b24988
commit
7065953166
6 changed files with 280 additions and 76 deletions
|
|
@ -13,7 +13,7 @@ from pathlib import Path
|
|||
from appdirs import user_data_dir
|
||||
from pype.version import __version__
|
||||
from pype.lib import PypeSettingsRegistry
|
||||
from igniter.tools import load_environments
|
||||
from .tools import load_environments
|
||||
|
||||
|
||||
class BootstrapRepos:
|
||||
|
|
@ -26,7 +26,14 @@ class BootstrapRepos:
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, progress_callback: Callable = None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
progress_callback (callable): Optional callback method to report
|
||||
progress.
|
||||
|
||||
"""
|
||||
# vendor and app used to construct user data dir
|
||||
self._vendor = "pypeclub"
|
||||
self._app = "pype"
|
||||
|
|
@ -34,6 +41,14 @@ class BootstrapRepos:
|
|||
self.data_dir = Path(user_data_dir(self._app, self._vendor))
|
||||
self.registry = PypeSettingsRegistry()
|
||||
|
||||
# dummy progress reporter
|
||||
def empty_progress(x: int):
|
||||
return x
|
||||
|
||||
if not progress_callback:
|
||||
progress_callback = empty_progress
|
||||
self._progress_callback = progress_callback
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
self.live_repo_dir = Path(sys.executable).parent / "repos"
|
||||
else:
|
||||
|
|
@ -44,25 +59,44 @@ class BootstrapRepos:
|
|||
"""Get version of local Pype."""
|
||||
return __version__
|
||||
|
||||
def install_live_repos(self, progress_callback=None) -> Union[Path, None]:
|
||||
"""Copy zip created from local repositories to user data dir.
|
||||
@staticmethod
|
||||
def get_version(repo_dir: Path) -> Union[str, None]:
|
||||
"""Get version of Pype in given directory.
|
||||
|
||||
Args:
|
||||
progress_callback (callable): Optional callback method to report
|
||||
progress.
|
||||
repo_dir (Path): Path to Pype repo.
|
||||
|
||||
Returns:
|
||||
str: version string.
|
||||
None: if Pype is not found.
|
||||
|
||||
"""
|
||||
# try to find version
|
||||
version_file = Path(repo_dir) / "pype" / "version.py"
|
||||
if not version_file.exists():
|
||||
return None
|
||||
|
||||
version = {}
|
||||
with version_file.open("r") as fp:
|
||||
exec(fp.read(), version)
|
||||
|
||||
return version['__version__']
|
||||
|
||||
def install_live_repos(self, repo_dir: Path = None) -> Union[Path, None]:
|
||||
"""Copy zip created from Pype repositories to user data dir.
|
||||
|
||||
Args:
|
||||
repo_dir (Path, optional): Path to Pype repository.
|
||||
|
||||
Returns:
|
||||
Path: path of installed repository file.
|
||||
|
||||
"""
|
||||
# dummy progress reporter
|
||||
def empty_progress(x: int):
|
||||
return x
|
||||
|
||||
if not progress_callback:
|
||||
progress_callback = empty_progress
|
||||
|
||||
# create zip from repositories
|
||||
local_version = self.get_local_version()
|
||||
if not repo_dir:
|
||||
version = self.get_local_version()
|
||||
repo_dir = self.live_repo_dir
|
||||
else:
|
||||
version = self.get_version(repo_dir)
|
||||
|
||||
# create destination directory
|
||||
try:
|
||||
|
|
@ -71,12 +105,10 @@ class BootstrapRepos:
|
|||
self._log.error("directory already exists")
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_zip = \
|
||||
Path(temp_dir) / f"pype-repositories-v{local_version}.zip"
|
||||
Path(temp_dir) / f"pype-repositories-v{version}.zip"
|
||||
self._log.info(f"creating zip: {temp_zip}")
|
||||
|
||||
BootstrapRepos._create_pype_zip(
|
||||
temp_zip, self.live_repo_dir,
|
||||
progress_callback=progress_callback)
|
||||
self._create_pype_zip(temp_zip, repo_dir)
|
||||
if not os.path.exists(temp_zip):
|
||||
self._log.error("make archive failed.")
|
||||
return None
|
||||
|
|
@ -98,10 +130,10 @@ class BootstrapRepos:
|
|||
return None
|
||||
return self.data_dir / temp_zip.name
|
||||
|
||||
@staticmethod
|
||||
def _create_pype_zip(
|
||||
self,
|
||||
zip_path: Path, include_dir: Path,
|
||||
progress_callback: Callable, include_pype: bool = True) -> None:
|
||||
include_pype: bool = True) -> None:
|
||||
"""Pack repositories and Pype into zip.
|
||||
|
||||
We are using :mod:`zipfile` instead :meth:`shutil.make_archive`
|
||||
|
|
@ -113,10 +145,7 @@ class BootstrapRepos:
|
|||
|
||||
Args:
|
||||
zip_path (str): path to zip file.
|
||||
include_dir: repo directories to include.
|
||||
progress_callback (Callable(progress: int): callback to
|
||||
report progress back to UI progress bar. It takes progress
|
||||
percents as argument.
|
||||
include_dir (Path): repo directories to include.
|
||||
include_pype (bool): add Pype module itself.
|
||||
|
||||
"""
|
||||
|
|
@ -130,22 +159,22 @@ class BootstrapRepos:
|
|||
else:
|
||||
repo_inc = 98.0 / float(repo_files)
|
||||
progress = 0
|
||||
with ZipFile(zip_path, "w") as zip:
|
||||
for root, _, files in os.walk(include_dir):
|
||||
with ZipFile(zip_path, "w") as zip_file:
|
||||
for root, _, files in os.walk(include_dir.as_posix()):
|
||||
for file in files:
|
||||
zip.write(
|
||||
zip_file.write(
|
||||
os.path.relpath(os.path.join(root, file),
|
||||
os.path.join(include_dir, '..')),
|
||||
os.path.relpath(os.path.join(root, file),
|
||||
os.path.join(include_dir))
|
||||
)
|
||||
progress += repo_inc
|
||||
progress_callback(int(progress))
|
||||
self._progress_callback(int(progress))
|
||||
# add pype itself
|
||||
if include_pype:
|
||||
for root, _, files in os.walk("pype"):
|
||||
for file in files:
|
||||
zip.write(
|
||||
zip_file.write(
|
||||
os.path.relpath(os.path.join(root, file),
|
||||
os.path.join('pype', '..')),
|
||||
os.path.join(
|
||||
|
|
@ -154,9 +183,9 @@ class BootstrapRepos:
|
|||
os.path.join('pype', '..')))
|
||||
)
|
||||
progress += pype_inc
|
||||
progress_callback(int(progress))
|
||||
zip.testzip()
|
||||
progress_callback(100)
|
||||
self._progress_callback(int(progress))
|
||||
zip_file.testzip()
|
||||
self._progress_callback(100)
|
||||
|
||||
@staticmethod
|
||||
def add_paths_from_archive(archive: Path) -> None:
|
||||
|
|
@ -169,7 +198,6 @@ class BootstrapRepos:
|
|||
archive (str): path to archive.
|
||||
|
||||
"""
|
||||
name_list = []
|
||||
with ZipFile(archive, "r") as zip_file:
|
||||
name_list = zip_file.namelist()
|
||||
|
||||
|
|
@ -249,13 +277,43 @@ class BootstrapRepos:
|
|||
"""
|
||||
os.environ["AVALON_MONGO"] = mongo_url
|
||||
env = load_environments()
|
||||
if not env.get("PYPE_ROOT"):
|
||||
if not env.get("PYPE_PATH"):
|
||||
return None
|
||||
return Path(env.get("PYPE_ROOT"))
|
||||
return Path(env.get("PYPE_PATH"))
|
||||
|
||||
def process_entered_path(self, location: str) -> str:
|
||||
def process_entered_location(self, location: str) -> Union[Path, None]:
|
||||
"""Process user entered location string.
|
||||
|
||||
It decides if location string is mongodb url or path.
|
||||
If it is mongodb url, it will connect and load ``PYPE_PATH`` from
|
||||
there and use it as path to Pype. In it is _not_ mongodb url, it
|
||||
is assumed we have a path, this is tested and zip file is
|
||||
produced and installed using :meth:`install_live_repos`.
|
||||
|
||||
Args:
|
||||
location (str): User entered location.
|
||||
|
||||
Returns:
|
||||
Path: to Pype zip produced from this location.
|
||||
None: Zipping failed.
|
||||
|
||||
"""
|
||||
pype_path = None
|
||||
if location.startswith("mongodb"):
|
||||
pype_path = self._get_pype_from_mongo(location)
|
||||
if not pype_path:
|
||||
self._log.error("cannot find PYPE_PATH in settings.")
|
||||
return None
|
||||
|
||||
return pype_path
|
||||
if not pype_path:
|
||||
pype_path = Path(location)
|
||||
|
||||
if not pype_path.exists():
|
||||
self._log.error(f"{pype_path} doesn't exists.")
|
||||
return None
|
||||
|
||||
repo_file = self.install_live_repos(pype_path)
|
||||
if not repo_file.exists():
|
||||
self._log.error(f"installing zip {repo_file} failed.")
|
||||
return None
|
||||
return repo_file
|
||||
|
|
|
|||
|
|
@ -124,6 +124,37 @@ class InstallDialog(QtWidgets.QDialog):
|
|||
input_layout.addWidget(self.user_input)
|
||||
input_layout.addWidget(self._btn_select)
|
||||
|
||||
# Mongo box | OK button
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
class MongoWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
self._btn_mongo = None
|
||||
super(MongoWidget, self).__init__(parent)
|
||||
mongo_layout = QtWidgets.QHBoxLayout()
|
||||
mongo_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._mongo_input = QtWidgets.QLineEdit()
|
||||
self._mongo_input.setPlaceholderText("Mongo URL")
|
||||
self._mongo_input.textChanged.connect(self._mongo_changed)
|
||||
self._mongo_input.setStyleSheet(
|
||||
("color: rgb(233, 233, 233);"
|
||||
"background-color: rgb(64, 64, 64);"
|
||||
"padding: 0.5em;"
|
||||
"border: 1px solid rgb(32, 32, 32);")
|
||||
)
|
||||
|
||||
mongo_layout.addWidget(self._mongo_input)
|
||||
self.setLayout(mongo_layout)
|
||||
|
||||
def _mongo_changed(self, mongo: str):
|
||||
self._mongo_url = mongo
|
||||
|
||||
def get_mongo_url(self):
|
||||
return self._mongo_url
|
||||
|
||||
self._mongo = MongoWidget(self)
|
||||
self._mongo.hide()
|
||||
|
||||
# Bottom button bar
|
||||
# --------------------------------------------------------------------
|
||||
bottom_widget = QtWidgets.QWidget()
|
||||
|
|
@ -237,6 +268,7 @@ class InstallDialog(QtWidgets.QDialog):
|
|||
main.addWidget(self.main_label)
|
||||
main.addWidget(self.pype_path_label)
|
||||
main.addLayout(input_layout)
|
||||
main.addWidget(self._mongo)
|
||||
main.addStretch(1)
|
||||
main.addWidget(self._status_label)
|
||||
main.addWidget(self._status_box)
|
||||
|
|
@ -292,10 +324,12 @@ class InstallDialog(QtWidgets.QDialog):
|
|||
|
||||
self._disable_buttons()
|
||||
self._install_thread = InstallThread(self)
|
||||
self._install_thread.ask_for_mongo.connect(self._show_mongo)
|
||||
self._install_thread.message.connect(self._update_console)
|
||||
self._install_thread.progress.connect(self._update_progress)
|
||||
self._install_thread.finished.connect(self._enable_buttons)
|
||||
self._install_thread.set_path(self._path)
|
||||
self._install_thread.set_mongo(self._mongo.get_mongo_url())
|
||||
self._install_thread.start()
|
||||
|
||||
def _update_progress(self, progress: int):
|
||||
|
|
@ -304,10 +338,19 @@ class InstallDialog(QtWidgets.QDialog):
|
|||
def _on_exit_clicked(self):
|
||||
self.close()
|
||||
|
||||
def _show_mongo(self):
|
||||
self._update_console("mongo showed")
|
||||
|
||||
def _path_changed(self, path: str) -> str:
|
||||
"""Set path."""
|
||||
self._path = path
|
||||
return path
|
||||
if not self._path.startswith("mongodb"):
|
||||
self._mongo.setVisible(True)
|
||||
else:
|
||||
self._mongo.setVisible(False)
|
||||
|
||||
if len(self._path) < 1:
|
||||
self._mongo.setVisible(False)
|
||||
|
||||
def _update_console(self, msg: str, error: bool = False) -> None:
|
||||
"""Display message in console.
|
||||
|
|
@ -425,6 +468,98 @@ class PathValidator(QValidator):
|
|||
QValidator.State.Acceptable, reason, path, pos)
|
||||
|
||||
|
||||
class CollapsibleWidget(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent=None, title: str = "", animation: int = 300):
|
||||
self._mainLayout = QtWidgets.QGridLayout(parent)
|
||||
self._toggleButton = QtWidgets.QToolButton(parent)
|
||||
self._headerLine = QtWidgets.QFrame(parent)
|
||||
self._toggleAnimation = QtCore.QParallelAnimationGroup(parent)
|
||||
self._contentArea = QtWidgets.QScrollArea(parent)
|
||||
self._animation = animation
|
||||
self._title = title
|
||||
super(CollapsibleWidget, self).__init__(parent)
|
||||
self._initUi()
|
||||
|
||||
def _initUi(self):
|
||||
self._toggleButton.setStyleSheet(
|
||||
"""QToolButton {
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
self._toggleButton.setToolButtonStyle(
|
||||
QtCore.Qt.ToolButtonTextBesideIcon)
|
||||
|
||||
self._toggleButton.setArrowType(QtCore.Qt.ArrowType.RightArrow)
|
||||
self._toggleButton.setText(self._title)
|
||||
self._toggleButton.setCheckable(True)
|
||||
self._toggleButton.setChecked(False)
|
||||
|
||||
self._headerLine.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
self._headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
self._headerLine.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Maximum)
|
||||
|
||||
self._contentArea.setStyleSheet(
|
||||
"""QScrollArea {
|
||||
background-color: rgb(32, 32, 32);
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
self._contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Fixed)
|
||||
self._contentArea.setMaximumHeight(0)
|
||||
self._contentArea.setMinimumHeight(0)
|
||||
|
||||
self._toggleAnimation.addAnimation(
|
||||
QtCore.QPropertyAnimation(self, b"minimumHeight"))
|
||||
self._toggleAnimation.addAnimation(
|
||||
QtCore.QPropertyAnimation(self, b"maximumHeight"))
|
||||
self._toggleAnimation.addAnimation(
|
||||
QtCore.QPropertyAnimation(self._contentArea, b"maximumHeight"))
|
||||
|
||||
self._mainLayout.setVerticalSpacing(0)
|
||||
self._mainLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
row = 0
|
||||
|
||||
self._mainLayout.addWidget(
|
||||
self._toggleButton, row, 0, 1, 1, QtCore.Qt.AlignCenter)
|
||||
self._mainLayout.addWidget(
|
||||
self._headerLine, row, 2, 1, 1)
|
||||
row += row
|
||||
self._mainLayout.addWidget(self._contentArea, row, 0, 1, 3)
|
||||
self.setLayout(self._mainLayout)
|
||||
|
||||
self._toggleButton.toggled.connect(self._toggle_action)
|
||||
|
||||
def _toggle_action(self, collapsed:bool):
|
||||
arrow = QtCore.Qt.ArrowType.DownArrow if collapsed else QtCore.Qt.ArrowType.RightArrow # noqa: E501
|
||||
direction = QtCore.QAbstractAnimation.Forward if collapsed else QtCore.QAbstractAnimation.Backward # noqa: E501
|
||||
self._toggleButton.setArrowType(arrow)
|
||||
self._toggleAnimation.setDirection(direction)
|
||||
self._toggleAnimation.start()
|
||||
|
||||
def setContentLayout(self, content_layout: QtWidgets.QLayout):
|
||||
self._contentArea.setLayout(content_layout)
|
||||
collapsed_height = \
|
||||
self.sizeHint().height() - self._contentArea.maximumHeight()
|
||||
content_height = self._contentArea.sizeHint().height()
|
||||
|
||||
for i in range(0, self._toggleAnimation.animationCount() - 1):
|
||||
sec_anim = self._toggleAnimation.animationAt(i)
|
||||
sec_anim.setDuration(self._animation)
|
||||
sec_anim.setStartValue(collapsed_height)
|
||||
sec_anim.setEndValue(collapsed_height + content_height)
|
||||
|
||||
con_anim = self._toggleAnimation.animationAt(
|
||||
self._toggleAnimation.animationCount() - 1)
|
||||
|
||||
con_anim.setDuration(self._animation)
|
||||
con_anim.setStartValue(0)
|
||||
con_anim.setEndValue(32)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
d = InstallDialog()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from Qt.QtCore import QThread, Signal
|
|||
from igniter.tools import load_environments
|
||||
|
||||
from .bootstrap_repos import BootstrapRepos
|
||||
from .tools import validate_mongo_connection
|
||||
|
||||
|
||||
class InstallThread(QThread):
|
||||
|
|
@ -23,66 +24,53 @@ class InstallThread(QThread):
|
|||
|
||||
"""
|
||||
|
||||
progress = Signal(int)
|
||||
message = Signal((str, bool))
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self.progress = Signal(int)
|
||||
self.message = Signal((str, bool))
|
||||
self._mongo = None
|
||||
self._path = None
|
||||
QThread.__init__(self, parent)
|
||||
|
||||
def run(self):
|
||||
self.message.emit("Installing Pype ...", False)
|
||||
# find local version of Pype
|
||||
bs = BootstrapRepos()
|
||||
bs = BootstrapRepos(progress_callback=self.set_progress)
|
||||
local_version = bs.get_local_version()
|
||||
|
||||
# if user did entered nothing, we install Pype from local version.
|
||||
# zip content of `repos`, copy it to user data dir and append
|
||||
# version to it.
|
||||
if not self._path:
|
||||
self.ask_for_mongo.emit(True)
|
||||
self.message.emit(
|
||||
f"We will use local Pype version {local_version}", False)
|
||||
repo_file = bs.install_live_repos(
|
||||
progress_callback=self.set_progress)
|
||||
repo_file = bs.install_live_repos()
|
||||
if not repo_file:
|
||||
self.message.emit(
|
||||
f"!!! install failed - {repo_file}", True)
|
||||
return
|
||||
self.message.emit(f"installed as {repo_file}", False)
|
||||
else:
|
||||
pype_path = None
|
||||
# find central pype location from database
|
||||
if self._path.startswith("mongodb"):
|
||||
self.message.emit("determining Pype location from db...")
|
||||
os.environ["AVALON_MONGO"] = self._path
|
||||
env = load_environments()
|
||||
if not env.get("PYPE_ROOT"):
|
||||
if self._mongo:
|
||||
if not validate_mongo_connection(self._mongo):
|
||||
self.message.emit(
|
||||
"!!! cannot load path to Pype from db", True)
|
||||
f"!!! invalid mongo url {self._mongo}", True)
|
||||
return
|
||||
bs.registry.set_secure_item("avalonMongo", self._mongo)
|
||||
os.environ["AVALON_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(f"path loaded from database ...", False)
|
||||
self.message.emit(env.get("PYPE_ROOT"), False)
|
||||
if not os.path.exists(env.get("PYPE_ROOT")):
|
||||
self.message.emit(f"!!! path doesn't exist", True)
|
||||
return
|
||||
pype_path = env.get("PYPE_ROOT")
|
||||
if not pype_path:
|
||||
pype_path = self._path
|
||||
repo_file = bs.process_entered_location(self._path)
|
||||
|
||||
if not os.path.exists(pype_path):
|
||||
self.message.emit(f"!!! path doesn't exist", True)
|
||||
if not repo_file:
|
||||
self.message.emit(f"!!! Cannot install", True)
|
||||
return
|
||||
|
||||
# detect Pype in path
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def set_path(self, path: str) -> None:
|
||||
self._path = path
|
||||
|
||||
def set_mongo(self, mongo: str) -> None:
|
||||
self._mongo = mongo
|
||||
|
||||
def set_progress(self, progress: int):
|
||||
self.progress.emit(progress)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ from pymongo.errors import ServerSelectionTimeoutError, InvalidURI
|
|||
|
||||
|
||||
def validate_mongo_connection(cnx: str) -> (bool, str):
|
||||
"""Check if provided mongodb URL is valid.
|
||||
|
||||
Args:
|
||||
cnx (str): URL to validate.
|
||||
|
||||
Returns:
|
||||
(bool, str): True if ok, False if not and reason in str.
|
||||
|
||||
"""
|
||||
parsed = urlparse(cnx)
|
||||
if parsed.scheme in ["mongodb", "mongodb+srv"]:
|
||||
# we have mongo connection string. Let's try if we can connect.
|
||||
|
|
@ -25,7 +34,7 @@ def validate_mongo_connection(cnx: str) -> (bool, str):
|
|||
|
||||
try:
|
||||
client = MongoClient(**mongo_args)
|
||||
# client.server_info()
|
||||
client.server_info()
|
||||
except ServerSelectionTimeoutError as e:
|
||||
return False, f"Cannot connect to server {cnx} - {e}"
|
||||
except ValueError:
|
||||
|
|
@ -70,6 +79,15 @@ def validate_path_string(path: str) -> (bool, str):
|
|||
|
||||
|
||||
def load_environments() -> dict:
|
||||
"""Load environments from Pype.
|
||||
|
||||
This will load environments from database, process them with
|
||||
:mod:`acre` and return them as flattened dictionary.
|
||||
|
||||
Returns;
|
||||
dict of str: loaded and processed environments.
|
||||
|
||||
"""
|
||||
try:
|
||||
import acre
|
||||
except ImportError:
|
||||
|
|
|
|||
13
pype.py
13
pype.py
|
|
@ -87,10 +87,15 @@ def boot():
|
|||
break
|
||||
|
||||
if not os.getenv("AVALON_MONGO"):
|
||||
print("*** No DB connection string specified.")
|
||||
import igniter
|
||||
igniter.run()
|
||||
set_environments()
|
||||
try:
|
||||
avalon_mongo = bootstrap.registry.get_secure_item("avalonMongo")
|
||||
except ValueError:
|
||||
print("*** No DB connection string specified.")
|
||||
import igniter
|
||||
igniter.run()
|
||||
set_environments()
|
||||
else:
|
||||
os.environ["AVALON_MONGO"] = avalon_mongo
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
if not pype_versions:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from pype.lib import PypeSettingsRegistry
|
|||
@pytest.fixture
|
||||
def fix_bootstrap(tmp_path):
|
||||
bs = BootstrapRepos()
|
||||
bs.live_repo_dir = os.path.abspath('repos')
|
||||
bs.live_repo_dir = Path(os.path.abspath('repos'))
|
||||
bs.data_dir = tmp_path
|
||||
return bs
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue