Compare commits

..

10 Commits

13 changed files with 647 additions and 218 deletions
+11 -8
View File
@@ -15,16 +15,20 @@ jobs:
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - name: Install uv
uses: astral-sh/setup-uv@v5
with: with:
python-version: "3.11" enable-cache: true
- name: Install build dependencies - name: Setup project
run: | run: |
python -m pip install --upgrade pip uv python install 3.13
pip install -r scripts/requirements.txt pyinstaller make setup
- name: Build binary - name: Build binary
run: | run: |
pyinstaller --onefile --name planka-cli scripts/planka_cli.py make build
- name: Smoke test binary
run: |
./dist/planka-cli --help
- name: Package tarball - name: Package tarball
run: | run: |
VERSION="${GITHUB_REF_NAME#v}" VERSION="${GITHUB_REF_NAME#v}"
@@ -49,5 +53,4 @@ jobs:
brew tap voydz/homebrew-tap brew tap voydz/homebrew-tap
brew bump-formula-pr planka-cli \ brew bump-formula-pr planka-cli \
--url "$URL" \ --url "$URL" \
--sha256 "$SHA256" \ --sha256 "$SHA256"
--version "$VERSION"
+1 -2
View File
@@ -28,5 +28,4 @@ htmlcov/
.idea/ .idea/
.DS_Store .DS_Store
# PyInstaller # PyInstaller (planka-cli.spec is committed for reproducible builds)
*.spec
+28 -3
View File
@@ -1,4 +1,6 @@
.PHONY: setup run build clean .PHONY: setup run build smoke clean lint lint-fix test check
.DEFAULT_GOAL := check
setup: setup:
uv venv uv venv
@@ -7,8 +9,31 @@ setup:
run: run:
uv run python scripts/planka_cli.py status 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
check: lint test
build: build:
uv run pyinstaller --onefile --name planka-cli scripts/planka_cli.py uv run pyinstaller planka-cli.spec --noconfirm
smoke: build
@set -e; \
tmp_home="$$(mktemp -d)"; \
trap 'rm -rf "$$tmp_home"' EXIT; \
env -i PATH="/usr/bin:/bin:/usr/sbin:/sbin" HOME="$$tmp_home" \
PYTHONNOUSERSITE=1 PYTHONPATH= PYTHONHOME= \
VIRTUAL_ENV= CONDA_PREFIX= CONDA_DEFAULT_ENV= PIPENV_ACTIVE= \
PYENV_VERSION= UV_PROJECT_ENV= \
./dist/planka-cli --help
clean: clean:
rm -rf dist build *.spec __pycache__ scripts/__pycache__ rm -rf dist build __pycache__ scripts/__pycache__ tests/__pycache__ .pytest_cache
+9 -4
View File
@@ -15,7 +15,7 @@ scan work, create cards, and stay on top of notifications with fast, scriptable
## Quick start ## Quick start
Requirements: A Planka account. For Homebrew installs, macOS + Homebrew. For source/pipx Requirements: A Planka account. For Homebrew installs, macOS + Homebrew. For source/pipx
installs, Python 3.10+. installs, Python 3.11+.
### Install (Homebrew, recommended) ### Install (Homebrew, recommended)
@@ -47,9 +47,10 @@ planka-cli boards list
## Authentication ## Authentication
`login` stores credentials in a `.env` file next to the `planka-cli` binary (or next to `login` stores credentials in `~/.config/planka-cli/tokens/credentials.json` by default.
`scripts/planka_cli.py` when running from source). You can also set credentials via Override with `--tokenstore PATH` or the `PLANKATOKENS` environment variable. You can also
environment variables: set credentials via environment variables. Note: `--tokenstore` is a global option and
must appear before the command name.
```bash ```bash
export PLANKA_URL=https://planka.example export PLANKA_URL=https://planka.example
@@ -69,6 +70,7 @@ planka-cli projects list
planka-cli boards list [PROJECT_ID] planka-cli boards list [PROJECT_ID]
planka-cli lists list <BOARD_ID> planka-cli lists list <BOARD_ID>
planka-cli cards list <LIST_ID> planka-cli cards list <LIST_ID>
planka-cli cards show <CARD_ID>
planka-cli cards create <LIST_ID> "Card title" --description "Details" planka-cli cards create <LIST_ID> "Card title" --description "Details"
planka-cli cards update <CARD_ID> --name "New title" planka-cli cards update <CARD_ID> --name "New title"
@@ -102,6 +104,9 @@ pip install -e .
```bash ```bash
make setup make setup
make run make run
make lint
make test
make check
make build make build
``` ```
+10
View File
@@ -16,6 +16,8 @@ This skill provides a CLI wrapper around the `plankapy` library to interact with
brew install planka-cli brew install planka-cli
``` ```
Source/pipx installs require Python 3.11+ to use plankapy v2.
2. **Configuration:** 2. **Configuration:**
Use the `login` command to store credentials: Use the `login` command to store credentials:
```bash ```bash
@@ -52,6 +54,9 @@ planka-cli lists list <BOARD_ID>
# List Cards in a List # List Cards in a List
planka-cli cards list <LIST_ID> planka-cli cards list <LIST_ID>
# Show a Card (includes attachments with URLs and comment text)
planka-cli cards show <CARD_ID>
# Create a Card # Create a Card
planka-cli cards create <LIST_ID> "Card title" planka-cli cards create <LIST_ID> "Card title"
@@ -80,6 +85,11 @@ planka-cli boards list
planka-cli cards list 1619901252164912136 planka-cli cards list 1619901252164912136
``` ```
**Show card details for card ID 1619901252164912137:**
```bash
planka-cli cards show 1619901252164912137
```
**Create a card in list ID 1619901252164912136:** **Create a card in list ID 1619901252164912136:**
```bash ```bash
planka-cli cards create 1619901252164912136 "Ship CLI" planka-cli cards create 1619901252164912136 "Ship CLI"
+49
View File
@@ -0,0 +1,49 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
from PyInstaller.utils.hooks import collect_all
datas = []
binaries = []
hiddenimports = ['plankapy.v2', 'plankapy.v2.interface', 'plankapy.v2.models', 'plankapy.v2.api', 'plankapy.v2.utils']
hiddenimports += collect_submodules('plankapy.v2.models')
hiddenimports += collect_submodules('plankapy.v2.api')
hiddenimports += collect_submodules('rich._unicode_data')
tmp_ret = collect_all('plankapy.v2')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['scripts/planka_cli.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=['scripts/pyi_rth_plankapy.py'],
excludes=['plankapy.v1'],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='planka-cli',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+16 -3
View File
@@ -6,11 +6,10 @@ build-backend = "setuptools.build_meta"
name = "planka-cli" name = "planka-cli"
version = "0.1.0" version = "0.1.0"
description = "CLI for Planka using plankapy." description = "CLI for Planka using plankapy."
requires-python = ">=3.10" requires-python = ">=3.11"
dependencies = [ dependencies = [
"plankapy", "plankapy>=2.2.2",
"typer", "typer",
"python-dotenv",
"rich", "rich",
] ]
@@ -20,7 +19,21 @@ planka-cli = "scripts.planka_cli:app"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pyinstaller", "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] [tool.setuptools]
packages = ["scripts"] packages = ["scripts"]
+359 -50
View File
@@ -1,3 +1,4 @@
import json
import os import os
import shutil import shutil
import sys import sys
@@ -7,29 +8,17 @@ from typing import Optional, Union
import click import click
import typer import typer
from dotenv import load_dotenv, set_key
from plankapy.v2 import Card, Planka from plankapy.v2 import Card, Planka
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from typer.core import TyperGroup from typer.core import TyperGroup
def get_binary_dir() -> Path: TOKEN_ENV_VAR = "PLANKATOKENS"
argv0 = sys.argv[0] DEFAULT_TOKEN_DIR = Path.home() / ".config" / "planka-cli" / "tokens"
path = Path(argv0) CREDENTIALS_FILENAME = "credentials.json"
if not path.is_absolute(): TOKENSTORE_OVERRIDE: Optional[str] = None
resolved = shutil.which(argv0)
if resolved:
path = Path(resolved)
if path.exists():
return path.resolve().parent if path.is_file() else path.resolve()
return Path(__file__).resolve().parent
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): class HelpOnUnknownCommandGroup(TyperGroup):
def get_command(self, ctx: click.Context, cmd_name: str): def get_command(self, ctx: click.Context, cmd_name: str):
command = super().get_command(ctx, cmd_name) command = super().get_command(ctx, cmd_name)
@@ -52,18 +41,71 @@ app.add_typer(lists_app, name="lists")
app.add_typer(cards_app, name="cards") app.add_typer(cards_app, name="cards")
app.add_typer(notifications_app, name="notifications") app.add_typer(notifications_app, name="notifications")
@app.callback(invoke_without_command=True) @app.callback(invoke_without_command=True)
def main(ctx: typer.Context): def main(
ctx: typer.Context,
tokenstore: Optional[str] = typer.Option(None, "--tokenstore", help="Token storage path."),
):
"""Planka CLI.""" """Planka CLI."""
global TOKENSTORE_OVERRIDE
TOKENSTORE_OVERRIDE = tokenstore
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
def get_token_dir(tokenstore: Optional[str] = None) -> Path:
if tokenstore:
return Path(tokenstore).expanduser().resolve()
if TOKENSTORE_OVERRIDE:
return Path(TOKENSTORE_OVERRIDE).expanduser().resolve()
env = os.environ.get(TOKEN_ENV_VAR)
if env:
return Path(env).expanduser().resolve()
return DEFAULT_TOKEN_DIR
def get_credentials_path(tokenstore: Optional[str] = None) -> Path:
return get_token_dir(tokenstore) / CREDENTIALS_FILENAME
def load_stored_credentials(tokenstore: Optional[str] = None) -> dict[str, str]:
credentials_path = get_credentials_path(tokenstore)
if not credentials_path.exists():
return {}
try:
data = json.loads(credentials_path.read_text())
except json.JSONDecodeError as exc:
console.print(
f"[bold red]Error:[/bold red] Invalid credentials file at {credentials_path}: {exc}"
)
raise typer.Exit(1) from exc
if not isinstance(data, dict):
console.print(
f"[bold red]Error:[/bold red] Credentials file at {credentials_path} must be a JSON object."
)
raise typer.Exit(1)
return {str(key): str(value) for key, value in data.items()}
def get_env_config() -> tuple[Optional[str], Optional[str], Optional[str]]: def get_env_config() -> tuple[Optional[str], Optional[str], Optional[str]]:
return ( planka_url = os.getenv("PLANKA_URL")
os.getenv("PLANKA_URL"), planka_username = os.getenv("PLANKA_USERNAME")
os.getenv("PLANKA_USERNAME"), planka_password = os.getenv("PLANKA_PASSWORD")
os.getenv("PLANKA_PASSWORD"),
) if planka_url and planka_username and planka_password:
return planka_url, planka_username, planka_password
stored = load_stored_credentials()
if not planka_url:
planka_url = stored.get("PLANKA_URL")
if not planka_username:
planka_username = stored.get("PLANKA_USERNAME")
if not planka_password:
planka_password = stored.get("PLANKA_PASSWORD")
return planka_url, planka_username, planka_password
def parse_iso_datetime(value: Optional[str]) -> Optional[datetime]: def parse_iso_datetime(value: Optional[str]) -> Optional[datetime]:
if value is None: if value is None:
@@ -78,6 +120,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." "Invalid datetime. Use ISO-8601 like 2025-01-31 or 2025-01-31T10:30:00Z."
) from exc ) from exc
def parse_position(value: Optional[str]) -> Optional[Union[str, int]]: def parse_position(value: Optional[str]) -> Optional[Union[str, int]]:
if value is None: if value is None:
return None return None
@@ -89,6 +132,7 @@ def parse_position(value: Optional[str]) -> Optional[Union[str, int]]:
except ValueError as exc: except ValueError as exc:
raise typer.BadParameter("Position must be 'top', 'bottom', or an integer.") from exc raise typer.BadParameter("Position must be 'top', 'bottom', or an integer.") from exc
def find_list(planka: Planka, list_id: str): def find_list(planka: Planka, list_id: str):
for project in planka.projects: for project in planka.projects:
for board in project.boards: for board in project.boards:
@@ -97,6 +141,17 @@ def find_list(planka: Planka, list_id: str):
return list_item return list_item
return None 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]: def get_card_by_id(planka: Planka, card_id: str) -> Optional[Card]:
try: try:
card_data = planka.endpoints.getCard(card_id)["item"] card_data = planka.endpoints.getCard(card_id)["item"]
@@ -104,6 +159,7 @@ def get_card_by_id(planka: Planka, card_id: str) -> Optional[Card]:
return None return None
return Card(card_data, planka) return Card(card_data, planka)
def make_table(title: str) -> Table: def make_table(title: str) -> Table:
return Table( return Table(
title=title, title=title,
@@ -114,6 +170,7 @@ def make_table(title: str) -> Table:
box=None, box=None,
) )
def render_notifications(title: str, notifications: list) -> None: def render_notifications(title: str, notifications: list) -> None:
if not notifications: if not notifications:
console.print("No notifications found.") console.print("No notifications found.")
@@ -138,19 +195,20 @@ def render_notifications(title: str, notifications: list) -> None:
console.print(table) console.print(table)
def get_planka() -> Planka: def get_planka() -> Planka:
planka_url, planka_username, planka_password = get_env_config() planka_url, planka_username, planka_password = get_env_config()
if not planka_url or not planka_username or not planka_password: if not planka_url or not planka_username or not planka_password:
credentials_path = get_credentials_path()
console.print( console.print(
"[bold red]Error:[/bold red] Missing credentials. " "[bold red]Error:[/bold red] Missing credentials. "
f"Set PLANKA_URL, PLANKA_USERNAME, and PLANKA_PASSWORD or run " f"Set PLANKA_URL, PLANKA_USERNAME, and PLANKA_PASSWORD or run "
f"`planka-cli login --url ... --username ... --password ...` " f"`planka-cli login --url ... --username ... --password ...` "
f"to write {ENV_PATH}." f"to write {credentials_path}."
) )
sys.exit(1) sys.exit(1)
try: try:
# Initialize Planka client (v2) and authenticate
planka = Planka(planka_url) planka = Planka(planka_url)
planka.login(username=planka_username, password=planka_password) planka.login(username=planka_username, password=planka_password)
return planka return planka
@@ -158,57 +216,74 @@ def get_planka() -> Planka:
console.print(f"[bold red]Connection Error:[/bold red] {e}") console.print(f"[bold red]Connection Error:[/bold red] {e}")
sys.exit(1) sys.exit(1)
@app.command() @app.command()
def login( def login(
url: str = typer.Option(..., "--url", "-u", help="Planka base URL"), url: str = typer.Option(..., "--url", "-u", help="Planka base URL"),
username: str = typer.Option(..., "--username", "-n", help="Planka username"), username: str = typer.Option(..., "--username", "-n", help="Planka username"),
password: str = typer.Option(..., "--password", "-p", help="Planka password"), password: str = typer.Option(..., "--password", "-p", help="Planka password"),
): ):
"""Store credentials in a .env file next to the binary.""" """Store credentials in ~/.config/planka-cli/tokens/credentials.json."""
credentials_path = get_credentials_path()
try: try:
ENV_PATH.parent.mkdir(parents=True, exist_ok=True) credentials_path.parent.mkdir(parents=True, exist_ok=True)
set_key(str(ENV_PATH), "PLANKA_URL", url) credentials_path.write_text(
set_key(str(ENV_PATH), "PLANKA_USERNAME", username) json.dumps(
set_key(str(ENV_PATH), "PLANKA_PASSWORD", password) {
"PLANKA_URL": url,
"PLANKA_USERNAME": username,
"PLANKA_PASSWORD": password,
},
indent=2,
)
+ "\n"
)
except OSError as e: except OSError as e:
console.print(f"[bold red]Error:[/bold red] Could not write {ENV_PATH}: {e}") console.print(f"[bold red]Error:[/bold red] Could not write {credentials_path}: {e}")
raise typer.Exit(1) raise typer.Exit(1)
try: try:
os.chmod(ENV_PATH, 0o600) os.chmod(credentials_path, 0o600)
except OSError: except OSError:
pass pass
console.print(f"[green]Saved credentials to[/green] {ENV_PATH}") console.print(f"[green]Saved credentials to[/green] {credentials_path}")
@app.command() @app.command()
def logout(): def logout():
"""Delete the stored .env file next to the binary.""" """Delete the stored ~/.config/planka-cli/tokens/credentials.json file."""
if not ENV_PATH.exists(): token_dir = get_token_dir()
console.print(f"No stored credentials found at {ENV_PATH}") credentials_path = get_credentials_path()
if not credentials_path.exists():
console.print(f"No stored credentials found at {credentials_path}")
return return
try: try:
ENV_PATH.unlink() shutil.rmtree(token_dir)
except OSError as e: except OSError as e:
console.print(f"[bold red]Error:[/bold red] Could not delete {ENV_PATH}: {e}") console.print(f"[bold red]Error:[/bold red] Could not delete {credentials_path}: {e}")
raise typer.Exit(1) raise typer.Exit(1)
console.print("[green]Logged out.[/green] Removed stored credentials.") console.print("[green]Logged out.[/green] Removed stored credentials.")
@app.command() @app.command()
def status(): def status():
"""Check connection and print current user info.""" """Check connection and print current user info."""
planka = get_planka() planka = get_planka()
try: try:
user = planka.me 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: if user.name:
console.print(f"Name: {user.name}") console.print(f"Name: {user.name}")
console.print(f"Email: {user.email}") console.print(f"Email: {user.email}")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error fetching status:[/bold red] {e}") console.print(f"[bold red]Error fetching status:[/bold red] {e}")
@projects_app.command("list") @projects_app.command("list")
def list_projects(): def list_projects():
"""List all projects.""" """List all projects."""
@@ -231,8 +306,11 @@ def list_projects():
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@boards_app.command("list") @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.""" """List boards. Optionally filter by Project ID."""
planka = get_planka() planka = get_planka()
try: try:
@@ -256,19 +334,25 @@ def list_boards(project_id: Optional[str] = typer.Argument(None, help="Project I
console.print("No boards found.") console.print("No boards found.")
return return
planka_url, _, _ = get_env_config()
table = make_table(title) table = make_table(title)
table.add_column("ID", justify="right", style="cyan", no_wrap=True) table.add_column("ID", justify="right", style="cyan", no_wrap=True)
table.add_column("Name", style="magenta") table.add_column("Name", style="magenta")
table.add_column("Project ID", justify="right") table.add_column("Project ID", justify="right")
table.add_column("URL", style="magenta")
for b in boards_list: for b in boards_list:
project_id_value = b.schema.get("projectId") 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) console.print(table)
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@lists_app.command("list") @lists_app.command("list")
def list_lists(board_id: str): def list_lists(board_id: str):
"""List all lists in a board.""" """List all lists in a board."""
@@ -292,46 +376,256 @@ def list_lists(board_id: str):
table = make_table(f"Lists in Board: {target_board.name}") table = make_table(f"Lists in Board: {target_board.name}")
table.add_column("ID", justify="right", style="cyan", no_wrap=True) table.add_column("ID", justify="right", style="cyan", no_wrap=True)
table.add_column("Name", style="magenta") table.add_column("Name", style="magenta")
table.add_column("Board ID", justify="right")
table.add_column("Position", justify="right") table.add_column("Position", justify="right")
for l in target_board.lists: for list_item in target_board.lists:
table.add_row(str(l.id), l.name, str(l.position)) table.add_row(
str(list_item.id), list_item.name, str(target_board.id), str(list_item.position)
)
console.print(table) console.print(table)
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@cards_app.command("list") @cards_app.command("list")
def list_cards(list_id: str): def list_cards(list_id: str):
"""List all cards in a list.""" """List all cards in a list."""
planka = get_planka() planka = get_planka()
try: try:
target_list = find_list(planka, list_id) target_list, target_board = find_list_with_board(planka, list_id)
if not target_list: if not target_list:
console.print(f"[red]List {list_id} not found.[/red]") console.print(f"[red]List {list_id} not found.[/red]")
return 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 = make_table(f"Cards in List: {target_list.name}")
table.add_column("ID", justify="right", style="cyan", no_wrap=True) table.add_column("ID", justify="right", style="cyan", no_wrap=True)
table.add_column("Name", style="magenta") table.add_column("Name", style="magenta")
table.add_column("List ID", justify="right")
table.add_column("Position", justify="right") table.add_column("Position", justify="right")
table.add_column("URL", style="magenta")
for c in target_list.cards: 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) console.print(table)
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {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."""
planka = get_planka()
try:
card = get_card_by_id(planka, card_id)
if not card:
console.print(f"[red]Card {card_id} not found.[/red]")
return
table = make_table(f"Card: {card.name}")
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value", style="magenta")
def safe_attr(obj: object, name: str) -> object:
try:
return getattr(obj, name)
except Exception:
return None
def normalize_url(base_url: Optional[str], value: Optional[str]) -> Optional[str]:
if not value:
return None
candidate = str(value).strip()
if not candidate:
return None
if candidate.startswith("http://") or candidate.startswith("https://"):
return candidate
if base_url:
return f"{base_url.rstrip('/')}/{candidate.lstrip('/')}"
return candidate
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",
):
value = data.get(key)
if isinstance(value, str) and value.strip():
return normalize_url(base_url, value)
file_info = data.get("file")
if isinstance(file_info, dict):
for key in (
"url",
"downloadUrl",
"download_url",
"path",
"thumbnailUrl",
"thumbUrl",
"thumbnail_url",
):
value = file_info.get(key)
if isinstance(value, str) and value.strip():
return normalize_url(base_url, value)
direct_url = safe_attr(attachment, "url")
if isinstance(direct_url, str) and direct_url.strip():
return normalize_url(base_url, direct_url)
return None
def add_row(label: str, value: object) -> None:
if value is None or value == "":
table.add_row(label, "-")
else:
table.add_row(label, str(value))
try:
schema = card.schema or {}
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:
list_display = f"{list_name_value} ({list_id_value})"
elif list_name_value:
list_display = list_name_value
elif list_id_value:
list_display = list_id_value
due_date = safe_attr(card, "due_date") or schema.get("dueDate")
due_completed = safe_attr(card, "due_date_completed")
if due_completed is None:
due_completed = schema.get("isDueCompleted")
if isinstance(due_completed, bool):
due_completed = "yes" if due_completed else "no"
attachments_error = None
comments_error = None
try:
attachments = list(safe_attr(card, "attachments") or [])
except Exception as exc:
attachments = []
attachments_error = str(exc)
try:
comments = list(safe_attr(card, "comments") or [])
except Exception as exc:
comments = []
comments_error = str(exc)
comments_count = safe_attr(card, "comments_count")
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"))
add_row("Due Date", due_date)
add_row("Due Completed", due_completed)
if attachments_error:
add_row("Attachments", f"Error: {attachments_error}")
else:
add_row("Attachments", len(attachments))
if comments_error:
add_row("Comments", f"Error: {comments_error}")
else:
add_row("Comments", comments_count)
add_row("Created At", safe_attr(card, "created_at") or schema.get("createdAt"))
add_row("Updated At", safe_attr(card, "updated_at") or schema.get("updatedAt"))
console.print(table)
if attachments and attachments_error is None:
attachments_table = make_table("Attachments")
attachments_table.add_column("ID", justify="right", style="cyan", no_wrap=True)
attachments_table.add_column("Name", style="magenta")
attachments_table.add_column("Type", style="magenta")
attachments_table.add_column("URL", style="magenta")
attachments_table.add_column("Created At", justify="right")
for attachment in attachments:
attachment_url = extract_attachment_url(attachment, planka_url)
attachments_table.add_row(
str(safe_attr(attachment, "id") or "-"),
str(safe_attr(attachment, "name") or "-"),
str(safe_attr(attachment, "type") or "-"),
str(attachment_url or "-"),
str(safe_attr(attachment, "created_at") or "-"),
)
console.print(attachments_table)
if comments and comments_error is None:
comments_table = make_table("Comments")
comments_table.add_column("ID", justify="right", style="cyan", no_wrap=True)
comments_table.add_column("User", style="magenta")
comments_table.add_column("Text", style="magenta")
comments_table.add_column("Created At", justify="right")
for comment in comments:
user = safe_attr(comment, "user")
user_label = (
safe_attr(user, "name")
or safe_attr(user, "username")
or safe_attr(user, "id")
or "-"
)
text = safe_attr(comment, "text")
text_label = " ".join(str(text).split()) if text else "-"
comments_table.add_row(
str(safe_attr(comment, "id") or "-"),
str(user_label),
text_label,
str(safe_attr(comment, "created_at") or "-"),
)
console.print(comments_table)
except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}")
@cards_app.command("create") @cards_app.command("create")
def create_card( def create_card(
list_id: str = typer.Argument(..., help="List ID to create the card in"), list_id: str = typer.Argument(..., help="List ID to create the card in"),
name: str = typer.Argument(..., help="Card name/title"), name: str = typer.Argument(..., help="Card name/title"),
description: Optional[str] = typer.Option(None, "--description", "-d", help="Card description"), 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)"), 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_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"), due_completed: bool = typer.Option(False, "--due-completed", help="Mark due date as completed"),
@@ -363,15 +657,24 @@ def create_card(
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@cards_app.command("update") @cards_app.command("update")
def update_card( def update_card(
card_id: str = typer.Argument(..., help="Card ID to update"), card_id: str = typer.Argument(..., help="Card ID to update"),
name: Optional[str] = typer.Option(None, "--name", help="New card name"), name: Optional[str] = typer.Option(None, "--name", help="New card name"),
description: Optional[str] = typer.Option(None, "--description", "-d", help="New card description"), description: Optional[str] = typer.Option(
clear_description: bool = typer.Option(False, "--clear-description", help="Clear the description"), None, "--description", "-d", help="New card description"
position: Optional[str] = typer.Option(None, "--position", "-p", help="Position: top, bottom, or integer"), ),
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"), 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)"), 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"), clear_due_date: bool = typer.Option(False, "--clear-due-date", help="Clear the due date"),
due_completed: Optional[bool] = typer.Option( due_completed: Optional[bool] = typer.Option(
@@ -382,7 +685,9 @@ def update_card(
): ):
"""Update an existing card.""" """Update an existing card."""
if description is not None and clear_description: 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) raise typer.Exit(1)
if due_date is not None and clear_due_date: 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.") console.print("[bold red]Error:[/bold red] Use either --due-date or --clear-due-date.")
@@ -430,6 +735,7 @@ def update_card(
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@cards_app.command("delete") @cards_app.command("delete")
def delete_card( def delete_card(
card_id: str = typer.Argument(..., help="Card ID to delete"), card_id: str = typer.Argument(..., help="Card ID to delete"),
@@ -454,6 +760,7 @@ def delete_card(
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@notifications_app.command("all") @notifications_app.command("all")
def all_notifications(): def all_notifications():
"""List all notifications.""" """List all notifications."""
@@ -463,6 +770,7 @@ def all_notifications():
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
@notifications_app.command("unread") @notifications_app.command("unread")
def unread_notifications(): def unread_notifications():
"""List unread notifications.""" """List unread notifications."""
@@ -472,5 +780,6 @@ def unread_notifications():
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}") console.print(f"[bold red]Error:[/bold red] {e}")
if __name__ == "__main__": if __name__ == "__main__":
app() app()
+24
View File
@@ -0,0 +1,24 @@
# PyInstaller runtime hook: stub out plankapy.v1 to avoid circular import issues
import sys
import types
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")
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")
# Create a dummy v1 module to satisfy plankapy's root __init__.py
v1_stub = types.ModuleType("plankapy.v1")
v1_stub.Planka = _DummyPlanka
v1_stub.PasswordAuth = _DummyPasswordAuth
sys.modules["plankapy.v1"] = v1_stub
-4
View File
@@ -1,4 +0,0 @@
plankapy
typer
python-dotenv
rich
View File
+72
View File
@@ -0,0 +1,72 @@
"""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."""
monkeypatch.setenv("PLANKATOKENS", str(tmp_path))
result = runner.invoke(app, ["logout"])
assert result.exit_code == 0
assert "No stored credentials" in result.output
Generated
+65 -141
View File
@@ -1,10 +1,9 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.10" requires-python = ">=3.11"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.13'", "python_full_version >= '3.13'",
"python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.13'",
"python_full_version < '3.11'",
] ]
[[package]] [[package]]
@@ -37,95 +36,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.1" version = "8.3.1"
@@ -193,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" }, { 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]] [[package]]
name = "macholib" name = "macholib"
version = "1.16.4" version = "1.16.4"
@@ -249,10 +168,8 @@ name = "planka-cli"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "plankapy", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "plankapy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
{ name = "plankapy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
{ name = "plankapy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "plankapy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" },
{ name = "python-dotenv" },
{ name = "rich" }, { name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
@@ -260,32 +177,22 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "pyinstaller" }, { name = "pyinstaller" },
{ name = "pytest" },
{ name = "ruff" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "plankapy" }, { name = "plankapy", specifier = ">=2.2.2" },
{ name = "python-dotenv" },
{ name = "rich" }, { name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [{ name = "pyinstaller" }] dev = [
{ name = "pyinstaller" },
[[package]] { name = "pytest" },
name = "plankapy" { name = "ruff" },
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "requests", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/f1/60b984fe9caf2dca21b7e87609fd99be2be547c48fcd58c870c4e8790c2d/plankapy-1.0.2.tar.gz", hash = "sha256:7f25642f521e2b7db45426d570f926a90f51d37f3b1246cef3fa186a75140fc2", size = 21205, upload-time = "2025-01-16T14:51:17.258Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/6e/ee59dec2e1a5ea8a49e73102326f3b130f7167c5940f392f91c0084b8cb4/plankapy-1.0.2-py3-none-any.whl", hash = "sha256:1338de7f97ed51fcf5cf343f4a39dc9655f4d35b0d47cabd87a8f74364af01b6", size = 20470, upload-time = "2025-01-16T14:51:15.136Z" },
] ]
[[package]] [[package]]
@@ -293,7 +200,7 @@ name = "plankapy"
version = "2.2.2" version = "2.2.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.13'",
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9b/57/29987afb2f9862b627a38648ee899e754b05de998cec386e1ca467f6efbd/plankapy-2.2.2.tar.gz", hash = "sha256:bd832f97f9e63e651946b4d1410ceb96c7cee1732cdddfe796ed60bf6404b6f3", size = 46442, upload-time = "2025-05-13T02:05:40.261Z" } sdist = { url = "https://files.pythonhosted.org/packages/9b/57/29987afb2f9862b627a38648ee899e754b05de998cec386e1ca467f6efbd/plankapy-2.2.2.tar.gz", hash = "sha256:bd832f97f9e63e651946b4d1410ceb96c7cee1732cdddfe796ed60bf6404b6f3", size = 46442, upload-time = "2025-05-13T02:05:40.261Z" }
wheels = [ wheels = [
@@ -315,6 +222,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" }, { 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]] [[package]]
name = "pygments" name = "pygments"
version = "2.19.2" version = "2.19.2"
@@ -366,12 +282,19 @@ wheels = [
] ]
[[package]] [[package]]
name = "python-dotenv" name = "pytest"
version = "1.2.1" version = "9.0.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } 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 = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, { 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]] [[package]]
@@ -383,21 +306,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
] ]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi", marker = "python_full_version < '3.11'" },
{ name = "charset-normalizer", marker = "python_full_version < '3.11'" },
{ name = "idna", marker = "python_full_version < '3.11'" },
{ name = "urllib3", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]] [[package]]
name = "rich" name = "rich"
version = "14.3.1" version = "14.3.1"
@@ -411,6 +319,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" }, { 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]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.10.2" version = "80.10.2"
@@ -452,12 +385,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
] ]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]