Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions inkBoard/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
###Inkboard

__version__ = "0.3.2.dev1"
"inkBoard version"

import logging as __logging

from types import MappingProxyType
Expand All @@ -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.
Expand Down
40 changes: 30 additions & 10 deletions inkBoard/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
19 changes: 12 additions & 7 deletions inkBoard/packaging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand All @@ -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

129 changes: 98 additions & 31 deletions inkBoard/packaging/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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 <main> or <dev> 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 <main> or <dev> 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 <main> or <dev> 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):
Expand All @@ -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:
Expand Down
18 changes: 15 additions & 3 deletions inkBoard/packaging/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Callable,
Union,
Optional,
Literal
)
from abc import abstractmethod
from contextlib import suppress
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading