From 8eab40eb69f9a634f5eb136d85c8ee8ce3dc9747 Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Mon, 28 Nov 2022 23:39:36 -0500 Subject: [PATCH] Add constructor manager CLI --- constructor-manager/LICENSE | 21 ++ constructor-manager/MANIFEST.in | 5 + constructor-manager/README.md | 25 +++ constructor-manager/pyproject.toml | 10 + constructor-manager/setup.cfg | 52 +++++ .../src/constructor_manager/__init__.py | 2 + .../src/constructor_manager/api.py | 182 ++++++++++++++++++ .../src/constructor_manager/defaults.py | 3 + .../src/constructor_manager/run.py | 32 +++ .../src/constructor_manager/utils/__init__.py | 2 + .../utils/_tests/test_worker.py | 5 + .../src/constructor_manager/utils/conda.py | 22 +++ .../src/constructor_manager/utils/worker.py | 55 ++++++ constructor-manager/tox.ini | 31 +++ 14 files changed, 447 insertions(+) create mode 100644 constructor-manager/LICENSE create mode 100644 constructor-manager/MANIFEST.in create mode 100644 constructor-manager/README.md create mode 100644 constructor-manager/pyproject.toml create mode 100644 constructor-manager/setup.cfg create mode 100644 constructor-manager/src/constructor_manager/__init__.py create mode 100644 constructor-manager/src/constructor_manager/api.py create mode 100644 constructor-manager/src/constructor_manager/defaults.py create mode 100644 constructor-manager/src/constructor_manager/run.py create mode 100644 constructor-manager/src/constructor_manager/utils/__init__.py create mode 100644 constructor-manager/src/constructor_manager/utils/_tests/test_worker.py create mode 100644 constructor-manager/src/constructor_manager/utils/conda.py create mode 100644 constructor-manager/src/constructor_manager/utils/worker.py create mode 100644 constructor-manager/tox.ini diff --git a/constructor-manager/LICENSE b/constructor-manager/LICENSE new file mode 100644 index 00000000..7bc580bb --- /dev/null +++ b/constructor-manager/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022, Napari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/constructor-manager/MANIFEST.in b/constructor-manager/MANIFEST.in new file mode 100644 index 00000000..f3155af7 --- /dev/null +++ b/constructor-manager/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include README.md + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/constructor-manager/README.md b/constructor-manager/README.md new file mode 100644 index 00000000..50d15284 --- /dev/null +++ b/constructor-manager/README.md @@ -0,0 +1,25 @@ +# Constructor manager + +## Requirements + +- qtpy +- constructor-manager-cli (on base environment) + +## Usage + +```python +from constructor_manager.api import check_updates + + +def finished(result): + print(result) + + +worker = check_updates(package_name="napari", current_version="0.4.10", channel="conda-forge") +worker.finished.connect(finished) +worker.start() +``` + +## License + +Distributed under the terms of the MIT license. is free and open source software diff --git a/constructor-manager/pyproject.toml b/constructor-manager/pyproject.toml new file mode 100644 index 00000000..a0154124 --- /dev/null +++ b/constructor-manager/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["setuptools>=42.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 79 + +[tool.isort] +profile = "black" +line_length = 79 diff --git a/constructor-manager/setup.cfg b/constructor-manager/setup.cfg new file mode 100644 index 00000000..3d80485c --- /dev/null +++ b/constructor-manager/setup.cfg @@ -0,0 +1,52 @@ +[metadata] +name = constructor-manager +version = 0.1.0 +description = TODO +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/napari/packaging/constructor-manager +author = napari +author_email = TODO +license = MIT +license_files = LICENSE +classifiers = + Development Status :: 2 - Pre-Alpha + Framework :: napari + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Topic :: Scientific/Engineering :: Image Processing +project_urls = + Bug Tracker = https://github.com/napari/packaging/issues + Source Code = https://github.com/napari/packaging/constructor-manager + +[options] +packages = find: +install_requires = + qtpy +python_requires = >=3.8 +include_package_data = True +package_dir = + =src +[options.packages.find] +where = src + +[options.extras_require] +testing = + pytest-cov + pytest>=7.0.0 + mypy + typing-extensions + types-requests + +[mypy] +exclude = venv|tests + +[mypy-packaging.*] +ignore_missing_imports = True diff --git a/constructor-manager/src/constructor_manager/__init__.py b/constructor-manager/src/constructor_manager/__init__.py new file mode 100644 index 00000000..e9bca1b2 --- /dev/null +++ b/constructor-manager/src/constructor_manager/__init__.py @@ -0,0 +1,2 @@ +VERSION_INFO = (0, 1, 0) +__version__ = "0.1.0" diff --git a/constructor-manager/src/constructor_manager/api.py b/constructor-manager/src/constructor_manager/api.py new file mode 100644 index 00000000..91f3ea85 --- /dev/null +++ b/constructor-manager/src/constructor_manager/api.py @@ -0,0 +1,182 @@ +"""Constructor manager api.""" + +from typing import List, Optional + +from constructor_manager.defaults import DEFAULT_CHANNEL +from constructor_manager.utils.worker import ConstructorManagerWorker + + +def _run_action( + cmd, + package_name: Optional[str] = None, + version: Optional[str] = None, + channel: str = DEFAULT_CHANNEL, + plugins: Optional[List[str]] = None, + dev: bool = False, +) -> ConstructorManagerWorker: + """Run constructor action. + + Parameters + ---------- + cmd : str + Action to run. + package_name : str, optional + Name of the package to execute action on. + version : str, optional + Version of package to execute action on, by default ``None``. + channel : str, optional + Channel to check for updates, by default ``DEFAULT_CHANNEL``. + plugins : List[str], optional + List of plugins to install, by default ``None``. + dev : bool, optional + Check for development version, by default ``False``. + + Returns + ------- + ConstructorManagerWorker + Worker to check for updates. Includes a finished signal that returns + a ``dict`` with the result. + """ + args = [cmd] + if package_name is not None and version is not None: + spec: Optional[str] = f"{package_name}={version}" + else: + spec = package_name + + if package_name is not None and version is not None: + args.extend([spec, "--channel", channel]) + + if plugins: + args.append("--plugins") + args.extend(plugins) + + if dev: + args.extend(["--dev"]) + + detached = cmd != "status" + return ConstructorManagerWorker(args, detached=detached) + + +def check_updates( + package_name, current_version, channel: str = DEFAULT_CHANNEL, dev: bool = False +) -> ConstructorManagerWorker: + """Check for updates. + + Parameters + ---------- + package_name : str + Name of the package to check for updates. + current_version : str + Current version of the package. + channel : str, optional + Channel to check for updates, by default ``DEFAULT_CHANNEL``. + dev : bool, optional + Check for development version, by default ``False``. + + Returns + ------- + ConstructorManagerWorker + Worker to check for updates. Includes a finished signal that returns + a ``dict`` with the result. + """ + return _run_action( + "check-updates", package_name, version=current_version, channel=channel, dev=dev + ) + + +def update( + package_name, + channel: str = DEFAULT_CHANNEL, + plugins: Optional[List[str]] = None, + dev: bool = False, +) -> ConstructorManagerWorker: + """Update the package to given version. + If version is None update to latest version found. + + Returns + ------- + ConstructorManagerWorker + Worker to check for updates. Includes a finished signal that returns + a ``dict`` with the result. + """ + return _run_action( + "update", package_name, channel=channel, plugins=plugins, dev=dev + ) + + +def rollback( + package_name, + current_version: Optional[str], + channel: str = DEFAULT_CHANNEL, + plugins: Optional[List[str]] = None, + dev: bool = False, +) -> ConstructorManagerWorker: + """Update the package to given version. + If version is None update to latest version found. + + Parameters + --------- + package_name : str + Name of the package to check for updates. + version : str, optional + Version to rollback to, by default ``None``. + channel : str, optional + Channel to check for updates, by default ``DEFAULT_CHANNEL``. + dev : bool, optional + Check for development version, by default ``False``. + + Returns + ------- + ConstructorManagerWorker + Worker to check for updates. Includes a finished signal that returns + a ``dict`` with the result. + """ + return _run_action( + "rollback", + package_name, + version=current_version, + channel=channel, + plugins=plugins, + dev=dev, + ) + + +def restore( + package_name, + version, + channel: str = DEFAULT_CHANNEL, + dev: bool = False, + plugins: Optional[List[str]] = None, +) -> ConstructorManagerWorker: + """Restore the current version of package. + + Parameters + --------- + package_name : str + Name of the package to check for updates. + version : str, optional + Version to rollback to, by default ``None``. + channel : str, optional + Channel to check for updates, by default ``DEFAULT_CHANNEL``. + dev : bool, optional + Check for development versions, by default ``False``. + + Returns + ------- + ConstructorManagerWorker + Worker to check for updates. Includes a finished signal that returns + a ``dict`` with the result. + """ + return _run_action( + "restore", + package_name, + version=version, + channel=channel, + plugins=plugins, + dev=dev, + ) + + +def status(): + """Get status for the state of the constructor updater.""" + return _run_action("status") diff --git a/constructor-manager/src/constructor_manager/defaults.py b/constructor-manager/src/constructor_manager/defaults.py new file mode 100644 index 00000000..de3e7b90 --- /dev/null +++ b/constructor-manager/src/constructor_manager/defaults.py @@ -0,0 +1,3 @@ +"""Defaults and constants.""" + +DEFAULT_CHANNEL = "conda-forge" diff --git a/constructor-manager/src/constructor_manager/run.py b/constructor-manager/src/constructor_manager/run.py new file mode 100644 index 00000000..aeeeecf0 --- /dev/null +++ b/constructor-manager/src/constructor_manager/run.py @@ -0,0 +1,32 @@ +"""Constructor updater api run tester.""" + +import sys + +from qtpy.QtCore import QCoreApplication, QTimer # type: ignore + +from constructor_manager.api import check_updates + + +def _finished(res): + print("This is the result", res) + + +if __name__ == "__main__": + app = QCoreApplication([]) + + # Process the event loop + timer = QTimer() + timer.timeout.connect(lambda: None) # type: ignore + timer.start(100) + + # worker = check_updates( + # "napari", + # current_version="0.4.15", + # channel="napari", + # dev=True, + # ) + worker = check_updates("napari", current_version="0.4.15") + worker.finished.connect(_finished) + worker.start() + + sys.exit(app.exec_()) diff --git a/constructor-manager/src/constructor_manager/utils/__init__.py b/constructor-manager/src/constructor_manager/utils/__init__.py new file mode 100644 index 00000000..e9bca1b2 --- /dev/null +++ b/constructor-manager/src/constructor_manager/utils/__init__.py @@ -0,0 +1,2 @@ +VERSION_INFO = (0, 1, 0) +__version__ = "0.1.0" diff --git a/constructor-manager/src/constructor_manager/utils/_tests/test_worker.py b/constructor-manager/src/constructor_manager/utils/_tests/test_worker.py new file mode 100644 index 00000000..94027677 --- /dev/null +++ b/constructor-manager/src/constructor_manager/utils/_tests/test_worker.py @@ -0,0 +1,5 @@ +from constructor_manager.utils.conda import get_base_prefix + + +def test_worker(): + assert get_base_prefix() diff --git a/constructor-manager/src/constructor_manager/utils/conda.py b/constructor-manager/src/constructor_manager/utils/conda.py new file mode 100644 index 00000000..b11006bf --- /dev/null +++ b/constructor-manager/src/constructor_manager/utils/conda.py @@ -0,0 +1,22 @@ +"""Conda utilities.""" + +import sys +from pathlib import Path + + +def get_base_prefix() -> Path: + """Get base conda prefix. + + Returns + ------- + pathlib.Path + Base conda prefix. + """ + current = Path(sys.prefix) + if (current / "envs").exists() and (current / "envs").is_dir(): + return current + + if current.parent.name == "envs" and current.parent.is_dir(): + return current.parent.parent + + return current diff --git a/constructor-manager/src/constructor_manager/utils/worker.py b/constructor-manager/src/constructor_manager/utils/worker.py new file mode 100644 index 00000000..ca9d6ac0 --- /dev/null +++ b/constructor-manager/src/constructor_manager/utils/worker.py @@ -0,0 +1,55 @@ +"""Constructor updater api worker.""" + +import json + +from qtpy.QtCore import QObject, QProcess, Signal # type: ignore + +from constructor_manager.utils.conda import get_base_prefix + + +class ConstructorManagerWorker(QObject): + """TODO: + + Parameters + ---------- + args : list + Arguments to pass to the constructor manager. + detached : bool, optional + Run the process detached, by default ``False``. + """ + + finished = Signal(dict) + + def __init__(self, args, detached=False): + super().__init__() + self._detached = detached + self._program = get_base_prefix() / "bin" / "constructor-manager" + + if not self._program.is_file(): + raise FileNotFoundError(f"Could not find {self._program}") + + self._process = QProcess() + self._process.setArguments(args) + self._process.setProgram(str(self._program)) + self._process.finished.connect(self._finished) + + def _finished(self, *args, **kwargs): + """Handle the finished signal of the worker and emit results.""" + stdout = self._process.readAllStandardOutput() + stderr = self._process.readAllStandardError() + data = stdout.data().decode() + error = stderr.data().decode() + try: + data = json.loads(data) + except Exception as e: + print(e) + + result = {"data": data, "error": error} + self.finished.emit(result) + + def start(self): + """Start the worker.""" + if self._detached: + self._process.startDetached() + else: + self._process.start() diff --git a/constructor-manager/tox.ini b/constructor-manager/tox.ini new file mode 100644 index 00000000..b1f09aa6 --- /dev/null +++ b/constructor-manager/tox.ini @@ -0,0 +1,31 @@ +# For more information about tox, see https://tox.readthedocs.io/en/latest/ +[tox] +envlist = py{38,39,310}-{linux,macos,windows} +isolated_build=true + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + +[gh-actions:env] +PLATFORM = + ubuntu-latest: linux + macos-latest: macos + windows-latest: windows + +[testenv] +platform = + macos: darwin + linux: linux + windows: win32 +passenv = + CI + GITHUB_ACTIONS + DISPLAY XAUTHORITY + NUMPY_EXPERIMENTAL_ARRAY_FUNCTION + PYVISTA_OFF_SCREEN +extras = + testing +commands = pytest -v --color=yes --cov=constructor_manager