diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b06118..e0d1101 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ exclude: ^static/.*|assets/.*|/migrations/.*|\.min\.js$|\.min\.css$|\.css\.map$|\.min\.js$|\.js\.map$|\.svg$ default_language_version: - python: python3.11 + python: python3.12 repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.2.1 diff --git a/README.md b/README.md index 08c5c56..cc0c635 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ For full documentation go to https://docs.taskbadger.net/python/. pip install taskbadger ``` +To use the `taskbadger` command-line tool, install the `cli` extra: + +```bash +pip install 'taskbadger[cli]' +``` + ### Client Usage ```python diff --git a/pyproject.toml b/pyproject.toml index f770850..dfa5e8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ dependencies = [ "httpx >=0.20.0", "attrs >=21.3.0", "python-dateutil >=2.8.0", - "typer[all] <0.10.0", "tomlkit >=0.12.5", "importlib-metadata >=1.0; python_version < '3.8'", "typing-extensions >=4.7.1; python_version <= '3.9'", @@ -42,6 +41,10 @@ include = [ celery = [ "celery>=4.0.0,<6.0.0", ] +cli = [ + "typer >=0.12", + "rich >=13.0", +] [tool.uv] package = true @@ -64,6 +67,7 @@ dev = [ "pytest-celery", "redis", "openapi-python-client", + "taskbadger[cli]", ] [project.scripts] diff --git a/taskbadger/cli/utils.py b/taskbadger/cli/utils.py index b6ebf6b..e9cdb1d 100644 --- a/taskbadger/cli/utils.py +++ b/taskbadger/cli/utils.py @@ -1,5 +1,6 @@ import json from enum import Enum +from typing import Optional import typer from rich import print @@ -28,9 +29,9 @@ def get_actions(action_def: tuple[str, str, str]) -> list[Action]: return [] -def merge_kv_json(metadata_kv: list[str], metadata_json: str) -> dict: +def merge_kv_json(metadata_kv: Optional[list[str]], metadata_json: str) -> dict: metadata = {} - for kv in metadata_kv: + for kv in metadata_kv or []: k, v = kv.strip().split("=", 1) metadata[k] = v diff --git a/taskbadger/cli_main.py b/taskbadger/cli_main.py index 1244a87..00115f1 100644 --- a/taskbadger/cli_main.py +++ b/taskbadger/cli_main.py @@ -1,99 +1,107 @@ +import sys from typing import Optional -import typer -from rich import print - -from taskbadger import __version__ -from taskbadger.cli import create, get, list_tasks_command, run, update -from taskbadger.config import get_config, write_config -from taskbadger.sdk import _parse_token - -app = typer.Typer( - rich_markup_mode="rich", - context_settings={"help_option_names": ["-h", "--help"]}, -) - - -app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": False})(run) -app.command(context_settings={"ignore_unknown_options": False})(get) -app.command(context_settings={"ignore_unknown_options": False})(create) -app.command(context_settings={"ignore_unknown_options": False})(update) -app.command("list", context_settings={"ignore_unknown_options": False})(list_tasks_command) - - -def version_callback(value: bool): - if value: - print(f"Task Badger CLI Version: {__version__}") - raise typer.Exit() - - -@app.command() -def configure(ctx: typer.Context): - """Update CLI configuration.""" - config = ctx.meta["tb_config"] - token = typer.prompt("API Key", default=config.token) - parsed = _parse_token(token) - if parsed: - org_slug, project_slug, api_key = parsed - print(f"Project key detected — organization: [green]{org_slug}[/green], project: [green]{project_slug}[/green]") - config.organization_slug = org_slug - config.project_slug = project_slug - config.token = token - else: - config.organization_slug = typer.prompt("Organization slug", default=config.organization_slug) - config.project_slug = typer.prompt("Project slug", default=config.project_slug) - config.token = token - path = write_config(config) - print(f"Config written to [green]{path}[/green]") - - -@app.command() -def docs(): - """Open Task Badger docs in a browser.""" - typer.launch("https://docs.taskbadger.net") - - -@app.command() -def info(ctx: typer.Context): - """Show CLI configuration.""" - config = ctx.meta["tb_config"] - print(str(config)) - - -@app.callback() -def main( - ctx: typer.Context, - org: Optional[str] = typer.Option( - None, - "--org", - "-o", - metavar="TASKBADGER_ORG", - show_default=False, - help="Organization Slug. This will override values from the config file and environment variables.", - ), - project: Optional[str] = typer.Option( - None, - "--project", - "-p", - show_envvar=False, - metavar="TASKBADGER_PROJECT", - show_default=False, - help="Project Slug. This will override values from the config file and environment variables.", - ), - version: Optional[bool] = typer.Option( # noqa - None, - "--version", - callback=version_callback, - is_eager=True, - help="Show CLI Version", - ), -): - """ - Task Badger CLI - """ - config = get_config(org=org, project=project) - ctx.meta["tb_config"] = config - - -if __name__ == "__main__": - app() +try: + import typer + from rich import print +except ImportError as exc: + _missing = exc.name or "typer" + + def app() -> None: + sys.stderr.write( + f"The Task Badger CLI requires the '{_missing}' package, which is not installed.\n" + "Install the CLI extras with:\n\n" + " pip install 'taskbadger[cli]'\n" + ) + sys.exit(1) +else: + from taskbadger import __version__ + from taskbadger.cli import create, get, list_tasks_command, run, update + from taskbadger.config import get_config, write_config + from taskbadger.sdk import _parse_token + + app = typer.Typer( + rich_markup_mode="rich", + context_settings={"help_option_names": ["-h", "--help"]}, + ) + + app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": False})(run) + app.command(context_settings={"ignore_unknown_options": False})(get) + app.command(context_settings={"ignore_unknown_options": False})(create) + app.command(context_settings={"ignore_unknown_options": False})(update) + app.command("list", context_settings={"ignore_unknown_options": False})(list_tasks_command) + + def version_callback(value: bool): + if value: + print(f"Task Badger CLI Version: {__version__}") + raise typer.Exit() + + @app.command() + def configure(ctx: typer.Context): + """Update CLI configuration.""" + config = ctx.meta["tb_config"] + token = typer.prompt("API Key", default=config.token) + parsed = _parse_token(token) + if parsed: + org_slug, project_slug, api_key = parsed + print( + f"Project key detected — organization: [green]{org_slug}[/green], " + f"project: [green]{project_slug}[/green]" + ) + config.organization_slug = org_slug + config.project_slug = project_slug + config.token = token + else: + config.organization_slug = typer.prompt("Organization slug", default=config.organization_slug) + config.project_slug = typer.prompt("Project slug", default=config.project_slug) + config.token = token + path = write_config(config) + print(f"Config written to [green]{path}[/green]") + + @app.command() + def docs(): + """Open Task Badger docs in a browser.""" + typer.launch("https://docs.taskbadger.net") + + @app.command() + def info(ctx: typer.Context): + """Show CLI configuration.""" + config = ctx.meta["tb_config"] + print(str(config)) + + @app.callback() + def main( + ctx: typer.Context, + org: Optional[str] = typer.Option( + None, + "--org", + "-o", + metavar="TASKBADGER_ORG", + show_default=False, + help="Organization Slug. This will override values from the config file and environment variables.", + ), + project: Optional[str] = typer.Option( + None, + "--project", + "-p", + show_envvar=False, + metavar="TASKBADGER_PROJECT", + show_default=False, + help="Project Slug. This will override values from the config file and environment variables.", + ), + version: Optional[bool] = typer.Option( # noqa + None, + "--version", + callback=version_callback, + is_eager=True, + help="Show CLI Version", + ), + ): + """ + Task Badger CLI + """ + config = get_config(org=org, project=project) + ctx.meta["tb_config"] = config + + if __name__ == "__main__": + app() diff --git a/uv.lock b/uv.lock index f4b283e..db062bb 100644 --- a/uv.lock +++ b/uv.lock @@ -1283,7 +1283,6 @@ dependencies = [ { name = "httpx" }, { name = "python-dateutil" }, { name = "tomlkit" }, - { name = "typer", extra = ["all"] }, { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] @@ -1291,6 +1290,10 @@ dependencies = [ celery = [ { name = "celery" }, ] +cli = [ + { name = "rich" }, + { name = "typer" }, +] [package.dev-dependencies] dev = [ @@ -1303,6 +1306,7 @@ dev = [ { name = "pytest-celery" }, { name = "pytest-httpx" }, { name = "redis" }, + { name = "taskbadger", extra = ["cli"] }, ] [package.metadata] @@ -1312,11 +1316,12 @@ requires-dist = [ { name = "httpx", specifier = ">=0.20.0" }, { name = "importlib-metadata", marker = "python_full_version < '3.8'", specifier = ">=1.0" }, { name = "python-dateutil", specifier = ">=2.8.0" }, + { name = "rich", marker = "extra == 'cli'", specifier = ">=13.0" }, { name = "tomlkit", specifier = ">=0.12.5" }, - { name = "typer", extras = ["all"], specifier = "<0.10.0" }, + { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12" }, { name = "typing-extensions", marker = "python_full_version < '3.10'", specifier = ">=4.7.1" }, ] -provides-extras = ["celery"] +provides-extras = ["celery", "cli"] [package.metadata.requires-dev] dev = [ @@ -1329,6 +1334,7 @@ dev = [ { name = "pytest-celery" }, { name = "pytest-httpx" }, { name = "redis" }, + { name = "taskbadger", extras = ["cli"] }, ] [[package]] @@ -1390,23 +1396,18 @@ wheels = [ [[package]] name = "typer" -version = "0.9.4" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/7d/b1e0399aa5e27071f0042784681d28417f3e526c61f62c8e3635ee5ad334/typer-0.9.4.tar.gz", hash = "sha256:f714c2d90afae3a7929fcd72a3abb08df305e1ff61719381384211c4070af57f", size = 276061, upload-time = "2024-03-23T17:07:55.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/39/82c9d3e10979851847361d922a373bdfef4091020da7f893acfaf07c0225/typer-0.9.4-py3-none-any.whl", hash = "sha256:aa6c4a4e2329d868b80ecbaf16f807f2b54e192209d7ac9dd42691d63f7a54eb", size = 45973, upload-time = "2024-03-23T17:07:53.985Z" }, -] - -[package.optional-dependencies] -all = [ - { name = "colorama" }, - { name = "rich" }, - { name = "shellingham" }, + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, ] [[package]]