From 36095d8cd2bc25921f8fd5c6f0f08c60caca9512 Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 27 Jul 2025 11:48:13 +0200 Subject: [PATCH 1/8] move version argument --- inkBoard/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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. From 83b99d8f3f74835d3927827f0abde5f85bbbb433 Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 27 Jul 2025 11:48:21 +0200 Subject: [PATCH 2/8] add some todos --- inkBoard/platforms/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) From f603a2437268412f5859aa71dfcfe73bc495a792 Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 27 Jul 2025 17:03:29 +0200 Subject: [PATCH 3/8] =?UTF-8?q?style(packaging):=20=F0=9F=8F=B7=EF=B8=8F?= =?UTF-8?q?=20Improve=20typehinting=20and=20checking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inkBoard/packaging/install.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/inkBoard/packaging/install.py b/inkBoard/packaging/install.py index b4ef82e..ac232a9 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 From 57ec40b615aa44580bfbea1134c5251d655ff42c Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 27 Jul 2025 17:04:26 +0200 Subject: [PATCH 4/8] =?UTF-8?q?feat(packaging):=20=F0=9F=8F=B7=EF=B8=8F=20?= =?UTF-8?q?Add=20typeddict=20for=20index=20entry=20and=20improve=20typehin?= =?UTF-8?q?ting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inkBoard/packaging/types.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/inkBoard/packaging/types.py b/inkBoard/packaging/types.py index dd6c9c7..58f8079 100644 --- a/inkBoard/packaging/types.py +++ b/inkBoard/packaging/types.py @@ -1,9 +1,16 @@ -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"] 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 +18,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[ From 9baf5c40e49a0808d9e47eb98dba8dfcaab7f69f Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 27 Jul 2025 17:05:39 +0200 Subject: [PATCH 5/8] =?UTF-8?q?style(packaging):=20=F0=9F=8E=A8=20Better?= =?UTF-8?q?=20code=20setup=20and=20start=20code=20for=20downloading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inkBoard/packaging/download.py | 123 +++++++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 30 deletions(-) diff --git a/inkBoard/packaging/download.py b/inkBoard/packaging/download.py index b863d51..3c771aa 100644 --- a/inkBoard/packaging/download.py +++ b/inkBoard/packaging/download.py @@ -4,19 +4,22 @@ 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 .types import ( - PackageIndex + PackageIndex, + branchtypes ) from .constants import ( PACKAGE_INDEX_URL, @@ -54,6 +57,9 @@ class Downloader: def __init__(self, destination_folder : Path): print(self._index_downloaded) self.destination_folder = destination_folder + + #[ ]: Implement confirmation_function + def get_package_index(self, force_get = False) -> dict: @@ -84,44 +90,101 @@ 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) + + 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 +199,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: From 591e58f64bf6224157fd73630167e0013ca345a1 Mon Sep 17 00:00:00 2001 From: Sander Date: Sun, 27 Jul 2025 20:00:41 +0200 Subject: [PATCH 6/8] Integrating downloader --- inkBoard/packaging/__init__.py | 19 ++++++++++++------- inkBoard/packaging/download.py | 5 ++++- inkBoard/packaging/install.py | 5 ++++- inkBoard/packaging/types.py | 1 + 4 files changed, 21 insertions(+), 9 deletions(-) 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 3c771aa..0220afe 100644 --- a/inkBoard/packaging/download.py +++ b/inkBoard/packaging/download.py @@ -17,6 +17,7 @@ from inkBoard import logging +from .install import PackageInstaller from .types import ( PackageIndex, branchtypes @@ -54,9 +55,10 @@ 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 @@ -96,6 +98,7 @@ def download_integration_package(self, name : str, package_branch : branchtypes 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? diff --git a/inkBoard/packaging/install.py b/inkBoard/packaging/install.py index ac232a9..80a37a1 100644 --- a/inkBoard/packaging/install.py +++ b/inkBoard/packaging/install.py @@ -775,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 58f8079..8c6fdec 100644 --- a/inkBoard/packaging/types.py +++ b/inkBoard/packaging/types.py @@ -6,6 +6,7 @@ from .version import Version internalinstalltypes = Literal["platform", "integration"] +downloadinstalltypes = Literal["platform", "integration"] packagetypes = Literal['package', 'integration', 'platform'] branchtypes = Literal["main", "dev"] From 55bf28462474dbcd9fef6c9305c0247f22efd7ee Mon Sep 17 00:00:00 2001 From: Sander Date: Wed, 30 Jul 2025 20:04:35 +0200 Subject: [PATCH 7/8] =?UTF-8?q?feat(packaging):=20=E2=9C=A8=20Implement=20?= =?UTF-8?q?improved=20command=20line=20arguments=20for=20packaging=20and?= =?UTF-8?q?=20downloading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inkBoard/arguments.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) 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') From cb9044bb80c76067485fc3d1a200f19c8c94769a Mon Sep 17 00:00:00 2001 From: Sander Date: Wed, 30 Jul 2025 20:04:46 +0200 Subject: [PATCH 8/8] add todo comment --- inkBoard/packaging/download.py | 1 + 1 file changed, 1 insertion(+) diff --git a/inkBoard/packaging/download.py b/inkBoard/packaging/download.py index 0220afe..e634f09 100644 --- a/inkBoard/packaging/download.py +++ b/inkBoard/packaging/download.py @@ -61,6 +61,7 @@ def __init__(self, destination_folder : Path, confirmation_function = None): self._confirmation_function = confirmation_function #[ ]: Implement confirmation_function + #[ ]: parse version from the string def get_package_index(self, force_get = False) -> dict: