feat: add linting and testing commands to Makefile; enhance CLI with new features and tests
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.PHONY: setup run build smoke clean
|
||||
.PHONY: setup run build smoke clean lint test
|
||||
|
||||
setup:
|
||||
uv venv
|
||||
@@ -7,6 +7,17 @@ setup:
|
||||
run:
|
||||
uv run python scripts/planka_cli.py status
|
||||
|
||||
lint:
|
||||
uv run ruff check scripts/ tests/
|
||||
uv run ruff format --check scripts/ tests/
|
||||
|
||||
lint-fix:
|
||||
uv run ruff check --fix scripts/ tests/
|
||||
uv run ruff format scripts/ tests/
|
||||
|
||||
test:
|
||||
uv run pytest tests/ -v
|
||||
|
||||
build:
|
||||
uv run pyinstaller planka-cli.spec --noconfirm
|
||||
|
||||
@@ -21,4 +32,4 @@ smoke: build
|
||||
./dist/planka-cli --help
|
||||
|
||||
clean:
|
||||
rm -rf dist build __pycache__ scripts/__pycache__
|
||||
rm -rf dist build __pycache__ scripts/__pycache__ tests/__pycache__ .pytest_cache
|
||||
|
||||
@@ -20,7 +20,21 @@ planka-cli = "scripts.planka_cli:app"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyinstaller",
|
||||
"ruff",
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "W"]
|
||||
ignore = ["E501"] # Line too long - handled by formatter
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["scripts"]
|
||||
|
||||
+102
-18
@@ -13,6 +13,7 @@ from rich.console import Console
|
||||
from rich.table import Table
|
||||
from typer.core import TyperGroup
|
||||
|
||||
|
||||
def get_binary_dir() -> Path:
|
||||
argv0 = sys.argv[0]
|
||||
path = Path(argv0)
|
||||
@@ -30,6 +31,7 @@ ENV_PATH = get_binary_dir() / ".env"
|
||||
# Load environment variables from the .env next to the binary/script.
|
||||
load_dotenv(dotenv_path=ENV_PATH)
|
||||
|
||||
|
||||
class HelpOnUnknownCommandGroup(TyperGroup):
|
||||
def get_command(self, ctx: click.Context, cmd_name: str):
|
||||
command = super().get_command(ctx, cmd_name)
|
||||
@@ -52,12 +54,14 @@ app.add_typer(lists_app, name="lists")
|
||||
app.add_typer(cards_app, name="cards")
|
||||
app.add_typer(notifications_app, name="notifications")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main(ctx: typer.Context):
|
||||
"""Planka CLI."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(ctx.get_help())
|
||||
|
||||
|
||||
def get_env_config() -> tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
return (
|
||||
os.getenv("PLANKA_URL"),
|
||||
@@ -65,6 +69,7 @@ def get_env_config() -> tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
os.getenv("PLANKA_PASSWORD"),
|
||||
)
|
||||
|
||||
|
||||
def parse_iso_datetime(value: Optional[str]) -> Optional[datetime]:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -78,6 +83,7 @@ def parse_iso_datetime(value: Optional[str]) -> Optional[datetime]:
|
||||
"Invalid datetime. Use ISO-8601 like 2025-01-31 or 2025-01-31T10:30:00Z."
|
||||
) from exc
|
||||
|
||||
|
||||
def parse_position(value: Optional[str]) -> Optional[Union[str, int]]:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -89,6 +95,7 @@ def parse_position(value: Optional[str]) -> Optional[Union[str, int]]:
|
||||
except ValueError as exc:
|
||||
raise typer.BadParameter("Position must be 'top', 'bottom', or an integer.") from exc
|
||||
|
||||
|
||||
def find_list(planka: Planka, list_id: str):
|
||||
for project in planka.projects:
|
||||
for board in project.boards:
|
||||
@@ -97,6 +104,17 @@ def find_list(planka: Planka, list_id: str):
|
||||
return list_item
|
||||
return None
|
||||
|
||||
|
||||
def find_list_with_board(planka: Planka, list_id: str):
|
||||
"""Find a list and return both the list and its parent board."""
|
||||
for project in planka.projects:
|
||||
for board in project.boards:
|
||||
for list_item in board.lists:
|
||||
if list_item.id == list_id:
|
||||
return list_item, board
|
||||
return None, None
|
||||
|
||||
|
||||
def get_card_by_id(planka: Planka, card_id: str) -> Optional[Card]:
|
||||
try:
|
||||
card_data = planka.endpoints.getCard(card_id)["item"]
|
||||
@@ -104,6 +122,7 @@ def get_card_by_id(planka: Planka, card_id: str) -> Optional[Card]:
|
||||
return None
|
||||
return Card(card_data, planka)
|
||||
|
||||
|
||||
def make_table(title: str) -> Table:
|
||||
return Table(
|
||||
title=title,
|
||||
@@ -114,6 +133,7 @@ def make_table(title: str) -> Table:
|
||||
box=None,
|
||||
)
|
||||
|
||||
|
||||
def render_notifications(title: str, notifications: list) -> None:
|
||||
if not notifications:
|
||||
console.print("No notifications found.")
|
||||
@@ -138,6 +158,7 @@ def render_notifications(title: str, notifications: list) -> None:
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
def get_planka() -> Planka:
|
||||
planka_url, planka_username, planka_password = get_env_config()
|
||||
if not planka_url or not planka_username or not planka_password:
|
||||
@@ -148,7 +169,7 @@ def get_planka() -> Planka:
|
||||
f"to write {ENV_PATH}."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
planka = Planka(planka_url)
|
||||
planka.login(username=planka_username, password=planka_password)
|
||||
@@ -157,6 +178,7 @@ def get_planka() -> Planka:
|
||||
console.print(f"[bold red]Connection Error:[/bold red] {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def login(
|
||||
url: str = typer.Option(..., "--url", "-u", help="Planka base URL"),
|
||||
@@ -180,6 +202,7 @@ def login(
|
||||
|
||||
console.print(f"[green]Saved credentials to[/green] {ENV_PATH}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def logout():
|
||||
"""Delete the stored .env file next to the binary."""
|
||||
@@ -195,19 +218,23 @@ def logout():
|
||||
|
||||
console.print("[green]Logged out.[/green] Removed stored credentials.")
|
||||
|
||||
|
||||
@app.command()
|
||||
def status():
|
||||
"""Check connection and print current user info."""
|
||||
planka = get_planka()
|
||||
try:
|
||||
user = planka.me
|
||||
console.print(f"[green]Connected![/green] Logged in as: [bold]{user.username}[/bold] (ID: {user.id})")
|
||||
console.print(
|
||||
f"[green]Connected![/green] Logged in as: [bold]{user.username}[/bold] (ID: {user.id})"
|
||||
)
|
||||
if user.name:
|
||||
console.print(f"Name: {user.name}")
|
||||
console.print(f"Email: {user.email}")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error fetching status:[/bold red] {e}")
|
||||
|
||||
|
||||
@projects_app.command("list")
|
||||
def list_projects():
|
||||
"""List all projects."""
|
||||
@@ -230,8 +257,11 @@ def list_projects():
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@boards_app.command("list")
|
||||
def list_boards(project_id: Optional[str] = typer.Argument(None, help="Project ID to filter by")):
|
||||
def list_boards(
|
||||
project_id: Optional[str] = typer.Argument(None, help="Project ID to filter by"),
|
||||
):
|
||||
"""List boards. Optionally filter by Project ID."""
|
||||
planka = get_planka()
|
||||
try:
|
||||
@@ -255,19 +285,25 @@ def list_boards(project_id: Optional[str] = typer.Argument(None, help="Project I
|
||||
console.print("No boards found.")
|
||||
return
|
||||
|
||||
planka_url, _, _ = get_env_config()
|
||||
table = make_table(title)
|
||||
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
||||
table.add_column("Name", style="magenta")
|
||||
table.add_column("Project ID", justify="right")
|
||||
table.add_column("URL", style="magenta")
|
||||
|
||||
for b in boards_list:
|
||||
project_id_value = b.schema.get("projectId")
|
||||
table.add_row(str(b.id), b.name, str(project_id_value))
|
||||
board_url = None
|
||||
if planka_url and b.id:
|
||||
board_url = f"{planka_url.rstrip('/')}/boards/{b.id}"
|
||||
table.add_row(str(b.id), b.name, str(project_id_value), str(board_url or "-"))
|
||||
|
||||
console.print(table)
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@lists_app.command("list")
|
||||
def list_lists(board_id: str):
|
||||
"""List all lists in a board."""
|
||||
@@ -283,7 +319,7 @@ def list_lists(board_id: str):
|
||||
break
|
||||
if target_board:
|
||||
break
|
||||
|
||||
|
||||
if not target_board:
|
||||
console.print(f"[red]Board {board_id} not found.[/red]")
|
||||
return
|
||||
@@ -291,40 +327,52 @@ def list_lists(board_id: str):
|
||||
table = make_table(f"Lists in Board: {target_board.name}")
|
||||
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
||||
table.add_column("Name", style="magenta")
|
||||
table.add_column("Board ID", justify="right")
|
||||
table.add_column("Position", justify="right")
|
||||
|
||||
for l in target_board.lists:
|
||||
table.add_row(str(l.id), l.name, str(l.position))
|
||||
for list_item in target_board.lists:
|
||||
table.add_row(
|
||||
str(list_item.id), list_item.name, str(target_board.id), str(list_item.position)
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@cards_app.command("list")
|
||||
def list_cards(list_id: str):
|
||||
"""List all cards in a list."""
|
||||
planka = get_planka()
|
||||
try:
|
||||
target_list = find_list(planka, list_id)
|
||||
|
||||
target_list, target_board = find_list_with_board(planka, list_id)
|
||||
|
||||
if not target_list:
|
||||
console.print(f"[red]List {list_id} not found.[/red]")
|
||||
return
|
||||
|
||||
planka_url, _, _ = get_env_config()
|
||||
board_id = target_board.id if target_board else None
|
||||
table = make_table(f"Cards in List: {target_list.name}")
|
||||
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
||||
table.add_column("Name", style="magenta")
|
||||
table.add_column("List ID", justify="right")
|
||||
table.add_column("Position", justify="right")
|
||||
table.add_column("URL", style="magenta")
|
||||
|
||||
for c in target_list.cards:
|
||||
table.add_row(str(c.id), c.name, str(c.position))
|
||||
card_url = None
|
||||
if planka_url and board_id and c.id:
|
||||
card_url = f"{planka_url.rstrip('/')}/boards/{board_id}/cards/{c.id}"
|
||||
table.add_row(str(c.id), c.name, str(list_id), str(c.position), str(card_url or "-"))
|
||||
|
||||
console.print(table)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@cards_app.command("show")
|
||||
def show_card(card_id: str):
|
||||
"""Show details for a card."""
|
||||
@@ -360,7 +408,14 @@ def show_card(card_id: str):
|
||||
def extract_attachment_url(attachment: object, base_url: Optional[str]) -> Optional[str]:
|
||||
data = safe_attr(attachment, "data")
|
||||
if isinstance(data, dict):
|
||||
for key in ("url", "downloadUrl", "download_url", "link", "href", "path"):
|
||||
for key in (
|
||||
"url",
|
||||
"downloadUrl",
|
||||
"download_url",
|
||||
"link",
|
||||
"href",
|
||||
"path",
|
||||
):
|
||||
value = data.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return normalize_url(base_url, value)
|
||||
@@ -394,11 +449,16 @@ def show_card(card_id: str):
|
||||
except Exception:
|
||||
schema = {}
|
||||
list_id_value = schema.get("listId")
|
||||
board_id_value = schema.get("boardId")
|
||||
list_name_value = None
|
||||
list_obj = safe_attr(card, "list")
|
||||
if list_obj is not None:
|
||||
list_id_value = safe_attr(list_obj, "id") or list_id_value
|
||||
list_name_value = safe_attr(list_obj, "name")
|
||||
if not board_id_value:
|
||||
list_schema = safe_attr(list_obj, "schema")
|
||||
if isinstance(list_schema, dict):
|
||||
board_id_value = list_schema.get("boardId")
|
||||
|
||||
list_display = "-"
|
||||
if list_name_value and list_id_value:
|
||||
@@ -432,9 +492,16 @@ def show_card(card_id: str):
|
||||
if comments_count is None and comments_error is None:
|
||||
comments_count = len(comments)
|
||||
|
||||
planka_url, _, _ = get_env_config()
|
||||
card_url = None
|
||||
if planka_url and board_id_value and card.id:
|
||||
card_url = f"{planka_url.rstrip('/')}/boards/{board_id_value}/cards/{card.id}"
|
||||
|
||||
add_row("ID", card.id)
|
||||
add_row("URL", card_url)
|
||||
add_row("Name", card.name)
|
||||
add_row("Description", safe_attr(card, "description") or schema.get("description"))
|
||||
add_row("Board ID", board_id_value)
|
||||
add_row("List", list_display)
|
||||
add_row("Position", safe_attr(card, "position") or schema.get("position"))
|
||||
add_row("Type", safe_attr(card, "type") or schema.get("type"))
|
||||
@@ -453,7 +520,6 @@ def show_card(card_id: str):
|
||||
|
||||
console.print(table)
|
||||
|
||||
planka_url, _, _ = get_env_config()
|
||||
if attachments and attachments_error is None:
|
||||
attachments_table = make_table("Attachments")
|
||||
attachments_table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
||||
@@ -502,12 +568,15 @@ def show_card(card_id: str):
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@cards_app.command("create")
|
||||
def create_card(
|
||||
list_id: str = typer.Argument(..., help="List ID to create the card in"),
|
||||
name: str = typer.Argument(..., help="Card name/title"),
|
||||
description: Optional[str] = typer.Option(None, "--description", "-d", help="Card description"),
|
||||
position: str = typer.Option("bottom", "--position", "-p", help="Position: top, bottom, or integer"),
|
||||
position: str = typer.Option(
|
||||
"bottom", "--position", "-p", help="Position: top, bottom, or integer"
|
||||
),
|
||||
card_type: str = typer.Option("project", "--type", "-t", help="Card type (project, story)"),
|
||||
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date (ISO-8601)"),
|
||||
due_completed: bool = typer.Option(False, "--due-completed", help="Mark due date as completed"),
|
||||
@@ -539,15 +608,24 @@ def create_card(
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@cards_app.command("update")
|
||||
def update_card(
|
||||
card_id: str = typer.Argument(..., help="Card ID to update"),
|
||||
name: Optional[str] = typer.Option(None, "--name", help="New card name"),
|
||||
description: Optional[str] = typer.Option(None, "--description", "-d", help="New card description"),
|
||||
clear_description: bool = typer.Option(False, "--clear-description", help="Clear the description"),
|
||||
position: Optional[str] = typer.Option(None, "--position", "-p", help="Position: top, bottom, or integer"),
|
||||
description: Optional[str] = typer.Option(
|
||||
None, "--description", "-d", help="New card description"
|
||||
),
|
||||
clear_description: bool = typer.Option(
|
||||
False, "--clear-description", help="Clear the description"
|
||||
),
|
||||
position: Optional[str] = typer.Option(
|
||||
None, "--position", "-p", help="Position: top, bottom, or integer"
|
||||
),
|
||||
list_id: Optional[str] = typer.Option(None, "--list-id", help="Move to a new list"),
|
||||
card_type: Optional[str] = typer.Option(None, "--type", "-t", help="Card type (project, story)"),
|
||||
card_type: Optional[str] = typer.Option(
|
||||
None, "--type", "-t", help="Card type (project, story)"
|
||||
),
|
||||
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date (ISO-8601)"),
|
||||
clear_due_date: bool = typer.Option(False, "--clear-due-date", help="Clear the due date"),
|
||||
due_completed: Optional[bool] = typer.Option(
|
||||
@@ -558,7 +636,9 @@ def update_card(
|
||||
):
|
||||
"""Update an existing card."""
|
||||
if description is not None and clear_description:
|
||||
console.print("[bold red]Error:[/bold red] Use either --description or --clear-description.")
|
||||
console.print(
|
||||
"[bold red]Error:[/bold red] Use either --description or --clear-description."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if due_date is not None and clear_due_date:
|
||||
console.print("[bold red]Error:[/bold red] Use either --due-date or --clear-due-date.")
|
||||
@@ -606,6 +686,7 @@ def update_card(
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@cards_app.command("delete")
|
||||
def delete_card(
|
||||
card_id: str = typer.Argument(..., help="Card ID to delete"),
|
||||
@@ -630,6 +711,7 @@ def delete_card(
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@notifications_app.command("all")
|
||||
def all_notifications():
|
||||
"""List all notifications."""
|
||||
@@ -639,6 +721,7 @@ def all_notifications():
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
@notifications_app.command("unread")
|
||||
def unread_notifications():
|
||||
"""List unread notifications."""
|
||||
@@ -648,5 +731,6 @@ def unread_notifications():
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
@@ -7,18 +7,14 @@ class _DummyPlanka:
|
||||
"""Stub for plankapy.v1.Planka to satisfy root __init__.py"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"plankapy.v1 is not available in this build; use plankapy.v2"
|
||||
)
|
||||
raise NotImplementedError("plankapy.v1 is not available in this build; use plankapy.v2")
|
||||
|
||||
|
||||
class _DummyPasswordAuth:
|
||||
"""Stub for plankapy.v1.PasswordAuth to satisfy root __init__.py"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"plankapy.v1 is not available in this build; use plankapy.v2"
|
||||
)
|
||||
raise NotImplementedError("plankapy.v1 is not available in this build; use plankapy.v2")
|
||||
|
||||
|
||||
# Create a dummy v1 module to satisfy plankapy's root __init__.py
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Tests for planka-cli."""
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from scripts.planka_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
class TestHelp:
|
||||
"""Test help output for all commands."""
|
||||
|
||||
def test_main_help(self):
|
||||
"""Main command should show help."""
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Planka CLI" in result.output
|
||||
|
||||
def test_projects_help(self):
|
||||
"""Projects subcommand should show help."""
|
||||
result = runner.invoke(app, ["projects", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage projects" in result.output
|
||||
|
||||
def test_boards_help(self):
|
||||
"""Boards subcommand should show help."""
|
||||
result = runner.invoke(app, ["boards", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage boards" in result.output
|
||||
|
||||
def test_lists_help(self):
|
||||
"""Lists subcommand should show help."""
|
||||
result = runner.invoke(app, ["lists", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage lists" in result.output
|
||||
|
||||
def test_cards_help(self):
|
||||
"""Cards subcommand should show help."""
|
||||
result = runner.invoke(app, ["cards", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage cards" in result.output
|
||||
|
||||
def test_notifications_help(self):
|
||||
"""Notifications subcommand should show help."""
|
||||
result = runner.invoke(app, ["notifications", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage notifications" in result.output
|
||||
|
||||
|
||||
class TestUnknownCommand:
|
||||
"""Test behavior with unknown commands."""
|
||||
|
||||
def test_unknown_subcommand_shows_help(self):
|
||||
"""Unknown subcommand should show help and exit with code 2."""
|
||||
result = runner.invoke(app, ["unknown"])
|
||||
assert result.exit_code == 2
|
||||
|
||||
|
||||
class TestLoginLogout:
|
||||
"""Test login/logout commands (without actually connecting)."""
|
||||
|
||||
def test_login_requires_options(self):
|
||||
"""Login without options should fail."""
|
||||
result = runner.invoke(app, ["login"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_logout_without_credentials(self, tmp_path, monkeypatch):
|
||||
"""Logout without stored credentials should be graceful."""
|
||||
# Point ENV_PATH to a temp location
|
||||
import scripts.planka_cli as cli_module
|
||||
|
||||
monkeypatch.setattr(cli_module, "ENV_PATH", tmp_path / ".env")
|
||||
result = runner.invoke(app, ["logout"])
|
||||
assert result.exit_code == 0
|
||||
assert "No stored credentials" in result.output
|
||||
@@ -103,6 +103,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.4"
|
||||
@@ -169,6 +178,8 @@ dependencies = [
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -180,7 +191,11 @@ requires-dist = [
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pyinstaller" }]
|
||||
dev = [
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plankapy"
|
||||
@@ -209,6 +224,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/57/db8f36d86e649ec19e1978a665c6b6cde945f9d72f28f326154d3705562b/plankapy-2.3.0-py3-none-any.whl", hash = "sha256:699532cfff4e5ca7e47409d17ff64c34aff85a41ea14887a87adc1770b3c1066", size = 135703, upload-time = "2026-01-27T00:11:41.456Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -259,6 +283,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318, upload-time = "2026-01-20T00:15:21.88Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
@@ -290,6 +330,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.10.2"
|
||||
|
||||
Reference in New Issue
Block a user