Add uv package structure and pytest test framework
Set up mcquack as a proper uv package with flat layout (no src/ directory). Uses hatchling build backend to support single-file module structure. Added pytest as dev dependency with fixtures for testing against temporary directories instead of actual macOS LaunchAgent paths. - Rename list() to list_agents() to avoid shadowing builtin (Python 3.14 compat) - Add mock_dirs fixture that monkeypatches LAUNCH_AGENTS_DIR and LOGS_DIR - Add mock_script and mock_launchctl fixtures - 26 tests covering list, create, edit, show, delete commands - Parameterized tests for argument handling - XML validation tests independent of plistlib Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0bcb1b8785
commit
c7a0b04573
7 changed files with 535 additions and 2 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
.claude/
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.14
|
||||
61
conftest.py
Normal file
61
conftest.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Pytest configuration and fixtures for mcquack tests."""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dirs(tmp_path, monkeypatch):
|
||||
"""Set up temporary directories for LAUNCH_AGENTS_DIR and LOGS_DIR.
|
||||
|
||||
Returns a dict with the paths for test assertions.
|
||||
"""
|
||||
import mcquack
|
||||
|
||||
launch_agents_dir = tmp_path / "LaunchAgents"
|
||||
logs_dir = tmp_path / "Logs"
|
||||
launch_agents_dir.mkdir()
|
||||
logs_dir.mkdir()
|
||||
|
||||
monkeypatch.setattr(mcquack, "LAUNCH_AGENTS_DIR", launch_agents_dir)
|
||||
monkeypatch.setattr(mcquack, "LOGS_DIR", logs_dir)
|
||||
|
||||
return {
|
||||
"launch_agents_dir": launch_agents_dir,
|
||||
"logs_dir": logs_dir,
|
||||
"tmp_path": tmp_path,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_script(tmp_path):
|
||||
"""Create a mock executable script for testing.
|
||||
|
||||
Returns the path to the script.
|
||||
"""
|
||||
script = tmp_path / "test_script.sh"
|
||||
script.write_text("#!/bin/bash\necho 'hello'\n")
|
||||
script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
return script
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_launchctl(monkeypatch):
|
||||
"""Mock subprocess.run to avoid actual launchctl calls.
|
||||
|
||||
Returns a list that captures all subprocess.run calls for assertions.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
calls = []
|
||||
|
||||
def mock_run(args, **kwargs):
|
||||
calls.append({"args": args, "kwargs": kwargs})
|
||||
result = subprocess.CompletedProcess(args=args, returncode=0, stdout="", stderr="")
|
||||
return result
|
||||
|
||||
monkeypatch.setattr("subprocess.run", mock_run)
|
||||
return calls
|
||||
|
|
@ -47,8 +47,8 @@ def create_plist_dict(script_path: Path, args: list[str]) -> dict:
|
|||
}
|
||||
|
||||
|
||||
@app.command()
|
||||
def list() -> None:
|
||||
@app.command("list")
|
||||
def list_agents() -> None:
|
||||
"""List all mcquack-managed LaunchAgents."""
|
||||
if not LAUNCH_AGENTS_DIR.exists():
|
||||
typer.echo("No LaunchAgents directory found.")
|
||||
|
|
|
|||
26
pyproject.toml
Normal file
26
pyproject.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[project]
|
||||
name = "mcquack"
|
||||
version = "0.1.0"
|
||||
description = "A simple macOS LaunchAgent manager for executable scripts"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Erich Blume", email = "blume.erich@gmail.com" }
|
||||
]
|
||||
requires-python = ">=3.14"
|
||||
dependencies = ["typer>=0.9.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["."]
|
||||
only-include = ["mcquack.py"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.sources]
|
||||
"mcquack.py" = "mcquack.py"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.2",
|
||||
]
|
||||
279
test_mcquack.py
Normal file
279
test_mcquack.py
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
"""Tests for mcquack."""
|
||||
|
||||
import plistlib
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
import mcquack
|
||||
from mcquack import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def get_plist_path(mock_dirs, script_name="test_script"):
|
||||
"""Helper to get the plist path for a script."""
|
||||
return mock_dirs["launch_agents_dir"] / f"{mcquack.PLIST_PREFIX}.{script_name}.plist"
|
||||
|
||||
|
||||
def read_plist_args(plist_path):
|
||||
"""Read ProgramArguments from a plist file."""
|
||||
with open(plist_path, "rb") as f:
|
||||
plist = plistlib.load(f)
|
||||
return plist.get("ProgramArguments", [])
|
||||
|
||||
|
||||
class TestList:
|
||||
def test_list_empty(self, mock_dirs):
|
||||
"""Test list command with no agents."""
|
||||
result = runner.invoke(app, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "No mcquack-managed LaunchAgents found" in result.stdout
|
||||
|
||||
def test_list_shows_agents(self, mock_dirs, mock_script):
|
||||
"""Test list command shows existing agents."""
|
||||
# Create a plist file manually
|
||||
plist_path = get_plist_path(mock_dirs)
|
||||
plist_data = {
|
||||
"Label": f"{mcquack.PLIST_PREFIX}.test_script",
|
||||
"ProgramArguments": [str(mock_script)],
|
||||
}
|
||||
with open(plist_path, "wb") as f:
|
||||
plistlib.dump(plist_data, f)
|
||||
|
||||
result = runner.invoke(app, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "test_script" in result.stdout
|
||||
|
||||
|
||||
class TestCreate:
|
||||
@pytest.mark.parametrize("script_args,expected_args", [
|
||||
([], []),
|
||||
(["--verbose"], ["--verbose"]),
|
||||
(["--config", "/path/to/config.json"], ["--config", "/path/to/config.json"]),
|
||||
(["arg1", "arg2", "arg3"], ["arg1", "arg2", "arg3"]),
|
||||
(["--flag", "value with spaces"], ["--flag", "value with spaces"]),
|
||||
])
|
||||
def test_create_with_arguments(self, mock_dirs, mock_script, mock_launchctl, script_args, expected_args):
|
||||
"""Test creating agents with various argument configurations."""
|
||||
cmd = ["create", str(mock_script)]
|
||||
if script_args:
|
||||
cmd += ["--"] + script_args
|
||||
result = runner.invoke(app, cmd)
|
||||
assert result.exit_code == 0
|
||||
assert "Created:" in result.stdout
|
||||
assert "Loaded:" in result.stdout
|
||||
|
||||
# Verify plist was created with correct arguments
|
||||
plist_path = get_plist_path(mock_dirs)
|
||||
assert plist_path.exists()
|
||||
|
||||
program_args = read_plist_args(plist_path)
|
||||
assert program_args[0] == str(mock_script.resolve())
|
||||
assert program_args[1:] == expected_args
|
||||
|
||||
def test_create_nonexistent_script(self, mock_dirs):
|
||||
"""Test creating agent for nonexistent script."""
|
||||
result = runner.invoke(app, ["create", "/nonexistent/script.sh"])
|
||||
assert result.exit_code == 1
|
||||
assert "does not exist" in result.output
|
||||
|
||||
def test_create_non_executable(self, mock_dirs, tmp_path):
|
||||
"""Test creating agent for non-executable script."""
|
||||
script = tmp_path / "not_executable.sh"
|
||||
script.write_text("#!/bin/bash\necho 'hello'\n")
|
||||
# Don't make it executable
|
||||
|
||||
result = runner.invoke(app, ["create", str(script)])
|
||||
assert result.exit_code == 1
|
||||
assert "not executable" in result.output
|
||||
|
||||
def test_create_already_exists(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Test creating agent that already exists."""
|
||||
# Create first
|
||||
runner.invoke(app, ["create", str(mock_script)])
|
||||
|
||||
# Try to create again
|
||||
result = runner.invoke(app, ["create", str(mock_script)])
|
||||
assert result.exit_code == 1
|
||||
assert "already exists" in result.output
|
||||
|
||||
|
||||
class TestEdit:
|
||||
def test_edit_add_arguments(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Test adding arguments to an existing agent."""
|
||||
# Create without arguments
|
||||
runner.invoke(app, ["create", str(mock_script)])
|
||||
|
||||
# Edit to add arguments
|
||||
result = runner.invoke(app, ["edit", str(mock_script), "--", "--verbose", "--config", "test.json"])
|
||||
assert result.exit_code == 0
|
||||
assert "Updated:" in result.stdout
|
||||
assert "Reloaded:" in result.stdout
|
||||
|
||||
# Verify arguments were added
|
||||
program_args = read_plist_args(get_plist_path(mock_dirs))
|
||||
assert program_args[1:] == ["--verbose", "--config", "test.json"]
|
||||
|
||||
def test_edit_remove_arguments(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Test removing arguments from an existing agent."""
|
||||
# Create with arguments
|
||||
runner.invoke(app, ["create", str(mock_script), "--", "--old-arg", "old-value"])
|
||||
|
||||
# Edit with no arguments (clears them)
|
||||
result = runner.invoke(app, ["edit", str(mock_script)])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify arguments were removed
|
||||
program_args = read_plist_args(get_plist_path(mock_dirs))
|
||||
assert len(program_args) == 1 # Only the script path remains
|
||||
|
||||
def test_edit_replace_arguments(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Test replacing arguments on an existing agent."""
|
||||
# Create with initial arguments
|
||||
runner.invoke(app, ["create", str(mock_script), "--", "--old"])
|
||||
|
||||
# Edit to replace with new arguments
|
||||
result = runner.invoke(app, ["edit", str(mock_script), "--", "--new", "value"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify arguments were replaced
|
||||
program_args = read_plist_args(get_plist_path(mock_dirs))
|
||||
assert program_args[1:] == ["--new", "value"]
|
||||
|
||||
def test_edit_nonexistent(self, mock_dirs, mock_script):
|
||||
"""Test editing nonexistent agent."""
|
||||
result = runner.invoke(app, ["edit", str(mock_script), "--", "--arg"])
|
||||
assert result.exit_code == 1
|
||||
assert "does not exist" in result.output
|
||||
|
||||
@pytest.mark.parametrize("new_args", [
|
||||
["--single"],
|
||||
["--multi", "arg1", "arg2"],
|
||||
[],
|
||||
])
|
||||
def test_edit_various_arguments(self, mock_dirs, mock_script, mock_launchctl, new_args):
|
||||
"""Test editing with various argument configurations."""
|
||||
runner.invoke(app, ["create", str(mock_script), "--", "--initial"])
|
||||
|
||||
cmd = ["edit", str(mock_script)]
|
||||
if new_args:
|
||||
cmd += ["--"] + new_args
|
||||
result = runner.invoke(app, cmd)
|
||||
assert result.exit_code == 0
|
||||
|
||||
program_args = read_plist_args(get_plist_path(mock_dirs))
|
||||
assert program_args[1:] == new_args
|
||||
|
||||
|
||||
class TestShow:
|
||||
@pytest.mark.parametrize("script_args,expected_display", [
|
||||
([], "(none)"),
|
||||
(["--verbose"], "--verbose"),
|
||||
(["--config", "test.json"], "--config test.json"),
|
||||
])
|
||||
def test_show_with_arguments(self, mock_dirs, mock_script, mock_launchctl, script_args, expected_display):
|
||||
"""Test showing agent details with various arguments."""
|
||||
cmd = ["create", str(mock_script)]
|
||||
if script_args:
|
||||
cmd += ["--"] + script_args
|
||||
runner.invoke(app, cmd)
|
||||
|
||||
result = runner.invoke(app, ["show", str(mock_script)])
|
||||
assert result.exit_code == 0
|
||||
assert "Label:" in result.stdout
|
||||
assert "test_script" in result.stdout
|
||||
assert f"Arguments: {expected_display}" in result.stdout
|
||||
|
||||
def test_show_nonexistent(self, mock_dirs, mock_script):
|
||||
"""Test showing nonexistent agent."""
|
||||
result = runner.invoke(app, ["show", str(mock_script)])
|
||||
assert result.exit_code == 1
|
||||
assert "does not exist" in result.output
|
||||
|
||||
|
||||
class TestDelete:
|
||||
def test_delete_agent(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Test deleting an agent."""
|
||||
runner.invoke(app, ["create", str(mock_script)])
|
||||
|
||||
result = runner.invoke(app, ["delete", str(mock_script)])
|
||||
assert result.exit_code == 0
|
||||
assert "Deleted:" in result.stdout
|
||||
|
||||
# Verify plist was removed
|
||||
plist_path = get_plist_path(mock_dirs)
|
||||
assert not plist_path.exists()
|
||||
|
||||
def test_delete_nonexistent(self, mock_dirs, mock_script):
|
||||
"""Test deleting nonexistent agent."""
|
||||
result = runner.invoke(app, ["delete", str(mock_script)])
|
||||
assert result.exit_code == 1
|
||||
assert "does not exist" in result.output
|
||||
|
||||
|
||||
class TestPlistFormat:
|
||||
"""Tests verifying plist file format independently of plistlib."""
|
||||
|
||||
def test_plist_is_valid_xml(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Verify created plist is valid XML (not relying on plistlib)."""
|
||||
runner.invoke(app, ["create", str(mock_script), "--", "--test-arg", "value"])
|
||||
|
||||
plist_path = get_plist_path(mock_dirs)
|
||||
|
||||
# Parse as raw XML
|
||||
tree = ET.parse(plist_path)
|
||||
root = tree.getroot()
|
||||
|
||||
assert root.tag == "plist"
|
||||
assert root.attrib.get("version") == "1.0"
|
||||
|
||||
def test_plist_contains_expected_keys(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Verify plist contains required LaunchAgent keys via XML parsing."""
|
||||
runner.invoke(app, ["create", str(mock_script), "--", "--arg1", "val1"])
|
||||
|
||||
plist_path = get_plist_path(mock_dirs)
|
||||
tree = ET.parse(plist_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Find the dict element containing key-value pairs
|
||||
dict_elem = root.find("dict")
|
||||
assert dict_elem is not None
|
||||
|
||||
# Extract keys from the plist
|
||||
keys = [elem.text for elem in dict_elem.findall("key")]
|
||||
|
||||
assert "Label" in keys
|
||||
assert "ProgramArguments" in keys
|
||||
assert "RunAtLoad" in keys
|
||||
assert "KeepAlive" in keys
|
||||
assert "StandardOutPath" in keys
|
||||
assert "StandardErrorPath" in keys
|
||||
|
||||
def test_plist_program_arguments_structure(self, mock_dirs, mock_script, mock_launchctl):
|
||||
"""Verify ProgramArguments array structure via XML parsing."""
|
||||
runner.invoke(app, ["create", str(mock_script), "--", "--flag", "value"])
|
||||
|
||||
plist_path = get_plist_path(mock_dirs)
|
||||
tree = ET.parse(plist_path)
|
||||
root = tree.getroot()
|
||||
dict_elem = root.find("dict")
|
||||
|
||||
# Find ProgramArguments key and its following array
|
||||
children = list(dict_elem)
|
||||
for i, child in enumerate(children):
|
||||
if child.tag == "key" and child.text == "ProgramArguments":
|
||||
array_elem = children[i + 1]
|
||||
assert array_elem.tag == "array"
|
||||
|
||||
# Extract string values from array
|
||||
args = [s.text for s in array_elem.findall("string")]
|
||||
assert len(args) == 3 # script path + 2 args
|
||||
assert args[0] == str(mock_script.resolve())
|
||||
assert args[1] == "--flag"
|
||||
assert args[2] == "value"
|
||||
break
|
||||
else:
|
||||
pytest.fail("ProgramArguments key not found")
|
||||
162
uv.lock
generated
Normal file
162
uv.lock
generated
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[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 = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcquack"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "typer", specifier = ">=0.9.0" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=9.0.2" }]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[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 = "rich"
|
||||
version = "14.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.21.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
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" },
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue