diff --git a/inkBoard/__init__.py b/inkBoard/__init__.py index 58acca0..48a8375 100644 --- a/inkBoard/__init__.py +++ b/inkBoard/__init__.py @@ -1,8 +1,5 @@ ###Inkboard -__version__ = "0.3.2.dev1" -"inkBoard version" - import logging as __logging from types import MappingProxyType @@ -20,6 +17,8 @@ integration_objects: MappingProxyType[Literal["integration_entry"],Any] +__version__ = "0.3.2.dev1" +"inkBoard version" def getLogger(name: Union[str,None] = None) -> "ib_logging.BaseLogger": """Convenience method to get a logger with type hinting for additional levels like verbose. diff --git a/inkBoard/arguments.py b/inkBoard/arguments.py index f7948c0..7257c69 100644 --- a/inkBoard/arguments.py +++ b/inkBoard/arguments.py @@ -121,16 +121,36 @@ def parse_args(): const.COMMAND_INSTALL, help="Installs inkBoard packages or requirements from a config folder" ) - parser_install.add_argument("file", help=""" - The file to install. If it is a ZIP file, inkBoard will check if it is an inkBoard compatible one (Either an inkBoard package, or a zip of a platform or integration folder). - If a YAML file, inkBoard will go through the base directory, the files folder and the custom folder and call pip install on any files it finds titled requirements.txt. In the custom integration folder, it will also take care of installing requirements for the integrations presents. - If it is platform or integration, appended by a name, inkBoard will go through the install process of that respective platform/integration, provided it is installed internally. - If not supplied, the installer will look for all suitable ZIP files in the current directory. - """, default=None, nargs='?') - ##How to deal with passing the name part of the command? - - parser_install.add_argument("name", help="The name of platform or integration to install. This is not used if the file command is not one of integration or platform.", - default=None, nargs="?") + parser_install.add_argument("--upgrade", "-U", action='store_true', help=""" + Checks if any integrations and platforms passed can be upgraded from the inkBoard package index. + """, dest="upgrade") + parser_install.add_argument("--dev", "-D", action='store_true', help=""" + Enables downloading packages from the developer branch. + """, dest="upgrade") + parser_install.add_argument("--local", "-L", help=""" + Installs local files. Pass appropriate file arguments. + Accepts:\n + - (paths to) ZIP files of integrations, platforms, or packages + - (paths to) a configuration YAML file. This creates a list of install requirements for custom integrations and all requirements.txt files found in the configuration directory and sub directories.\n + - The names of a platform or integration already internally installed, to install its requirements. This case is usually already handled by the other methods. + """, dest="local_installs", default=[], nargs='?') + parser_install.add_argument("--platforms", "-P", help=""" + Downloads and installs provided platforms. If --upgrade is not provided, nothing is done if the platform is already installed. + """, dest="platforms", default=[], nargs='?') + parser_install.add_argument("--integrations", "-I", help=""" + Downloads and installs provided integrations. If --upgrade is not provided, nothing is done if the integration is already installed. + """, dest="integrations", default=[], nargs='?') + parser_install.add_argument("index", "--index", "-X", help=""" + Manually determines the install type for each argument. Names ending with .zip, .txt, .yaml and .yml are handled as arguments to --local. + Other arguments are looked up in the inkBoard index and downloaded and installed if needed. + """, dest="index_installs", default=[], nargs='?') + # raise Exception("Go implement the functions for the install arguments. But first get the index repo set up probably.") + ##installer command + #[x]: rename file to type/installtype; options will be integration, platform and file + #[x]: names argument; the name(s) of things to install. + #[x]: shorthands (-f, -i, -p); either in the function or somehow in the parser make them exclusive -> no, add them as regular options, with nargs + #[ ]: FOR LATER: add argument that allows installation from github links + ## ok so: seperate arg for each type, make the command call everything at once parser_install.add_argument('--no-input', help="Disables any input prompts that are deemed optional", action='store_true') diff --git a/inkBoard/packaging/__init__.py b/inkBoard/packaging/__init__.py index 508605d..12d03d0 100644 --- a/inkBoard/packaging/__init__.py +++ b/inkBoard/packaging/__init__.py @@ -7,7 +7,7 @@ import inkBoard # from inkBoard.types import * from inkBoard import constants as bootstrap -from .types import internalinstalltypes +from .types import internalinstalltypes, downloadinstalltypes if TYPE_CHECKING: @@ -76,16 +76,20 @@ def create_core_package(core: "CORE", name: str = None, pack_all: bool = False, return 0 -def run_install_command(file: str, name: str, no_input: bool): +def run_install_command(install_arg: str, name: str, no_input: bool): ##Add functionality to installer for internal installs (platforms and integrations) ##Usage: install [platform/integration] [name] - if file in internalinstalltypes.__args__: - return install_internal(file, name, no_input) + #[ ]: install by default goes to download (i.e. similar to pip install) + #[ ]: how to: allow to pass platforms/integrations -> goes to download; otherwise check if file + #[ ]: Add a flag to call the internal installer (or a install - requirements) + + if install_arg in downloadinstalltypes.__args__: + return install_internal_requirements(install_arg, name, no_input) else: - return install_packages(file, no_input) + return install_packages(install_arg, no_input) -def install_internal(install_type: str, name:str, no_input: bool = False): +def install_internal_requirements(install_type: str, name:str, no_input: bool = False): from .install import InternalInstaller return InternalInstaller(install_type, name, no_input, confirm_input).install() @@ -109,5 +113,6 @@ def install_packages(file: Union[str, Path] = None, no_input: bool = False): PackageInstaller(package, skip_confirmations=no_input, confirmation_function=confirm_input).install() return 0 - +def download_packages(): + return diff --git a/inkBoard/packaging/download.py b/inkBoard/packaging/download.py index b863d51..e634f09 100644 --- a/inkBoard/packaging/download.py +++ b/inkBoard/packaging/download.py @@ -4,19 +4,23 @@ from typing import ( Union, Literal, - TYPE_CHECKING + TYPE_CHECKING, ) + import urllib.request from pathlib import Path import os from datetime import datetime as dt from datetime import timedelta import json +import tempfile from inkBoard import logging +from .install import PackageInstaller from .types import ( - PackageIndex + PackageIndex, + branchtypes ) from .constants import ( PACKAGE_INDEX_URL, @@ -51,9 +55,14 @@ class Downloader: ##Invalid url (i.e. invalid name thing) ##No internet ##Mainly just to know what the errors are - def __init__(self, destination_folder : Path): + def __init__(self, destination_folder : Path, confirmation_function = None): print(self._index_downloaded) self.destination_folder = destination_folder + self._confirmation_function = confirmation_function + + #[ ]: Implement confirmation_function + #[ ]: parse version from the string + def get_package_index(self, force_get = False) -> dict: @@ -84,44 +93,102 @@ def get_package_index(self, force_get = False) -> dict: self.index : PackageIndex = package_index return package_index - def download_integration_package(self, name : str, package_type : Literal["main","dev"] = "main", version : str = ""): - #"https://github.com/Slalamander/inkBoard-package-index/raw/refs/heads/main/platforms/desktop0.0.2.zip" - if package_type not in ("main","dev"): - raise ValueError(f"branch must be one of {packagebranchtypes}, you passed {package_type}") + def download_integration_package(self, name : str, package_branch : branchtypes = "main", version : str = ""): + ##[ ]: create url -> download into temp folder -> copy to appropriate inkboard folder + filename, package_url = self._make_package_url(name, "integrations", package_branch, version) + with tempfile.TemporaryDirectory() as tempdir: + temp_path = Path(tempdir) + download_loc = self._download_package(package_url, filename, tempdir) + installer = PackageInstaller(download_loc, confirmation_function=self._confirmation_function, package_type="integration") + + def download_platform_package(self, name : str, package_branch : branchtypes = "main", version : str = ""): + #[ ] for platforms, ask to copy files like readme etc. into the current working directory? + filename, package_url = self._make_package_url(name, "platforms", package_branch, version) + + def _make_package_url(self, package_name : str, package_type : Literal["integrations", "platforms"], package_branch: branchtypes, version : str = ''): + """Creates the raw url for a platform or integration package + + Parameters + ---------- + package_name : str + The name of the package + package_type : Literal["integrations", "platforms"] + Type of package, either integration or platform + package_branch : branchtypes + The branch to download from. Not relevant when version is specified + version : str + Specific version to get + + Returns + ------- + tuple[str, str] + The filename and corresponding url of the package + + Raises + ------ + ValueError + _description_ + KeyError + _description_ + ValueError + _description_ + ValueError + _description_ + """ + #create the raw_url for a package + + #"https://github.com/Slalamander/inkBoard-package-index/raw/refs/heads/main/platforms/desktop0.0.2.zip" + if package_branch not in ("main","dev"): + raise ValueError(f"branch must be one of {packagebranchtypes}, you passed {package_branch}") + if not hasattr(self,"index"): self.get_package_index() - if name not in self.index["integrations"]: - raise KeyError(f"Integration {name} is not known in the package index") + if package_name not in self.index[package_type]: + raise KeyError(f"Integration {package_name} is not known in the package index") if version: - ##Handle cases where _dev should be appended to it? - _LOGGER.warning(f"Downloading custom versions is not available (yet). Download of {name} will fail if the version is not the newest one in the
or branch.") - if package_type == "dev" or version.count(".") >= 3: - ##main versions should be 0.2.5 etc. For dev, it may be 0.2.5.dev1 etc. - ##If it's more something went wrong tbf lol - ##Minor versions **should** end up in the main index too. - _LOGGER.debug(f"appending _dev suffix to {name} version {version} for package handling") + _LOGGER.warning(f"Downloading archived versions is not available (yet). Download of {package_name} will fail if the version is not the newest one in the
or branch.") + if version == self.index[package_type][package_name]["main"]["version"]: + fileversion = version + elif version == self.index[package_type][package_name]["dev"]["version"]: fileversion = f"{version}_dev" - raise ValueError("I don't think it should be handled like this for specific versions") - ##Basically: for versions if specified, check if it is the main version -> prefer - ##If dev version -> well you know it's the dev version - ##Otherwise, there will be a dev/main folder in the repo that handles those and which should be set via the package_type honestly? - ##Or should there be a check in the indexer whether a dev version is elligeble? + else: + # _LOGGER.warning(f"Downloading archived versions is not available (yet). Download of {name} will fail if the version is not the newest one in the
or branch.") + _LOGGER.error("Downloading archived components is not available yet.") + raise ValueError("Downloading archived components is not available yet.") + if package_branch == "dev" or version.count(".") >= 3: + ##main versions should be 0.2.5 etc. For dev, it may be 0.2.5.dev1 etc. + ##If it's more something went wrong tbf lol + ##Minor versions **should** end up in the main index too. + _LOGGER.debug(f"appending _dev suffix to {package_name} version {version} for package handling") + fileversion = f"{version}_dev" + raise ValueError("I don't think it should be handled like this for specific versions") + ##Basically: for versions if specified, check if it is the main version -> prefer + ##If dev version -> well you know it's the dev version + ##Otherwise, there will be a dev/main folder in the repo that handles those and which should be set via the package_type honestly? + ##Or should there be a check in the indexer whether a dev version is elligeble? - pass else: - fileversion = self.index["integrations"][name][package_type] - if package_type == "dev": + fileversion = self.index[package_type][package_name][package_branch] + if package_branch == "dev": # version = fileversion = f"{fileversion}_dev" - pass + + filename = f"{package_name}-{fileversion}.zip" + filepath = f"/{package_type}/{package_name}/{filename}" - raw_url = self._make_raw_file_link() + raw_url = self._make_raw_file_link(filepath, package_branch) + return filename, raw_url + + def _download_package(self, raw_url : str, filename : str, dest : Path): + + dest = Path(dest) / filename + loc, _ = self._download_raw_file(raw_url, dest) + return loc + ##From here an installer instance can be created I think? Since that one reidentifies the package anyways - #[ ] for platforms, ask to copy files like readme etc. into the current working directory? - return @classmethod def _download_package_index(cls, *, _destination_file : Union[Path, str] = None): @@ -136,9 +203,9 @@ def _download_package_index(cls, *, _destination_file : Union[Path, str] = None) @staticmethod def _download_raw_file(raw_file_url : str, destination_file : str): - filename, headers = urllib.request.urlretrieve(raw_file_url, filename=destination_file) - _LOGGER.debug(f"Successfully downloaded {filename}") - return filename, headers + path_to_download, headers = urllib.request.urlretrieve(raw_file_url, filename=destination_file) + _LOGGER.debug(f"Successfully downloaded {path_to_download}") + return path_to_download, headers @staticmethod def _make_raw_file_link(file : str, branch = "main", repo_url : str = PACKAGE_INDEX_URL) -> str: diff --git a/inkBoard/packaging/install.py b/inkBoard/packaging/install.py index b4ef82e..80a37a1 100644 --- a/inkBoard/packaging/install.py +++ b/inkBoard/packaging/install.py @@ -5,6 +5,7 @@ Callable, Union, Optional, + Literal ) from abc import abstractmethod from contextlib import suppress @@ -326,13 +327,19 @@ class PackageInstaller(BaseInstaller): Function to call when asking for confirmation, gets passed the question to confirm and the Installer instance., by default None """ - def __init__(self, file: Union[Path,str], skip_confirmations: bool = False, confirmation_function: Callable[[str, 'BaseInstaller'],bool] = None): + def __init__(self, + file: Union[Path,str], + skip_confirmations: bool = False, confirmation_function: Callable[[str, 'BaseInstaller'],bool] = None, + package_type : Literal[None, "integration", "platform", "package", "configuration"] = None, + ): self._file = Path(file) assert self._file.exists(), f"{file} does not exist" self._confirmation_function = confirmation_function self._skip_confirmations = skip_confirmations - if self._file.suffix in CONFIG_FILE_TYPES: + if package_type is not None: + self._package_type = package_type + elif self._file.suffix in CONFIG_FILE_TYPES: self._package_type = "configuration" else: self._package_type: packagetypes = self.identify_zip_file(self._file) @@ -350,6 +357,8 @@ def install(self): self.install_package() elif self._package_type == "configuration": self.install_config_requirements(self._file, self._skip_confirmations, self._confirmation_function) + else: + raise ValueError(f"Unknown package_type given: {self._package_type}") def install_package(self) -> Optional[packagetypes]: """Installs a package type .zip file file @@ -766,7 +775,10 @@ def _path_to_zipfile(file: Union[str, Path]) -> zipfile.ZipFile: return zipfile.ZipFile(file, 'r') class InternalInstaller(BaseInstaller): - "Handles installing requirements of already installed platforms and integrations." + """Handles installing requirements of already installed platforms and integrations. + + I.e. used to call the appropriate `pip install ...` commands + """ def __init__(self, install_type: internalinstalltypes, name: str, skip_confirmations = False, confirmation_function = None): ##May remove the subclassing, but just reuse the usable functions (i.e. seperate out a few funcs.) ##Also, use the constant designer mod in case something is not found internally. diff --git a/inkBoard/packaging/types.py b/inkBoard/packaging/types.py index dd6c9c7..8c6fdec 100644 --- a/inkBoard/packaging/types.py +++ b/inkBoard/packaging/types.py @@ -1,9 +1,17 @@ -from typing import Literal, TypedDict +from typing import Literal, TypedDict, TYPE_CHECKING + +from inkBoard.types import manifestjson, platformjson # noqa: F401 + +if TYPE_CHECKING: + from .version import Version internalinstalltypes = Literal["platform", "integration"] +downloadinstalltypes = Literal["platform", "integration"] packagetypes = Literal['package', 'integration', 'platform'] +branchtypes = Literal["main", "dev"] class PackageDict(TypedDict): + "Dict holding info for a packaged inkBoard configuration" created: str "The date and time the package was created, in isoformat" @@ -11,31 +19,37 @@ class PackageDict(TypedDict): created_with: Literal["inkBoard", "inkBoarddesigner"] "Whether this package was created via inkBoard itself, or via the designer" - versions: dict[Literal["inkBoard", "PythonScreenStackManager", "inkBoarddesigner"],str] + versions: dict[Literal["inkBoard", "PythonScreenStackManager", "inkBoarddesigner"],"Version"] "The versions of the core packages installed when creating it. Designer version is None if not installed" platform: str "The platform the package was created for" +class indexpackagedict(TypedDict): + "Holds information pertaining to a package in the index" + + version : "Version" + "The package's version" + class PackageIndex(TypedDict): "Structure of the index.json file in the package index" - inkBoard : str + inkBoard : dict[branchtypes,"Version"] "inkBoard version the index was made on" - inkBoarddesigner : str + inkBoarddesigner : dict[branchtypes,"Version"] "Version of inkBoard designer the index was made on" - PythonScreenStackManager : str + PythonScreenStackManager : dict[branchtypes,"Version"] "Version of PSSM the index was made on" - timestamp : str + timestamp : dict[branchtypes, str] "ISO format timestamp of when the index file was run" - platforms : dict[str,dict[Literal["main","dev"],str]] + platforms : dict[str,dict[branchtypes,indexpackagedict]] "List of platforms and their versions in the main and dev branch" - integrations : dict[str,dict[Literal["main","dev"],str]] + integrations : dict[str,dict[branchtypes,indexpackagedict]] "List of integrations and their versions in the main and dev branch" comparisonstrings = Literal[ diff --git a/inkBoard/platforms/__init__.py b/inkBoard/platforms/__init__.py index 2d62077..4fb7155 100644 --- a/inkBoard/platforms/__init__.py +++ b/inkBoard/platforms/__init__.py @@ -18,9 +18,13 @@ if TYPE_CHECKING: from inkBoard import config as configuration, CORE as CORE + from . import basedevice _LOGGER = logging.getLogger(__name__) +#[ ]: move this code to a 'device' folder (or basedevice, idk). Platform folder will only hold that +#[ ]: and features can be put in a subfolder such that they are categorised by the feature name. + def get_device(config : "configuration", core: "CORE") -> Device: "Initialises the correct device based on the config." @@ -56,7 +60,7 @@ def get_device(config : "configuration", core: "CORE") -> Device: else: ##Test this again once the platform is installed dev_spec = importlib.util.spec_from_file_location(platform_package, str(platform_path / "__init__.py"), submodule_search_locations=[]) - device_platform: basedevice = importlib.util.module_from_spec(dev_spec) + device_platform: "basedevice" = importlib.util.module_from_spec(dev_spec) sys.modules[platform_package] = device_platform dev_spec.loader.exec_module(device_platform) device_platform = importlib.import_module(".device",platform_package)