anaconda-cli-base


Nameanaconda-cli-base JSON
Version 0.5.2 PyPI version JSON
download
home_pageNone
SummaryA base CLI entrypoint supporting Anaconda CLI plugins
upload_time2025-02-28 21:55:33
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseBSD-3-Clause
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # anaconda-cli-base

A base CLI entrypoint supporting Anaconda CLI plugins using [Typer](https://github.com/fastapi/typer).

## Registering plugins

To develop a subcommand in a third-party package, first create a `typer.Typer()` app with one or more commands.
See [this example](https://typer.tiangolo.com/#example-upgrade). The commands defined in your package will be prefixed
with the *subcommand* you define when you register the plugin.

In your `pyproject.toml` subcommands can be registered as follows:

```toml
# In pyproject.toml

[project.entry-points."anaconda_cli.subcommand"]
auth = "anaconda_cloud_auth.cli:app"
```

In the example above:

* `"anaconda_cloud_cli.subcommand"` is the required string to use for registration. The quotes are important.
* `auth` is the name of the new subcommand, i.e. `anaconda auth`
  * All `typer.Typer` commands you define in your package are accessible the registered subcommand
  * i.e. `anaconda auth <command>`.
* `anaconda_cloud_auth.cli:app` signifies the object named `app` in the `anaconda_cloud_auth.cli` module is the entry point for the subcommand.

### Error handling

By default any exception raised during CLI execution in your registered plugin will be caught and only a minimal
message will be displayed to the user.

You can define a custom callback for individual exceptions that may be thrown from your subcommand. You can
register handlers for standard library exceptions or custom defined exceptions. It may be best to use custom
exceptions to avoid unintended consequences for other plugins.

To register the callback decorate a function that takes an exception as input, and return an integer error code.
The error code will be sent back through the CLI and your subcommand will exit with that error code.

```python
from typing import Type
from anaconda_cli_base.exceptions import register_error_handler

@register_error_handler(MyCustomException)
def better_exception_handling(e: Type[Exception]) -> int:
    # do something or print useful information
    return 1

@register_error_handler(AnotherException)
def just_ignore_it(e: Type[Exception])
    # ignore the error and let the CLI exit successfully
    return 0


@register_error_handler(YetAnotherException)
def fix_the_error_and_try_again(e: Type[Exception]) -> int:
    # do something and retry the CLI command
    return -1
```

In the second example the handler returns `-1`. This means that the handler has attempted to correct the error
and the CLI subcommand should be re-tried. The handler could call another interactive command, like a login action,
before attempting the CLI subcommand again.

See the [anaconda-cloud-auth](https://github.com/anaconda/anaconda-cloud-tools/blob/main/libs/anaconda-cloud-auth/src/anaconda_cloud_auth/cli.py) plugin for an example custom handler.

### Config file

If your plugin wants to utilize the Anaconda config file, default location `~/.anaconda/config.toml`, to read configuration
parameters you can derive from `anaconda_cli_base.config.AnacondaBaseSettings` to add a section in the config file for
your plugin.
 Each subclass of `AnacondaBaseSettings`
defines the section header. The base class is configured so that parameters defined in subclasses can be read in the
following priority from lowest to highest.

1. default value in the subclass of `AnacondaBaseSettings`
1. Global config file at ~/.anaconda/config.toml
1. `ANACONDA_<PLUGIN-NAME>_<FIELD>` variables defined in the .env file in your working directory
1. `ANACONDA_<PLUGIN-NAME>_<FIELD>` env variables set in your shell or on command invocation
1. value passed as kwarg when using the config subclass directly

Notes:

* `AnacondaBaseSettings` is a subclass of `BaseSettings` from [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#usage).
* Nested pydantic models are also supported.

Here's an example subclass

```python
from anaconda_cli_base.config import AnacondaBaseSettings

class MyPluginConfig(AnacondaBaseSettings, plugin_name="my_plugin"):
    foo: str = "bar"
```

To read the config value in your plugin according to the above
priority:

```python
config = MyPluginConfig()
assert config.foo == "bar"
```

Since there is no value of `foo` in the config file it assumes the default value from the subclass definition.

The value of `foo` can now be written to the config file under the section `my_plugin`

```toml
# ~/.anaconda/config.toml
[plugin.my_plugin]
foo = "baz"
```

Now that the config file has been written, the value of `foo` is read from the
config.toml file:

```python
config = MyPluginConfig()
assert config.foo == "baz"
```

### Nested tables

The AnacondaBaseSettings supports nested Pydantic models.

```python
from anaconda_cli_base.config import AnacondaBaseSettings
from pydantic import BaseModel

class Nested(BaseModel):
    n1: int = 0
    n2: int = 0

class MyPluginConfig(AnacondaBaseSettings, plugin_name="my_plugin"):
    foo: str = "bar"
    nested: Nested = Nested()
```

In the `~/.anaconda/config.toml` you can set values of nested fields as an in-line table

```toml
# ~/.anaconda/config.toml
[plugin.my_plugin]
foo = "baz"
nested = { n1 = 1, n2 = 2}
```

Or as a separate table entry

```toml
# ~/.anaconda/config.toml
[plugin.my_plugin]
foo = "baz"

[plugin.my_plugin.nested]
n1 = 1
n2 = 2
```

To set environment variables use the `__` delimiter

```bash
ANACONDA_MY_PLUGIN_NESTED__N1=1
ANACONDA_MY_PLUGIN_NESTED__N2=2
```

### Nested plugins

You can pass a tuple to `plugin_name=` in subclasses of `AnacondaBaseSettings` to nest whole plugins,
which may be defined in separate packages.

```python
class Nested(BaseModel):
    n1: int = 0
    n2: int = 0
class MyPluginConfig(AnacondaBaseSettings, plugin_name="my_plugin"):
    foo: str = "bar"
    nested: Nested = Nested()
```

Then in another package you can nest a new config into `my_plugin`.

```python
class MyPluginExtrasConfig(AnacondaBaseSettings, plugin_name=("my_plugin", "extras")):
    field: str = "default"
```

The new config table is now nested in the config.toml

```toml
# ~/.anaconda/config.toml
[plugin.my_plugin]
foo = "baz"
nested = { n1 = 1, n2 = 2}
[plugin.my_plugin.extras]
field = "value"
```

And can be set by env variable using the concatenation of `plugin_name`

```bash
ANACONDA_MY_PLUGIN_EXTRAS_FIELD="value"
```

See the [tests](https://github.com/anaconda/anaconda-cli-base/blob/main/tests/test_config.py) for more examples.

## Setup for development

Ensure you have `conda` installed.
Then run:

```shell
make setup
```

### Run the unit tests

```shell
make test
```

### Run the unit tests across isolated environments with tox

```shell
make tox
```

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "anaconda-cli-base",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/e0/ec/46882abf04b3d307f2c8cb27b0d7746737d6210be7b4589988b76275678a/anaconda_cli_base-0.5.2.tar.gz",
    "platform": null,
    "description": "# anaconda-cli-base\n\nA base CLI entrypoint supporting Anaconda CLI plugins using [Typer](https://github.com/fastapi/typer).\n\n## Registering plugins\n\nTo develop a subcommand in a third-party package, first create a `typer.Typer()` app with one or more commands.\nSee [this example](https://typer.tiangolo.com/#example-upgrade). The commands defined in your package will be prefixed\nwith the *subcommand* you define when you register the plugin.\n\nIn your `pyproject.toml` subcommands can be registered as follows:\n\n```toml\n# In pyproject.toml\n\n[project.entry-points.\"anaconda_cli.subcommand\"]\nauth = \"anaconda_cloud_auth.cli:app\"\n```\n\nIn the example above:\n\n* `\"anaconda_cloud_cli.subcommand\"` is the required string to use for registration. The quotes are important.\n* `auth` is the name of the new subcommand, i.e. `anaconda auth`\n  * All `typer.Typer` commands you define in your package are accessible the registered subcommand\n  * i.e. `anaconda auth <command>`.\n* `anaconda_cloud_auth.cli:app` signifies the object named `app` in the `anaconda_cloud_auth.cli` module is the entry point for the subcommand.\n\n### Error handling\n\nBy default any exception raised during CLI execution in your registered plugin will be caught and only a minimal\nmessage will be displayed to the user.\n\nYou can define a custom callback for individual exceptions that may be thrown from your subcommand. You can\nregister handlers for standard library exceptions or custom defined exceptions. It may be best to use custom\nexceptions to avoid unintended consequences for other plugins.\n\nTo register the callback decorate a function that takes an exception as input, and return an integer error code.\nThe error code will be sent back through the CLI and your subcommand will exit with that error code.\n\n```python\nfrom typing import Type\nfrom anaconda_cli_base.exceptions import register_error_handler\n\n@register_error_handler(MyCustomException)\ndef better_exception_handling(e: Type[Exception]) -> int:\n    # do something or print useful information\n    return 1\n\n@register_error_handler(AnotherException)\ndef just_ignore_it(e: Type[Exception])\n    # ignore the error and let the CLI exit successfully\n    return 0\n\n\n@register_error_handler(YetAnotherException)\ndef fix_the_error_and_try_again(e: Type[Exception]) -> int:\n    # do something and retry the CLI command\n    return -1\n```\n\nIn the second example the handler returns `-1`. This means that the handler has attempted to correct the error\nand the CLI subcommand should be re-tried. The handler could call another interactive command, like a login action,\nbefore attempting the CLI subcommand again.\n\nSee the [anaconda-cloud-auth](https://github.com/anaconda/anaconda-cloud-tools/blob/main/libs/anaconda-cloud-auth/src/anaconda_cloud_auth/cli.py) plugin for an example custom handler.\n\n### Config file\n\nIf your plugin wants to utilize the Anaconda config file, default location `~/.anaconda/config.toml`, to read configuration\nparameters you can derive from `anaconda_cli_base.config.AnacondaBaseSettings` to add a section in the config file for\nyour plugin.\n Each subclass of `AnacondaBaseSettings`\ndefines the section header. The base class is configured so that parameters defined in subclasses can be read in the\nfollowing priority from lowest to highest.\n\n1. default value in the subclass of `AnacondaBaseSettings`\n1. Global config file at ~/.anaconda/config.toml\n1. `ANACONDA_<PLUGIN-NAME>_<FIELD>` variables defined in the .env file in your working directory\n1. `ANACONDA_<PLUGIN-NAME>_<FIELD>` env variables set in your shell or on command invocation\n1. value passed as kwarg when using the config subclass directly\n\nNotes:\n\n* `AnacondaBaseSettings` is a subclass of `BaseSettings` from [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#usage).\n* Nested pydantic models are also supported.\n\nHere's an example subclass\n\n```python\nfrom anaconda_cli_base.config import AnacondaBaseSettings\n\nclass MyPluginConfig(AnacondaBaseSettings, plugin_name=\"my_plugin\"):\n    foo: str = \"bar\"\n```\n\nTo read the config value in your plugin according to the above\npriority:\n\n```python\nconfig = MyPluginConfig()\nassert config.foo == \"bar\"\n```\n\nSince there is no value of `foo` in the config file it assumes the default value from the subclass definition.\n\nThe value of `foo` can now be written to the config file under the section `my_plugin`\n\n```toml\n# ~/.anaconda/config.toml\n[plugin.my_plugin]\nfoo = \"baz\"\n```\n\nNow that the config file has been written, the value of `foo` is read from the\nconfig.toml file:\n\n```python\nconfig = MyPluginConfig()\nassert config.foo == \"baz\"\n```\n\n### Nested tables\n\nThe AnacondaBaseSettings supports nested Pydantic models.\n\n```python\nfrom anaconda_cli_base.config import AnacondaBaseSettings\nfrom pydantic import BaseModel\n\nclass Nested(BaseModel):\n    n1: int = 0\n    n2: int = 0\n\nclass MyPluginConfig(AnacondaBaseSettings, plugin_name=\"my_plugin\"):\n    foo: str = \"bar\"\n    nested: Nested = Nested()\n```\n\nIn the `~/.anaconda/config.toml` you can set values of nested fields as an in-line table\n\n```toml\n# ~/.anaconda/config.toml\n[plugin.my_plugin]\nfoo = \"baz\"\nnested = { n1 = 1, n2 = 2}\n```\n\nOr as a separate table entry\n\n```toml\n# ~/.anaconda/config.toml\n[plugin.my_plugin]\nfoo = \"baz\"\n\n[plugin.my_plugin.nested]\nn1 = 1\nn2 = 2\n```\n\nTo set environment variables use the `__` delimiter\n\n```bash\nANACONDA_MY_PLUGIN_NESTED__N1=1\nANACONDA_MY_PLUGIN_NESTED__N2=2\n```\n\n### Nested plugins\n\nYou can pass a tuple to `plugin_name=` in subclasses of `AnacondaBaseSettings` to nest whole plugins,\nwhich may be defined in separate packages.\n\n```python\nclass Nested(BaseModel):\n    n1: int = 0\n    n2: int = 0\nclass MyPluginConfig(AnacondaBaseSettings, plugin_name=\"my_plugin\"):\n    foo: str = \"bar\"\n    nested: Nested = Nested()\n```\n\nThen in another package you can nest a new config into `my_plugin`.\n\n```python\nclass MyPluginExtrasConfig(AnacondaBaseSettings, plugin_name=(\"my_plugin\", \"extras\")):\n    field: str = \"default\"\n```\n\nThe new config table is now nested in the config.toml\n\n```toml\n# ~/.anaconda/config.toml\n[plugin.my_plugin]\nfoo = \"baz\"\nnested = { n1 = 1, n2 = 2}\n[plugin.my_plugin.extras]\nfield = \"value\"\n```\n\nAnd can be set by env variable using the concatenation of `plugin_name`\n\n```bash\nANACONDA_MY_PLUGIN_EXTRAS_FIELD=\"value\"\n```\n\nSee the [tests](https://github.com/anaconda/anaconda-cli-base/blob/main/tests/test_config.py) for more examples.\n\n## Setup for development\n\nEnsure you have `conda` installed.\nThen run:\n\n```shell\nmake setup\n```\n\n### Run the unit tests\n\n```shell\nmake test\n```\n\n### Run the unit tests across isolated environments with tox\n\n```shell\nmake tox\n```\n",
    "bugtrack_url": null,
    "license": "BSD-3-Clause",
    "summary": "A base CLI entrypoint supporting Anaconda CLI plugins",
    "version": "0.5.2",
    "project_urls": null,
    "split_keywords": [],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d42602b8dde76946df4fe9830303c218303b69eaa98598344eda650df48293a1",
                "md5": "437999402bc565c2b1a962dc7c09ef0e",
                "sha256": "d302fd73e6b94d960eef040bdfc9cc228510a8905f178951e5447b959a333df0"
            },
            "downloads": -1,
            "filename": "anaconda_cli_base-0.5.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "437999402bc565c2b1a962dc7c09ef0e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 12749,
            "upload_time": "2025-02-28T21:55:32",
            "upload_time_iso_8601": "2025-02-28T21:55:32.566989Z",
            "url": "https://files.pythonhosted.org/packages/d4/26/02b8dde76946df4fe9830303c218303b69eaa98598344eda650df48293a1/anaconda_cli_base-0.5.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "e0ec46882abf04b3d307f2c8cb27b0d7746737d6210be7b4589988b76275678a",
                "md5": "0fa8acc4c2d8f992d11828d00cb78a7b",
                "sha256": "96ba616ae7091ef2cb5bd473dc164c68e2328ba98b70ce5afbfe7ad3123c8081"
            },
            "downloads": -1,
            "filename": "anaconda_cli_base-0.5.2.tar.gz",
            "has_sig": false,
            "md5_digest": "0fa8acc4c2d8f992d11828d00cb78a7b",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 15522,
            "upload_time": "2025-02-28T21:55:33",
            "upload_time_iso_8601": "2025-02-28T21:55:33.987064Z",
            "url": "https://files.pythonhosted.org/packages/e0/ec/46882abf04b3d307f2c8cb27b0d7746737d6210be7b4589988b76275678a/anaconda_cli_base-0.5.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-02-28 21:55:33",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "anaconda-cli-base"
}
        
Elapsed time: 1.52825s