Initial commmit of something kinda working
This commit is contained in:
parent
8466e8bc6c
commit
61512de3aa
1
.gitignore
vendored
1
.gitignore
vendored
@ -139,3 +139,4 @@ dmypy.json
|
|||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
tags
|
tags
|
||||||
|
unhacs.txt
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
default_language_version:
|
|
||||||
python: python3.8
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 24.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.4.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
@ -16,11 +14,14 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: name-tests-test
|
- id: name-tests-test
|
||||||
exclude: tests/(common.py|util.py|(helpers|integration/factories)/(.+).py)
|
exclude: tests/(common.py|util.py|(helpers|integration/factories)/(.+).py)
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: v2.4.0
|
rev: 5.13.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: isort
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.800
|
rev: v1.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
|
exclude: docs/
|
||||||
|
additional_dependencies:
|
||||||
|
- types-requests
|
||||||
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "unhacs"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [{name = "Ian Fijolek", email = "ian@iamthefij.com"}]
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"requests"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
unhacs = 'unhacs.main:main'
|
@ -2,3 +2,5 @@
|
|||||||
pytest
|
pytest
|
||||||
coverage
|
coverage
|
||||||
pre-commit
|
pre-commit
|
||||||
|
mypy
|
||||||
|
types-requests
|
||||||
|
45
setup.py
45
setup.py
@ -1,45 +0,0 @@
|
|||||||
from codecs import open
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
from setuptools import find_packages
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
here = path.abspath(path.dirname(__file__))
|
|
||||||
|
|
||||||
# Get the long description from the README file
|
|
||||||
with open(path.join(here, "README.md"), encoding="utf-8") as f:
|
|
||||||
long_description = f.read()
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="unhacs",
|
|
||||||
version="0.0.0",
|
|
||||||
description="A command line alternative to the "Home Assistant Community Store", aka HACS",
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
url="https://git.iamthefij.com/iamthefij/unhacs.git",
|
|
||||||
download_url=("https://git.iamthefij.com/iamthefij/unhacs.git/archive/master.tar.gz"),
|
|
||||||
author="iamthefij",
|
|
||||||
author_email="",
|
|
||||||
classifiers=[
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
],
|
|
||||||
keywords="",
|
|
||||||
packages=find_packages(
|
|
||||||
exclude=[
|
|
||||||
"contrib",
|
|
||||||
"docs",
|
|
||||||
"examples",
|
|
||||||
"scripts",
|
|
||||||
"tests",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
install_requires=[],
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": [
|
|
||||||
"unhacs=unhacs:main",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
0
unhacs/__init__.py
Normal file
0
unhacs/__init__.py
Normal file
4
unhacs/__main__.py
Normal file
4
unhacs/__main__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from unhacs.main import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
206
unhacs/main.py
Normal file
206
unhacs/main.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from unhacs.packages import DEFAULT_PACKAGE_FILE
|
||||||
|
from unhacs.packages import Package
|
||||||
|
from unhacs.packages import read_packages
|
||||||
|
from unhacs.packages import write_packages
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_HASS_CONFIG_PATH = Path(".")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_zip(zip_file: ZipFile, dest_dir: Path):
|
||||||
|
for info in zip_file.infolist():
|
||||||
|
if info.is_dir():
|
||||||
|
continue
|
||||||
|
file = Path(info.filename)
|
||||||
|
# Strip top directory from path
|
||||||
|
file = Path(*file.parts[1:])
|
||||||
|
path = dest_dir / file
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zip_file.open(info) as source, open(path, "wb") as dest:
|
||||||
|
dest.write(source.read())
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser():
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
"-c",
|
||||||
|
type=Path,
|
||||||
|
default=DEFAULT_HASS_CONFIG_PATH,
|
||||||
|
help="The path to the Home Assistant configuration directory.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--package-file",
|
||||||
|
"-p",
|
||||||
|
type=Path,
|
||||||
|
default=DEFAULT_PACKAGE_FILE,
|
||||||
|
help="The path to the package file.",
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
||||||
|
|
||||||
|
list_parser = subparsers.add_parser("list")
|
||||||
|
list_parser.add_argument("--verbose", "-v", action="store_true")
|
||||||
|
|
||||||
|
add_parser = subparsers.add_parser("add")
|
||||||
|
add_parser.add_argument("url", type=str, help="The URL of the package.")
|
||||||
|
add_parser.add_argument(
|
||||||
|
"name", type=str, nargs="?", help="The name of the package."
|
||||||
|
)
|
||||||
|
add_parser.add_argument(
|
||||||
|
"--version", "-v", type=str, help="The version of the package."
|
||||||
|
)
|
||||||
|
add_parser.add_argument(
|
||||||
|
"--update",
|
||||||
|
"-u",
|
||||||
|
action="store_true",
|
||||||
|
help="Update the package if it already exists.",
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_parser = subparsers.add_parser("remove")
|
||||||
|
remove_parser.add_argument("packages", nargs="*")
|
||||||
|
|
||||||
|
update_parser = subparsers.add_parser("update")
|
||||||
|
update_parser.add_argument("packages", nargs="*")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
class Unhacs:
|
||||||
|
def add_package(
|
||||||
|
self,
|
||||||
|
package_url: str,
|
||||||
|
package_name: str | None = None,
|
||||||
|
version: str | None = None,
|
||||||
|
update: bool = False,
|
||||||
|
):
|
||||||
|
# Parse the package URL to get the owner and repo name
|
||||||
|
parts = package_url.split("/")
|
||||||
|
owner = parts[-2]
|
||||||
|
repo = parts[-1]
|
||||||
|
|
||||||
|
# Fetch the releases from the GitHub API
|
||||||
|
response = requests.get(f"https://api.github.com/repos/{owner}/{repo}/releases")
|
||||||
|
response.raise_for_status()
|
||||||
|
releases = response.json()
|
||||||
|
|
||||||
|
# If a version is provided, check if it exists in the releases
|
||||||
|
if version:
|
||||||
|
for release in releases:
|
||||||
|
if release["tag_name"] == version:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Version {version} does not exist for this package")
|
||||||
|
else:
|
||||||
|
# If no version is provided, use the latest release
|
||||||
|
version = releases[0]["tag_name"]
|
||||||
|
|
||||||
|
if not version:
|
||||||
|
raise ValueError("No releases found for this package")
|
||||||
|
|
||||||
|
package = Package(name=package_name or repo, url=package_url, version=version)
|
||||||
|
packages = read_packages()
|
||||||
|
|
||||||
|
# Raise an error if the package is already in the list
|
||||||
|
if package in packages:
|
||||||
|
if update:
|
||||||
|
# Remove old version of the package
|
||||||
|
packages = [p for p in packages if p.url != package_url]
|
||||||
|
else:
|
||||||
|
raise ValueError("Package already exists in the list")
|
||||||
|
|
||||||
|
packages.append(package)
|
||||||
|
write_packages(packages)
|
||||||
|
|
||||||
|
self.download_package(package)
|
||||||
|
|
||||||
|
def download_package(self, package: Package, replace: bool = True):
|
||||||
|
# Parse the package URL to get the owner and repo name
|
||||||
|
parts = package.url.split("/")
|
||||||
|
owner = parts[-2]
|
||||||
|
repo = parts[-1]
|
||||||
|
|
||||||
|
# Fetch the releases from the GitHub API
|
||||||
|
response = requests.get(f"https://api.github.com/repos/{owner}/{repo}/releases")
|
||||||
|
response.raise_for_status()
|
||||||
|
releases = response.json()
|
||||||
|
|
||||||
|
# Find the release with the specified version
|
||||||
|
for release in releases:
|
||||||
|
if release["tag_name"] == package.version:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Version {package.version} not found for this package")
|
||||||
|
|
||||||
|
# Download the release zip with the specified name
|
||||||
|
response = requests.get(release["zipball_url"])
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
release_zip = ZipFile(BytesIO(response.content))
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="unhacs-") as tempdir:
|
||||||
|
tmpdir = Path(tempdir)
|
||||||
|
extract_zip(release_zip, tmpdir)
|
||||||
|
|
||||||
|
for file in tmpdir.glob("*"):
|
||||||
|
print(file)
|
||||||
|
hacs = json.loads((tmpdir / "hacs.json").read_text())
|
||||||
|
print(hacs)
|
||||||
|
|
||||||
|
for custom_component in tmpdir.glob("custom_components/*"):
|
||||||
|
dest = (
|
||||||
|
DEFAULT_HASS_CONFIG_PATH
|
||||||
|
/ "custom_components"
|
||||||
|
/ custom_component.name
|
||||||
|
)
|
||||||
|
if replace:
|
||||||
|
shutil.rmtree(dest, ignore_errors=True)
|
||||||
|
|
||||||
|
shutil.move(custom_component, dest)
|
||||||
|
|
||||||
|
def update_packages(self, package_names: list[str]):
|
||||||
|
if not package_names:
|
||||||
|
package_urls = [p.url for p in read_packages()]
|
||||||
|
else:
|
||||||
|
package_urls = [p.url for p in read_packages() if p.name in package_names]
|
||||||
|
|
||||||
|
for package in package_urls:
|
||||||
|
print("Updating", package)
|
||||||
|
self.add_package(package, update=True)
|
||||||
|
|
||||||
|
def list_packages(self, verbose: bool = False):
|
||||||
|
for package in read_packages():
|
||||||
|
print(package.verbose_str() if verbose else str(package))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# If the sub command is add package, it should pass the parsed arguments to the add_package function and return
|
||||||
|
parser = create_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
unhacs = Unhacs()
|
||||||
|
|
||||||
|
if args.subcommand == "add":
|
||||||
|
unhacs.add_package(args.url, args.name, args.version, args.update)
|
||||||
|
elif args.subcommand == "list":
|
||||||
|
unhacs.list_packages(args.verbose)
|
||||||
|
elif args.subcommand == "remove":
|
||||||
|
print("Not implemented")
|
||||||
|
elif args.subcommand == "update":
|
||||||
|
unhacs.update_packages(args.packages)
|
||||||
|
else:
|
||||||
|
print("Not implemented")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
34
unhacs/packages.py
Normal file
34
unhacs/packages.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_PACKAGE_FILE = "unhacs.txt"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Package:
|
||||||
|
url: str
|
||||||
|
version: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} {self.version}"
|
||||||
|
|
||||||
|
def verbose_str(self):
|
||||||
|
return f"{self.name} {self.version} ({self.url})"
|
||||||
|
|
||||||
|
|
||||||
|
# Read a list of Packages from a text file in the plain text format "URL version name"
|
||||||
|
def read_packages(package_file: str = DEFAULT_PACKAGE_FILE) -> list[Package]:
|
||||||
|
path = Path(package_file)
|
||||||
|
if path.exists():
|
||||||
|
with path.open() as f:
|
||||||
|
return [Package(*line.strip().split()) for line in f]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# Write a list of Packages to a text file in the format URL version name
|
||||||
|
def write_packages(packages: list[Package], package_file: str = DEFAULT_PACKAGE_FILE):
|
||||||
|
with open(package_file, "w") as f:
|
||||||
|
for package in packages:
|
||||||
|
f.write(f"{package.url} {package.version} {package.name}\n")
|
Loading…
Reference in New Issue
Block a user