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
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 1.1.0 (2025-10-21)

- Add package name autocomplete for `dexi remove` and `dexi update` ([#8](<https://github.com/Dotsian/DexI/pull/8>))
- Use pathlib for path manipulation ([#7](<https://github.com/Dotsian/DexI/pull/7>))
- Stylize error messages ([#9](<https://github.com/Dotsian/DexI/pull/9>))
- Replace default installation method with uv
- Fix error when installing a package that omits the `exclude` attribute
- Fix missing error checking for `dexi update`

### Contributors

- [@dormieriancitizen](<https://github.com/dormieriancitizen>)
- [@ethanthopkins](<https://github.com/ethanthopkins>)

## 1.0.0 (2025-10-08)

- Released DexI version 1.0.0
- Release DexI version 1.0.0

14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@

[![CI](https://github.com/Dotsian/DexI/actions/workflows/CI.yml/badge.svg)](https://github.com/Dotsian/DexI/actions/workflows/CI.yml)
[![Issues](https://img.shields.io/github/issues/Dotsian/DexI)](https://github.com/Dotsian/DexI/issues)
[![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/Dotsian/DexI/blob/master/CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.1.0-blue)](https://github.com/Dotsian/DexI/blob/master/CHANGELOG.md)

Dex Inventory "DexI" is a Ballsdex package manager developed by DotZZ that provides developers with package control and easily allows users to add, remove, install, and update third-party packages.

_**App support will only be available after Ballsdex v2.29.4**_
_**Packages with Django apps can only be downloaded from Ballsdex v2.29.5+**_

## DexI vs. the Traditional Method

Using DexI over the traditional method for package management is far better, as it automatically handles tasks and allows developers to have more control over their packages. Additionally, if a package was updated, DexI will detect the package update. Users can then easily update that package with a single command.

## Installation

You can install and update DexI using [pip](https://www.python.org/downloads/):
You can install DexI using [uv](https://docs.astral.sh/uv/getting-started/installation/):

```bash
pip install git+https://github.com/Dotsian/DexI.git
uv tool install git+https://github.com/Dotsian/DexI
```

Updating DexI with uv:

```bash
uv tool upgrade dexi
```

## Usage
Expand Down
2 changes: 1 addition & 1 deletion dexi/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.1.0"
24 changes: 20 additions & 4 deletions dexi/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typer
from typing_extensions import Annotated

from .commands.installer import install_packages
from .commands.manager import (
Expand All @@ -7,7 +8,7 @@
update_all_packages,
update_package,
)
from .commands.viewer import list_packages
from .commands.viewer import autocomplete_packages, list_packages
from .core.errors import Errors

app = typer.Typer()
Expand All @@ -31,7 +32,14 @@ def add(package: str, branch: str = "main"):


@app.command()
def remove(package: str):
def remove(
package: Annotated[
str,
typer.Argument(
help="The name of the package", autocompletion=autocomplete_packages
),
],
):
"""
Removes and uninstalls a package.

Expand All @@ -46,7 +54,15 @@ def remove(package: str):


@app.command()
def update(package: str | None = None):
def update(
package: Annotated[
str,
typer.Argument(
help="The name of the package", autocompletion=autocomplete_packages
),
]
| None = None,
):
"""
Updates all packages or a specified package.

Expand All @@ -56,7 +72,7 @@ def update(package: str | None = None):
The package you want to update.
Automatically updates all packages if not specified.
"""
Errors(["invalid_project", "invalid_version", "no_config_found"]).check
Errors(["invalid_project", "invalid_version", "no_config_found"]).check()

if package is None:
update_all_packages()
Expand Down
53 changes: 28 additions & 25 deletions dexi/commands/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import random
import shutil
import zipfile
from pathlib import Path
from typing import cast

import requests
Expand Down Expand Up @@ -42,17 +43,17 @@ def uninstall_package(package: str):
found_package = fetch_package(package, dexi_tool["packages"])

if found_package is None:
error(f"Could not find '{package}' package")
error(f"Could not find [red]'{package}'[/red] package")
return

data = Package.from_git(found_package["git"], found_package["branch"])

if data.app is not None and not app_operations_supported():
return

desination = f"{os.getcwd()}/ballsdex/packages/{data.package.target}"
destination = Path.cwd() / "ballsdex" / "packages" / data.package.target

if not os.path.isdir(desination):
if not destination.is_dir():
return

if data.app is not None:
Expand All @@ -65,7 +66,7 @@ def uninstall_package(package: str):

remove_list_entry("packages", f"ballsdex.packages.{data.package.target}")

shutil.rmtree(desination)
shutil.rmtree(destination)


def install_package(
Expand Down Expand Up @@ -93,48 +94,45 @@ def install_package(
author, repository = repository.split("/")

zip_url = f"https://github.com/{author}/{repository}/archive/refs/heads/{branch}.zip"
desination = f"{os.getcwd()}/ballsdex/packages/{data.package.target}"
destination = Path.cwd() / "ballsdex" / "packages" / data.package.target

name = package_name(repository, branch)

if os.path.isdir(desination):
if destination.is_dir():
if cancel_if_exists:
return False

replaced = True
shutil.rmtree(desination)
shutil.rmtree(destination)

if data.app is not None:
if not app_operations_supported():
error(
f"DexI packages with Django apps are not supported "
f"on Ballsdex v$BD_V, please update to v{SUPPORTED_APP_VERSION}+"
f"[red]DexI packages[/red] with Django apps are not supported on "
f"red]Ballsdex v$BD_V[/red], please update to v{SUPPORTED_APP_VERSION}+"
)

app_desination = f"{os.getcwd()}/admin_panel/{data.app.target}"
app_destination = Path.cwd() / "admin_panel" / data.app.target

if os.path.isdir(app_desination):
if app_destination.is_dir():
replaced = True
shutil.rmtree(app_desination)
shutil.rmtree(app_destination)

os.makedirs(app_desination, exist_ok=True)
app_destination.mkdir(parents=True, exist_ok=True)

os.makedirs(desination, exist_ok=True)
destination.mkdir(parents=True, exist_ok=True)

response = requests.get(zip_url)

if not response.ok:
error(f"Failed to fetch {name}")
error(f"Failed to fetch [red]{name}[/red]")

with zipfile.ZipFile(io.BytesIO(response.content)) as z:
base_folder = f"{repository}-{branch}/"

for member in z.namelist():
if member[-7:] in ["LICENSE", "LICENCE"]:
with (
z.open(member) as src,
open(f"{desination}/{member[-7:]}", "wb") as dst,
):
with z.open(member) as src, (destination / member[-7:]).open("wb") as dst:
shutil.copyfileobj(src, dst)

continue
Expand All @@ -147,7 +145,7 @@ def install_package(
if not relative_path or relative_path in data.package.exclude:
continue

target_path = os.path.join(desination, relative_path)
target_path = destination / relative_path

if member.endswith("/"):
os.makedirs(target_path, exist_ok=True)
Expand All @@ -159,7 +157,12 @@ def install_package(
shutil.copyfileobj(src, dst)

if data.app is not None: # I'll refactor this later
app_desination = cast(str, app_desination) # type: ignore
if not app_destination:
# something has gone remarkably wrong
# (shouldnt be possible)
raise Exception(
"Somehow app_destination has gone missing while copying files"
)

for member in z.namelist():
if not member.startswith(f"{base_folder}{data.app.source}/"):
Expand All @@ -170,15 +173,15 @@ def install_package(
if not relative_path:
continue

target_path = os.path.join(app_desination, relative_path)
target_path = app_destination / relative_path

if member.endswith("/"):
os.makedirs(target_path, exist_ok=True)
target_path.mkdir(parents=True, exist_ok=True)
continue

os.makedirs(os.path.dirname(target_path), exist_ok=True)
target_path.parent.mkdir(parents=True, exist_ok=True)

with z.open(member) as src, open(target_path, "wb") as dst:
with z.open(member) as src, target_path.open("wb") as dst:
shutil.copyfileobj(src, dst)

add_list_entry(
Expand Down
14 changes: 7 additions & 7 deletions dexi/commands/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ def add_package(package: str, branch: str):

if installed_version not in specifier:
error(
f"Ballsdex version requirement for '{package}' is set to "
f"'{data.ballsdex_version}', while this instance is on "
f"version '{ballsdex}'"
f"Ballsdex version requirement for [red]'{package}'[/red] is set to "
f"[red]'{data.ballsdex_version}'[/red], while this instance is on "
f"version [red]'{ballsdex}'[/red]"
)

project = parse_pyproject()
Expand All @@ -58,7 +58,7 @@ def add_package(package: str, branch: str):
dexi = tool.setdefault("dexi", table())

if not initialized and fetch_package(package, fetch_all_packages()) is not None:
error("This package has already been added")
error("This [red]package[/red] has already been added")

package_array = dexi.setdefault("packages", array().multiline(True))

Expand Down Expand Up @@ -113,7 +113,7 @@ def remove_package(package: str):
package_entry = fetch_package(package, dexi_tool["packages"])

if package_entry is None:
error(f"Could not find '{package}' package")
error(f"Could not find [red]'{package}'[/red] package")
return

uninstall_package(package)
Expand Down Expand Up @@ -176,12 +176,12 @@ def update_package(package: str | PackageEntry):
dexi_project = parse_pyproject()

if "tool" not in dexi_project or "dexi" not in dexi_project["tool"]: # type: ignore
error("pyproject.toml contains invalid DexI data")
error("[red]pyproject.toml[/red] contains invalid [red]DexI data[/red]")

dexi_tool = dexi_project["tool"]["dexi"] # type: ignore

if "packages" not in dexi_tool: # type: ignore
error("pyproject.toml contains invalid DexI data")
error("[[red]pyproject.toml[/red] contains invalid [red]DexI data[/red]")

packages = dexi_tool["packages"] # type: ignore

Expand Down
8 changes: 8 additions & 0 deletions dexi/commands/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
from ..core.utils import console, fetch_all_packages, fetch_pyproject, package_name


def autocomplete_packages(incomplete: str) -> list[str]:
return [
package["git"]
for package in fetch_all_packages()
if package["git"].startswith(incomplete)
]


def list_packages(hide_update: bool = False):
"""
Displays a list of packages.
Expand Down
12 changes: 6 additions & 6 deletions dexi/core/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from dataclasses import dataclass, field
from pathlib import Path

from packaging.version import parse as parse_version

Expand All @@ -18,25 +18,25 @@ class Errors:

@staticmethod
def invalid_project() -> None:
if os.path.isdir("ballsdex") and os.path.isfile("pyproject.toml"):
if Path("ballsdex").is_dir() and Path("pyproject.toml").is_file():
return

error("Attempted to use DexI command on an invalid project")
error("Attempted to use [red]DexI[/red] command on an [red]invalid project[/red]")

@staticmethod
def no_config_found() -> None:
if os.path.isfile("config.yml"):
if Path("config.yml").is_file():
return

error("No 'config.yml' file detected")
error("No [red]'config.yml'[/red] file detected")

@staticmethod
def invalid_version() -> None:
if parse_version(fetch_ballsdex_version()) >= parse_version(SUPPORTED_VERSION):
return

error(
"DexI does not support Ballsdex v$BD_V, please update to "
"DexI does not support [red]Ballsdex v$BD_V[/red], please update to "
f"v{SUPPORTED_VERSION}+"
)

Expand Down
11 changes: 7 additions & 4 deletions dexi/core/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,25 @@ class Package:
def from_git(cls, package: str, branch: str) -> Self:
if package.count("/") != 1:
error(
"Invalid GitHub repository identifier entered; Expected <name/repository>"
"Invalid GitHub repository identifier entered; "
"Expected [red]<name/repository>[/red]"
)

data = fetch_pyproject(package, branch)

if not data or "tool" not in data or "dexi" not in data["tool"]:
error(f"Could not locate {package_name(package, branch)}")
error(f"Could not locate [red]{package_name(package, branch)}[/red]")

dexi_tool = data["tool"]["dexi"]
dexi_package = dexi_tool["package"]

if not dexi_tool.get("public", False):
error(f"Could not locate {package_name(package, branch)}")
error(f"Could not locate [red]{package_name(package, branch)}[/red]")

package_config = PackageConfig(
dexi_package["source"], dexi_package["target"], dexi_package.get("exclude")
dexi_package["source"],
dexi_package["target"],
dexi_package.get("exclude", [])
)

fields = {
Expand Down
Loading
Loading