anaconda-cli-base 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ *.egg-info
12
+
13
+ # Unit test / coverage reports
14
+ htmlcov/
15
+ .coverage
16
+ .coverage.*
17
+ coverage.xml
18
+ .pytest_cache/
19
+ .tox/
20
+
21
+ # Sphinx documentation
22
+ docs/_build/
23
+
24
+ # Jupyter Notebook
25
+ .ipynb_checkpoints
26
+
27
+ # dotenv
28
+ .env
29
+
30
+ # virtualenv
31
+ .venv
32
+ venv/
33
+
34
+ # mypy
35
+ .mypy_cache/
36
+
37
+ # vscode ide stuff
38
+ *.code-workspace
39
+ .history
40
+ .vscode
41
+
42
+ # Version file generated by setuptools-scm
43
+ /libs/*/src/*/_version.py
44
+
45
+ # Stored tokens (for dev)
46
+ /tokens.json
@@ -0,0 +1,27 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Anaconda, Inc.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright
10
+ notice, this list of conditions and the following disclaimer.
11
+ * Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the distribution.
14
+ * Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products
16
+ derived from this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
22
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,28 @@
1
+ # Conda-related paths
2
+ conda_env_dir ?= ./env
3
+
4
+ # Command aliases
5
+ CONDA_EXE ?= conda
6
+ CONDA_RUN := $(CONDA_EXE) run --prefix $(conda_env_dir) --no-capture-output
7
+
8
+ help: ## Display help on all Makefile targets
9
+ @@grep -h '^[a-zA-Z]' $(MAKEFILE_LIST) | awk -F ':.*?## ' 'NF==2 {printf " %-20s%s\n", $$1, $$2}' | sort
10
+
11
+ setup: ## Setup local dev conda environment
12
+ $(CONDA_EXE) env $(shell [ -d $(conda_env_dir) ] && echo update || echo create) -p $(conda_env_dir) --file environment-dev.yml
13
+
14
+ test: ## Run all the unit tests
15
+ $(CONDA_RUN) pytest
16
+
17
+ tox: ## Run tox to test in isolated environments
18
+ $(CONDA_RUN) tox
19
+
20
+ clean: ## Clean up cache and temporary files
21
+ find . -name \*.py[cod] -delete
22
+ rm -rf .pytest_cache .mypy_cache .tox build dist
23
+
24
+ clean-all: clean ## Clean up, including build files and conda environment
25
+ find . -name \*.egg-info -delete
26
+ rm -rf $(conda_env_dir)
27
+
28
+ .PHONY: $(MAKECMDGOALS)
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.1
2
+ Name: anaconda-cli-base
3
+ Version: 0.1.0
4
+ Summary: A base CLI entrypoint supporting Anaconda CLI plugins
5
+ License: BSD-3-Clause
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.8
8
+ Requires-Dist: readchar
9
+ Requires-Dist: rich
10
+ Requires-Dist: typer
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy; extra == 'dev'
13
+ Requires-Dist: pytest; extra == 'dev'
14
+ Requires-Dist: pytest-mock; extra == 'dev'
15
+ Requires-Dist: tox; extra == 'dev'
16
+ Requires-Dist: types-requests; extra == 'dev'
17
+ Provides-Extra: publish
18
+ Requires-Dist: build; extra == 'publish'
19
+ Requires-Dist: twine; extra == 'publish'
20
+ Requires-Dist: wheel; extra == 'publish'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # anaconda-cli-base
24
+
25
+ A base CLI entrypoint supporting Anaconda CLI plugins
26
+
27
+ ## Registering plugins
28
+
29
+ Subcommands can be registered as follows:
30
+
31
+ ```toml
32
+ # In pyproject.toml
33
+
34
+ [project.entry-points."anaconda_cli.subcommand"]
35
+ auth = "anaconda_cloud_auth.cli:app"
36
+ ```
37
+
38
+ In the example above:
39
+
40
+ * `"anaconda_cloud_cli.subcommand"` is the required string to use for registration. The quotes are important.
41
+ * `auth` is the name of the new subcommand, i.e. `anaconda auth`
42
+ * `anaconda_cloud_auth.cli:app` signifies the object named `app` in the `anaconda_cloud_auth.cli` module is the entry point for the subcommand.
43
+
44
+
45
+ ## Setup for development
46
+
47
+ Ensure you have `conda` installed.
48
+ Then run:
49
+ ```shell
50
+ make setup
51
+ ```
52
+
53
+ ## Run the unit tests
54
+ ```shell
55
+ make test
56
+ ```
57
+
58
+ ## Run the unit tests across isolated environments with tox
59
+ ```shell
60
+ make tox
61
+ ```
@@ -0,0 +1,39 @@
1
+ # anaconda-cli-base
2
+
3
+ A base CLI entrypoint supporting Anaconda CLI plugins
4
+
5
+ ## Registering plugins
6
+
7
+ Subcommands can be registered as follows:
8
+
9
+ ```toml
10
+ # In pyproject.toml
11
+
12
+ [project.entry-points."anaconda_cli.subcommand"]
13
+ auth = "anaconda_cloud_auth.cli:app"
14
+ ```
15
+
16
+ In the example above:
17
+
18
+ * `"anaconda_cloud_cli.subcommand"` is the required string to use for registration. The quotes are important.
19
+ * `auth` is the name of the new subcommand, i.e. `anaconda auth`
20
+ * `anaconda_cloud_auth.cli:app` signifies the object named `app` in the `anaconda_cloud_auth.cli` module is the entry point for the subcommand.
21
+
22
+
23
+ ## Setup for development
24
+
25
+ Ensure you have `conda` installed.
26
+ Then run:
27
+ ```shell
28
+ make setup
29
+ ```
30
+
31
+ ## Run the unit tests
32
+ ```shell
33
+ make test
34
+ ```
35
+
36
+ ## Run the unit tests across isolated environments with tox
37
+ ```shell
38
+ make tox
39
+ ```
@@ -0,0 +1,12 @@
1
+ # Local dev conda dependencies
2
+ channels:
3
+ - defaults
4
+ - conda-forge
5
+ dependencies:
6
+ - python=3.10
7
+ - anaconda-client>=1.12.0
8
+ - tox=3
9
+ - tox-conda
10
+ - pip
11
+ - pip:
12
+ - "-e .[dev]"
@@ -0,0 +1,127 @@
1
+ [build-system]
2
+ build-backend = "hatchling.build"
3
+ requires = ["hatchling", "hatch-vcs>=0.3", "setuptools-scm>=7.1"]
4
+
5
+ [project]
6
+ dependencies = [
7
+ "rich",
8
+ "readchar",
9
+ "typer"
10
+ ]
11
+ description = "A base CLI entrypoint supporting Anaconda CLI plugins"
12
+ dynamic = ["version"]
13
+ license = {text = "BSD-3-Clause"}
14
+ name = "anaconda-cli-base"
15
+ readme = "README.md"
16
+ requires-python = ">=3.8"
17
+
18
+ [project.entry-points."anaconda_cli.main"]
19
+ anaconda = "anaconda_cli_base.cli:app"
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "mypy",
24
+ "pytest",
25
+ "pytest-mock",
26
+ "tox",
27
+ "types-requests"
28
+ ]
29
+ publish = [
30
+ "build",
31
+ "twine",
32
+ "wheel"
33
+ ]
34
+
35
+ [tool.distutils.bdist_wheel]
36
+ universal = true
37
+
38
+ [tool.hatch.build.hooks.vcs]
39
+ version-file = "src/anaconda_cli_base/_version.py"
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = [
43
+ "/src/anaconda_cli_base",
44
+ "/pyproject.toml",
45
+ "/tests",
46
+ "/Makefile",
47
+ "/environment-dev.yml"
48
+ ]
49
+
50
+ [tool.hatch.version]
51
+ source = "vcs"
52
+
53
+ [tool.hatch.version.raw-options]
54
+ git_describe_command = "git describe --dirty --tags --long --match 'anaconda-cli-base-*'"
55
+ root = "../.."
56
+
57
+ [tool.isort]
58
+ force_single_line = true
59
+ profile = "black"
60
+
61
+ [tool.mypy]
62
+ disallow_untyped_defs = true
63
+ files = [
64
+ "src/**/*.py",
65
+ "tests/**/*.py"
66
+ ]
67
+ python_version = "3.8"
68
+
69
+ [[tool.mypy.overrides]]
70
+ ignore_missing_imports = true
71
+ module = "rich_click.*"
72
+
73
+ [tool.pytest.ini_options]
74
+ markers = [
75
+ "integration: Integration tests requiring a browser"
76
+ ]
77
+ norecursedirs = ["env", "envs", ".tox"]
78
+
79
+ [tool.tox]
80
+ legacy_tox_ini = """
81
+ [tox]
82
+ envlist = py38,py39,py310,py311,mypy
83
+ isolated_build = True
84
+
85
+ [gh-actions]
86
+ python =
87
+ 3.8: py38, mypy
88
+ 3.9: py39, mypy
89
+ 3.10: py310, mypy
90
+ 3.11: py311
91
+
92
+ [testenv]
93
+ deps =
94
+ mypy
95
+ pytest
96
+ pytest-mock
97
+ conda_deps =
98
+ anaconda-client >=1.12.0
99
+ conda_channels =
100
+ anaconda-cloud
101
+ defaults
102
+ conda-forge
103
+ commands = pytest -m "not integration"
104
+
105
+ [testenv:mypy]
106
+ deps =
107
+ mypy
108
+ pytest
109
+ pytest-mock
110
+ types-requests
111
+ typer
112
+ rich
113
+ commands = mypy
114
+ """
115
+
116
+ [tool.vendoring]
117
+ destination = "src/anaconda_cli_base/_vendor/"
118
+ namespace = "anaconda_cli_base._vendor"
119
+ patches-dir = "tools/vendoring/patches"
120
+ protected-files = ["__init__.py", "requirements.txt"]
121
+ requirements = "src/anaconda_cli_base/_vendor/requirements.txt"
122
+
123
+ [tool.vendoring.license.directories]
124
+ setuptools = "pkg_resources"
125
+
126
+ [tool.vendoring.license.fallback-urls]
127
+ readchar = "https://raw.githubusercontent.com/magmax/python-readchar/master/LICENCE"
@@ -0,0 +1,8 @@
1
+ try:
2
+ from anaconda_cli_base._version import version as __version__
3
+ except ImportError: # pragma: nocover
4
+ __version__ = "unknown"
5
+
6
+ from anaconda_cli_base.console import console
7
+
8
+ __all__ = ["__version__", "console"]
@@ -0,0 +1,4 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = '0.1.0'
4
+ __version_tuple__ = version_tuple = (0, 1, 0)
@@ -0,0 +1,26 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+
5
+ from anaconda_cli_base import __version__
6
+ from anaconda_cli_base import console
7
+ from anaconda_cli_base.plugins import load_registered_subcommands
8
+
9
+ app = typer.Typer(add_completion=False, help="Welcome to the Anaconda CLI!")
10
+
11
+ load_registered_subcommands(app)
12
+
13
+
14
+ @app.callback(invoke_without_command=True, no_args_is_help=True)
15
+ def main(
16
+ version: Optional[bool] = typer.Option(
17
+ None, "--version", help="Show version and exit."
18
+ )
19
+ ) -> None:
20
+ """Anaconda CLI."""
21
+ if version:
22
+ console.print(
23
+ f"Anaconda CLI, version [cyan]{__version__}[/cyan]",
24
+ style="bold green",
25
+ )
26
+ raise typer.Exit()
@@ -0,0 +1,51 @@
1
+ from typing import List
2
+
3
+ from readchar import key
4
+ from readchar import readkey
5
+ from rich.console import Console
6
+ from rich.live import Live
7
+ from rich.style import Style
8
+ from rich.table import Table
9
+
10
+ __all__ = ["console", "select_from_list"]
11
+
12
+ SELECTED = Style(color="green", bold=True)
13
+
14
+ console = Console()
15
+
16
+
17
+ def _generate_table(header: str, rows: List[str], selected: int) -> Table:
18
+ table = Table(box=None)
19
+
20
+ table.add_column(header)
21
+
22
+ for i, row in enumerate(rows):
23
+ if i == selected:
24
+ style = SELECTED
25
+ value = f"* {row}"
26
+ else:
27
+ style = None
28
+ value = f" {row}"
29
+ table.add_row(value, style=style)
30
+
31
+ return table
32
+
33
+
34
+ def select_from_list(prompt: str, choices: List[str]) -> str:
35
+ """Dynamically select from a list of choices, by using the up/down keys."""
36
+ # inspired by https://github.com/Textualize/rich/discussions/1785#discussioncomment-1883808
37
+ items = choices.copy()
38
+
39
+ selected = 0
40
+ with Live(_generate_table(prompt, items, selected), auto_refresh=False) as live:
41
+ while ch := readkey():
42
+ if ch == key.UP or ch == "k":
43
+ selected = max(0, selected - 1)
44
+ if ch == key.DOWN or ch == "j":
45
+ selected = min(len(items) - 1, selected + 1)
46
+ if ch == key.ENTER:
47
+ live.stop()
48
+ return items[selected]
49
+ live.update(_generate_table(prompt, items, selected), refresh=True)
50
+
51
+ raise ValueError("Unreachable")
@@ -0,0 +1,34 @@
1
+ import logging
2
+ from importlib.metadata import entry_points
3
+ from sys import version_info
4
+
5
+ from typer import Typer
6
+ from typer.models import DefaultPlaceholder
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ PLUGIN_GROUP_NAME = "anaconda_cli.subcommand"
11
+
12
+
13
+ def load_registered_subcommands(app: Typer) -> None:
14
+ """Load all subcommands from plugins."""
15
+ # The API was changed in Python 3.10, see https://docs.python.org/3/library/importlib.metadata.html#entry-points
16
+ if version_info.major == 3 and version_info.minor <= 9:
17
+ subcommand_entry_points = entry_points().get(PLUGIN_GROUP_NAME, [])
18
+ else:
19
+ subcommand_entry_points = entry_points().select(group=PLUGIN_GROUP_NAME) # type: ignore
20
+
21
+ for subcommand_entry_point in subcommand_entry_points:
22
+ # Load the entry point and register it as a subcommand with the base CLI app
23
+ subcommand_app = subcommand_entry_point.load() # type: ignore
24
+
25
+ # Allow plugins to disable this if they explicitly want to, but otherwise make True the default
26
+ if isinstance(subcommand_app.info.no_args_is_help, DefaultPlaceholder):
27
+ subcommand_app.info.no_args_is_help = True
28
+
29
+ app.add_typer(subcommand_app, name=subcommand_entry_point.name) # type: ignore
30
+ log.debug(
31
+ "Loaded subcommand '%s' from '%s'",
32
+ subcommand_entry_point.name, # type: ignore
33
+ subcommand_entry_point.value, # type: ignore
34
+ )
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+ from typing import Callable
6
+
7
+ import pytest
8
+ from mypy_extensions import VarArg
9
+ from typer.testing import CliRunner
10
+ from typer.testing import Result
11
+
12
+ from anaconda_cli_base.cli import app
13
+
14
+ if TYPE_CHECKING:
15
+ from _pytest.monkeypatch import MonkeyPatch
16
+
17
+ CLIInvoker = Callable[[VarArg(str)], Result]
18
+
19
+
20
+ @pytest.fixture()
21
+ def tmp_cwd(monkeypatch: MonkeyPatch, tmp_path: Path) -> Path:
22
+ """Create & return a temporary directory after setting current working directory to it."""
23
+ monkeypatch.chdir(tmp_path)
24
+ return tmp_path
25
+
26
+
27
+ @pytest.fixture()
28
+ def invoke_cli(tmp_cwd: Path) -> CLIInvoker:
29
+ """Returns a function, which can be used to call the CLI from within a temporary directory."""
30
+ runner = CliRunner()
31
+
32
+ def f(*args: str) -> Result:
33
+ return runner.invoke(app, args)
34
+
35
+ return f
@@ -0,0 +1,26 @@
1
+ from typing import Tuple
2
+
3
+ import pytest
4
+
5
+ from anaconda_cli_base import __version__
6
+
7
+ from .conftest import CLIInvoker
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ "args",
12
+ [
13
+ pytest.param((), id="no-args"),
14
+ pytest.param(("--help",), id="--help"),
15
+ ],
16
+ )
17
+ def test_cli_help(invoke_cli: CLIInvoker, args: Tuple[str]) -> None:
18
+ result = invoke_cli(*args)
19
+ assert result.exit_code == 0
20
+ assert "Welcome to the Anaconda CLI!" in result.stdout
21
+
22
+
23
+ def test_cli_version(invoke_cli: CLIInvoker) -> None:
24
+ result = invoke_cli("--version")
25
+ assert result.exit_code == 0
26
+ assert f"Anaconda CLI, version {__version__}" in result.stdout