terralab-cli 0.0.1__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.
- terralab_cli-0.0.1/LICENSE +29 -0
- terralab_cli-0.0.1/PKG-INFO +132 -0
- terralab_cli-0.0.1/README.md +111 -0
- terralab_cli-0.0.1/pyproject.toml +71 -0
- terralab_cli-0.0.1/terralab/__init__.py +10 -0
- terralab_cli-0.0.1/terralab/auth_helper.py +89 -0
- terralab_cli-0.0.1/terralab/cli.py +37 -0
- terralab_cli-0.0.1/terralab/client.py +43 -0
- terralab_cli-0.0.1/terralab/commands/__init__.py +0 -0
- terralab_cli-0.0.1/terralab/commands/auth_commands.py +22 -0
- terralab_cli-0.0.1/terralab/commands/pipelines_commands.py +35 -0
- terralab_cli-0.0.1/terralab/config.py +24 -0
- terralab_cli-0.0.1/terralab/log.py +8 -0
- terralab_cli-0.0.1/terralab/logic/__init__.py +0 -0
- terralab_cli-0.0.1/terralab/logic/auth_logic.py +31 -0
- terralab_cli-0.0.1/terralab/logic/pipelines_logic.py +27 -0
- terralab_cli-0.0.1/terralab/terralab +6 -0
- terralab_cli-0.0.1/terralab/utils.py +38 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Broad Institute
|
|
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
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: terralab-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Command line interface for interacting with the Terra Scientific Pipelines Service, or Teaspoons.
|
|
5
|
+
Author: Terra Scientific Services
|
|
6
|
+
Author-email: teaspoons-developers@broadinstitute.org
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: PyJWT (>=2.9.0,<3.0.0)
|
|
15
|
+
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
16
|
+
Requires-Dist: oauth2-cli-auth (>=1.5.0,<2.0.0)
|
|
17
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
18
|
+
Requires-Dist: terra-scientific-pipelines-service-api-client (==0.0.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Teaspoons CLI
|
|
22
|
+
|
|
23
|
+
[](https://sonarcloud.io/summary/new_code?id=DataBiosphere_terra-scientific-pipelines-service-cli)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Python CLI structure
|
|
27
|
+
The CLI code is structured as follows:
|
|
28
|
+
```
|
|
29
|
+
terra-scientific-pipelines-service-cli
|
|
30
|
+
├── terralab
|
|
31
|
+
│ └── commands
|
|
32
|
+
│ │ └── __init__.py
|
|
33
|
+
│ │ └── auth_commands.py
|
|
34
|
+
│ │ └── pipelines_commands.py
|
|
35
|
+
│ └── logic
|
|
36
|
+
│ │ └── __init__.py
|
|
37
|
+
│ │ └── auth_logic.py
|
|
38
|
+
│ │ └── pipelines_logic.py
|
|
39
|
+
│ └── __init__.py
|
|
40
|
+
│ └── auth_helper.py
|
|
41
|
+
│ └── cli.py
|
|
42
|
+
│ └── client.py
|
|
43
|
+
│ └── config.py
|
|
44
|
+
│ └── teaspoons
|
|
45
|
+
├── tests
|
|
46
|
+
│ └── commands
|
|
47
|
+
│ │ └── test_auth_commands.py
|
|
48
|
+
│ │ └── test_pipelines_commands.py
|
|
49
|
+
│ └── logic
|
|
50
|
+
│ │ └── test_auth_logic.py
|
|
51
|
+
│ │ └── test_pipelines_logic.py
|
|
52
|
+
│ └── __init__.py
|
|
53
|
+
│ └── auth_helper.py
|
|
54
|
+
│ └── cli.py
|
|
55
|
+
│ └── client.py
|
|
56
|
+
│ └── config.py
|
|
57
|
+
│ └── terralab
|
|
58
|
+
├── .gitignore
|
|
59
|
+
├── .terralab-cli-config
|
|
60
|
+
├── poetry.lock
|
|
61
|
+
├── pyproject.toml
|
|
62
|
+
├── README.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In the `terralab` directory, we have the following files and subdirectories:
|
|
66
|
+
- `auth_helper.py` contains the code for authenticating with the Terra Scientific Pipelines Service (Terra, via b2c).
|
|
67
|
+
- `cli.py` assembles the CLI sub-modules that are defined in `commands/`.
|
|
68
|
+
- `client.py` contains the code for wrapping API calls to the Terra Scientific Pipelines Service.
|
|
69
|
+
- `config.py` contains the code for managing the CLI configuration via environment variables.
|
|
70
|
+
- `terralab` is the entrypoint for the CLI. It contains the main function that is called when the CLI is run.
|
|
71
|
+
- `utils.py` contains utility functions that are used across the CLI.
|
|
72
|
+
- The `commands` directory contains the CLI sub-modules. This is effectively the controller layer for the CLI.
|
|
73
|
+
- The `logic` directory contains the business logic for the CLI, including calling Terra Scientific Pipelines Service APIs via the thin `teaspoons_client`,
|
|
74
|
+
which is autogenerated and published by the [Terra Scientific Pipelines Service repository](https://github.com/DataBiosphere/terra-scientific-pipelines-service).
|
|
75
|
+
|
|
76
|
+
In the `tests` directory, we have test files that can be run with pytest.
|
|
77
|
+
|
|
78
|
+
## Using the CLI
|
|
79
|
+
For now, the CLI requires poetry to be installed to run. See the [Development](#development) section for instructions on how to install poetry.
|
|
80
|
+
|
|
81
|
+
To run the CLI, navigate to the root (terra-scientific-pipelines-service-cli) directory and run the following command:
|
|
82
|
+
```bash
|
|
83
|
+
./terralab COMMAND [ARGS]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For example, to authenticate with Terralab (via Terra b2c), run the following command:
|
|
87
|
+
```bash
|
|
88
|
+
./terralab auth login
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
To list the pipelines you can run using Terralab, run the following command:
|
|
92
|
+
```bash
|
|
93
|
+
./terralab pipelines list
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
See WIP documentation for the CLI [here](https://docs.google.com/document/d/1ovbcHCzdyuC8RjFfkVJZiuDTQ_UAVrglSxSGaZwppoY/edit?tab=t.0#heading=h.jfsr3j3x0zjr).
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
You'll need to have poetry installed to manage python dependencies. Instructions for installing poetry can be found [here](https://python-poetry.org/docs/).
|
|
101
|
+
|
|
102
|
+
To install the CLI locally, run the following commands from the root project directory:
|
|
103
|
+
```
|
|
104
|
+
poetry lock # only needed if you updated dependencies in pyproject.toml
|
|
105
|
+
poetry install
|
|
106
|
+
```
|
|
107
|
+
You do not need to re-run these commands each time you update code locally, unless you've added dependencies in pyproject.toml.
|
|
108
|
+
|
|
109
|
+
If you do update dependencies in `pyproject.toml`, run `poetry lock` and check in the resulting changes to `poetry.lock` along with the rest of
|
|
110
|
+
your code changes.
|
|
111
|
+
|
|
112
|
+
To run the tests, execute the following command from the root project (terra-scientific-pipelines-service-cli) directory:
|
|
113
|
+
```bash
|
|
114
|
+
poetry run pytest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To run tests with a coverage report printed to the terminal:
|
|
118
|
+
```bash
|
|
119
|
+
poetry run pytest --cov-report term --cov=terralab
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To run the formatter, execute the following command from the root project directory:
|
|
123
|
+
```bash
|
|
124
|
+
poetry run black .
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To run the linter with fixes, execute the following command from the root project directory:
|
|
128
|
+
```bash
|
|
129
|
+
poetry run ruff check --fix
|
|
130
|
+
```
|
|
131
|
+
To run the linter as a check without fixes, omit the `--fix` flag.
|
|
132
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Teaspoons CLI
|
|
2
|
+
|
|
3
|
+
[](https://sonarcloud.io/summary/new_code?id=DataBiosphere_terra-scientific-pipelines-service-cli)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Python CLI structure
|
|
7
|
+
The CLI code is structured as follows:
|
|
8
|
+
```
|
|
9
|
+
terra-scientific-pipelines-service-cli
|
|
10
|
+
├── terralab
|
|
11
|
+
│ └── commands
|
|
12
|
+
│ │ └── __init__.py
|
|
13
|
+
│ │ └── auth_commands.py
|
|
14
|
+
│ │ └── pipelines_commands.py
|
|
15
|
+
│ └── logic
|
|
16
|
+
│ │ └── __init__.py
|
|
17
|
+
│ │ └── auth_logic.py
|
|
18
|
+
│ │ └── pipelines_logic.py
|
|
19
|
+
│ └── __init__.py
|
|
20
|
+
│ └── auth_helper.py
|
|
21
|
+
│ └── cli.py
|
|
22
|
+
│ └── client.py
|
|
23
|
+
│ └── config.py
|
|
24
|
+
│ └── teaspoons
|
|
25
|
+
├── tests
|
|
26
|
+
│ └── commands
|
|
27
|
+
│ │ └── test_auth_commands.py
|
|
28
|
+
│ │ └── test_pipelines_commands.py
|
|
29
|
+
│ └── logic
|
|
30
|
+
│ │ └── test_auth_logic.py
|
|
31
|
+
│ │ └── test_pipelines_logic.py
|
|
32
|
+
│ └── __init__.py
|
|
33
|
+
│ └── auth_helper.py
|
|
34
|
+
│ └── cli.py
|
|
35
|
+
│ └── client.py
|
|
36
|
+
│ └── config.py
|
|
37
|
+
│ └── terralab
|
|
38
|
+
├── .gitignore
|
|
39
|
+
├── .terralab-cli-config
|
|
40
|
+
├── poetry.lock
|
|
41
|
+
├── pyproject.toml
|
|
42
|
+
├── README.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In the `terralab` directory, we have the following files and subdirectories:
|
|
46
|
+
- `auth_helper.py` contains the code for authenticating with the Terra Scientific Pipelines Service (Terra, via b2c).
|
|
47
|
+
- `cli.py` assembles the CLI sub-modules that are defined in `commands/`.
|
|
48
|
+
- `client.py` contains the code for wrapping API calls to the Terra Scientific Pipelines Service.
|
|
49
|
+
- `config.py` contains the code for managing the CLI configuration via environment variables.
|
|
50
|
+
- `terralab` is the entrypoint for the CLI. It contains the main function that is called when the CLI is run.
|
|
51
|
+
- `utils.py` contains utility functions that are used across the CLI.
|
|
52
|
+
- The `commands` directory contains the CLI sub-modules. This is effectively the controller layer for the CLI.
|
|
53
|
+
- The `logic` directory contains the business logic for the CLI, including calling Terra Scientific Pipelines Service APIs via the thin `teaspoons_client`,
|
|
54
|
+
which is autogenerated and published by the [Terra Scientific Pipelines Service repository](https://github.com/DataBiosphere/terra-scientific-pipelines-service).
|
|
55
|
+
|
|
56
|
+
In the `tests` directory, we have test files that can be run with pytest.
|
|
57
|
+
|
|
58
|
+
## Using the CLI
|
|
59
|
+
For now, the CLI requires poetry to be installed to run. See the [Development](#development) section for instructions on how to install poetry.
|
|
60
|
+
|
|
61
|
+
To run the CLI, navigate to the root (terra-scientific-pipelines-service-cli) directory and run the following command:
|
|
62
|
+
```bash
|
|
63
|
+
./terralab COMMAND [ARGS]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For example, to authenticate with Terralab (via Terra b2c), run the following command:
|
|
67
|
+
```bash
|
|
68
|
+
./terralab auth login
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
To list the pipelines you can run using Terralab, run the following command:
|
|
72
|
+
```bash
|
|
73
|
+
./terralab pipelines list
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See WIP documentation for the CLI [here](https://docs.google.com/document/d/1ovbcHCzdyuC8RjFfkVJZiuDTQ_UAVrglSxSGaZwppoY/edit?tab=t.0#heading=h.jfsr3j3x0zjr).
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
You'll need to have poetry installed to manage python dependencies. Instructions for installing poetry can be found [here](https://python-poetry.org/docs/).
|
|
81
|
+
|
|
82
|
+
To install the CLI locally, run the following commands from the root project directory:
|
|
83
|
+
```
|
|
84
|
+
poetry lock # only needed if you updated dependencies in pyproject.toml
|
|
85
|
+
poetry install
|
|
86
|
+
```
|
|
87
|
+
You do not need to re-run these commands each time you update code locally, unless you've added dependencies in pyproject.toml.
|
|
88
|
+
|
|
89
|
+
If you do update dependencies in `pyproject.toml`, run `poetry lock` and check in the resulting changes to `poetry.lock` along with the rest of
|
|
90
|
+
your code changes.
|
|
91
|
+
|
|
92
|
+
To run the tests, execute the following command from the root project (terra-scientific-pipelines-service-cli) directory:
|
|
93
|
+
```bash
|
|
94
|
+
poetry run pytest
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To run tests with a coverage report printed to the terminal:
|
|
98
|
+
```bash
|
|
99
|
+
poetry run pytest --cov-report term --cov=terralab
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To run the formatter, execute the following command from the root project directory:
|
|
103
|
+
```bash
|
|
104
|
+
poetry run black .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
To run the linter with fixes, execute the following command from the root project directory:
|
|
108
|
+
```bash
|
|
109
|
+
poetry run ruff check --fix
|
|
110
|
+
```
|
|
111
|
+
To run the linter as a check without fixes, omit the `--fix` flag.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "terralab-cli"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Command line interface for interacting with the Terra Scientific Pipelines Service, or Teaspoons."
|
|
5
|
+
authors = ["Terra Scientific Services <teaspoons-developers@broadinstitute.org>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [
|
|
8
|
+
{include = "terralab"}
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[tool.poetry.dependencies]
|
|
12
|
+
python = "^3.9"
|
|
13
|
+
terra-scientific-pipelines-service-api-client = "0.0.0"
|
|
14
|
+
python-dotenv = "^1.0.1"
|
|
15
|
+
click = "^8.1.7"
|
|
16
|
+
oauth2-cli-auth = "^1.5.0"
|
|
17
|
+
PyJWT = "^2.9.0"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "^8.3.3"
|
|
21
|
+
pytest-cov = "^3.0.0"
|
|
22
|
+
mockito = ">=1.4.0"
|
|
23
|
+
pytest-mockito = ">=0.0.4"
|
|
24
|
+
ruff = "^0.7.1"
|
|
25
|
+
black = "^24.8.0"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["poetry-core"]
|
|
29
|
+
build-backend = "poetry.core.masonry.api"
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = "terralab"
|
|
33
|
+
|
|
34
|
+
[tool.coverage.run]
|
|
35
|
+
branch = true
|
|
36
|
+
relative_files = true # allows sonarcloud GHA step to process the coverage files
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
# Exclude a variety of commonly ignored directories.
|
|
40
|
+
exclude = [
|
|
41
|
+
".bzr",
|
|
42
|
+
".direnv",
|
|
43
|
+
".eggs",
|
|
44
|
+
".git",
|
|
45
|
+
".git-rewrite",
|
|
46
|
+
".hg",
|
|
47
|
+
".ipynb_checkpoints",
|
|
48
|
+
".mypy_cache",
|
|
49
|
+
".nox",
|
|
50
|
+
".pants.d",
|
|
51
|
+
".pyenv",
|
|
52
|
+
".pytest_cache",
|
|
53
|
+
".pytype",
|
|
54
|
+
".ruff_cache",
|
|
55
|
+
".svn",
|
|
56
|
+
".tox",
|
|
57
|
+
".venv",
|
|
58
|
+
".vscode",
|
|
59
|
+
"__pypackages__",
|
|
60
|
+
"_build",
|
|
61
|
+
"buck-out",
|
|
62
|
+
"build",
|
|
63
|
+
"dist",
|
|
64
|
+
"node_modules",
|
|
65
|
+
"site-packages",
|
|
66
|
+
"venv",
|
|
67
|
+
"generated"
|
|
68
|
+
]
|
|
69
|
+
# Same as Black.
|
|
70
|
+
line-length = 88
|
|
71
|
+
indent-width = 4
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terralab: A CLI for the Terra Scientific Pipelines Service (Teaspoons).
|
|
3
|
+
|
|
4
|
+
This package provides a command-line interface to interact with the Teaspoons service,
|
|
5
|
+
built on top of an autogenerated thin client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.0.1"
|
|
9
|
+
__author__ = "Terra Scientific Services"
|
|
10
|
+
__email__ = "teaspoons-developers@broadinstitute.org"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# auth_helper.py
|
|
2
|
+
|
|
3
|
+
import jwt
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import typing as t
|
|
7
|
+
|
|
8
|
+
from oauth2_cli_auth import (
|
|
9
|
+
OAuth2ClientInfo,
|
|
10
|
+
OAuthCallbackHttpServer,
|
|
11
|
+
get_auth_url,
|
|
12
|
+
open_browser,
|
|
13
|
+
exchange_code_for_access_token,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from config import CliConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
LOGGER = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_access_token_with_browser_open(client_info: OAuth2ClientInfo) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Note: this is overridden from the oauth2-cli-auth library to use a custom auth url
|
|
25
|
+
|
|
26
|
+
Provides a simplified API to:
|
|
27
|
+
|
|
28
|
+
- Spin up the callback server
|
|
29
|
+
- Open the browser with the authorization URL
|
|
30
|
+
- Wait for the code to arrive
|
|
31
|
+
- Get access token from code
|
|
32
|
+
|
|
33
|
+
:param client_info: Client Info for Oauth2 Interaction
|
|
34
|
+
:param server_port: Port of the local web server to spin up
|
|
35
|
+
:return: Access Token
|
|
36
|
+
"""
|
|
37
|
+
server_port = CliConfig().server_port
|
|
38
|
+
callback_server = OAuthCallbackHttpServer(server_port)
|
|
39
|
+
auth_url = get_auth_url(client_info, callback_server.callback_url)
|
|
40
|
+
open_browser(f"{auth_url}&prompt=login")
|
|
41
|
+
code = callback_server.wait_for_code()
|
|
42
|
+
if code is None:
|
|
43
|
+
raise ValueError("No code could be obtained from browser callback page")
|
|
44
|
+
return exchange_code_for_access_token(
|
|
45
|
+
client_info, callback_server.callback_url, code
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _validate_token(token: str) -> bool:
|
|
50
|
+
try:
|
|
51
|
+
# Attempt to read the token to ensure it is valid. If it isn't, the file will be removed and None will be returned.
|
|
52
|
+
# Note: to simplify, not worrying about the signature of the token since that will be verified by the backend services
|
|
53
|
+
# This is just to ensure the token is not expired
|
|
54
|
+
jwt.decode(token, options={"verify_signature": False, "verify_exp": True})
|
|
55
|
+
return True
|
|
56
|
+
except jwt.ExpiredSignatureError:
|
|
57
|
+
LOGGER.debug("Token expired")
|
|
58
|
+
return False
|
|
59
|
+
except Exception as e:
|
|
60
|
+
LOGGER.error(f"Error validating token: {e}")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _clear_local_token(token_file: str):
|
|
65
|
+
try:
|
|
66
|
+
os.remove(token_file)
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
LOGGER.debug("No local token found to clean up")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_local_token(token_file: str) -> t.Optional[str]:
|
|
72
|
+
try:
|
|
73
|
+
with open(token_file, "r") as f:
|
|
74
|
+
token = f.read()
|
|
75
|
+
if _validate_token(token):
|
|
76
|
+
return token
|
|
77
|
+
else:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
except FileNotFoundError:
|
|
81
|
+
_clear_local_token(token_file)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _save_local_token(token_file: str, token: str):
|
|
86
|
+
# Create the containing directory if it doesn't exist
|
|
87
|
+
os.makedirs(os.path.dirname(token_file), exist_ok=True)
|
|
88
|
+
with open(token_file, "w") as f:
|
|
89
|
+
f.write(token)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# cli.py
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from terralab import __version__, log
|
|
7
|
+
from terralab.commands.auth_commands import auth
|
|
8
|
+
from terralab.commands.pipelines_commands import pipelines
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Context settings for commands, for overwriting some click defaults
|
|
12
|
+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
13
|
+
|
|
14
|
+
LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group(name="terralab", context_settings=CONTEXT_SETTINGS)
|
|
18
|
+
@click.version_option(__version__)
|
|
19
|
+
@click.option(
|
|
20
|
+
"--debug",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
help="DEBUG-level logging",
|
|
23
|
+
)
|
|
24
|
+
def cli(debug):
|
|
25
|
+
log.configure_logging(debug)
|
|
26
|
+
LOGGER.debug(
|
|
27
|
+
"Log level set to: %s", logging.getLevelName(logging.getLogger().level)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
cli.add_command(auth)
|
|
32
|
+
cli.add_command(pipelines)
|
|
33
|
+
# will add runs_app later
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
cli()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# client.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from teaspoons_client import Configuration, ApiClient
|
|
6
|
+
from config import CliConfig
|
|
7
|
+
from auth_helper import (
|
|
8
|
+
_load_local_token,
|
|
9
|
+
_validate_token,
|
|
10
|
+
_save_local_token,
|
|
11
|
+
get_access_token_with_browser_open,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_api_client(token: str, api_url: str) -> ApiClient:
|
|
19
|
+
api_config = Configuration()
|
|
20
|
+
api_config.host = api_url
|
|
21
|
+
api_config.access_token = token
|
|
22
|
+
return ApiClient(configuration=api_config)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClientWrapper:
|
|
26
|
+
"""
|
|
27
|
+
Wrapper to ensure that the user is authenticated before running the callback and that provides the low level api client to be used
|
|
28
|
+
by subsequent commands
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
cli_config = CliConfig() # initialize the config from environment variables
|
|
33
|
+
token = _load_local_token(cli_config.token_file)
|
|
34
|
+
if not (token and _validate_token(token)):
|
|
35
|
+
LOGGER.info("No valid token found. Logging you in...")
|
|
36
|
+
token = get_access_token_with_browser_open(cli_config.client_info)
|
|
37
|
+
_save_local_token(cli_config.token_file, token)
|
|
38
|
+
|
|
39
|
+
return _get_api_client(token, cli_config.config["TEASPOONS_API_URL"])
|
|
40
|
+
|
|
41
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
42
|
+
# no action needed
|
|
43
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# commands/auth_commands.py
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from logic import auth_logic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def auth():
|
|
10
|
+
"""Commands for authenticating to Terralab"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@auth.command()
|
|
14
|
+
def login():
|
|
15
|
+
"""Authenticate with Terralab via browser login to Terra b2c"""
|
|
16
|
+
auth_logic.check_local_token_and_fetch_if_needed()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@auth.command()
|
|
20
|
+
def logout():
|
|
21
|
+
"""Clear the local authentication token"""
|
|
22
|
+
auth_logic.clear_local_token()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# commands/pipelines_commands.py
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from logic import pipelines_logic
|
|
7
|
+
from utils import _pretty_print, handle_api_exceptions
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
def pipelines():
|
|
14
|
+
"""Commands for running Teaspooons pipelines"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pipelines.command()
|
|
18
|
+
@handle_api_exceptions
|
|
19
|
+
def list():
|
|
20
|
+
"""List all available pipelines"""
|
|
21
|
+
pipelines_list = pipelines_logic.list_pipelines()
|
|
22
|
+
LOGGER.info(
|
|
23
|
+
f"Found {len(pipelines_list)} available pipeline{'' if len(pipelines_list) == 1 else 's'}:"
|
|
24
|
+
)
|
|
25
|
+
for pipeline in pipelines_list:
|
|
26
|
+
LOGGER.info(f"- {pipeline.pipeline_name} ({pipeline.description})")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pipelines.command()
|
|
30
|
+
@click.argument("pipeline_name")
|
|
31
|
+
@handle_api_exceptions
|
|
32
|
+
def get_info(pipeline_name: str):
|
|
33
|
+
"""Get information about a specific pipeline"""
|
|
34
|
+
pipeline_info = pipelines_logic.get_pipeline_info(pipeline_name)
|
|
35
|
+
_pretty_print(pipeline_info)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# config.py
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from oauth2_cli_auth import OAuth2ClientInfo
|
|
5
|
+
from dotenv import dotenv_values
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CliConfig:
|
|
9
|
+
"""A class to hold configuration information for the CLI"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, config_file="../.terralab-cli-config"):
|
|
12
|
+
self.config = dotenv_values(config_file)
|
|
13
|
+
|
|
14
|
+
self.client_info = OAuth2ClientInfo.from_oidc_endpoint(
|
|
15
|
+
self.config["OAUTH_OPENID_CONFIGURATION_URI"],
|
|
16
|
+
client_id=self.config["OAUTH_CLIENT_ID"],
|
|
17
|
+
scopes=[f"openid+email+profile+{self.config['OAUTH_CLIENT_ID']}"],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self.server_port = int(self.config["SERVER_PORT"])
|
|
21
|
+
|
|
22
|
+
self.token_file = (
|
|
23
|
+
f'{Path.home()}/{self.config["LOCAL_STORAGE_PATH"]}/access_token'
|
|
24
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# logic/auth_logic.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from auth_helper import (
|
|
5
|
+
get_access_token_with_browser_open,
|
|
6
|
+
_validate_token,
|
|
7
|
+
_save_local_token,
|
|
8
|
+
_load_local_token,
|
|
9
|
+
_clear_local_token,
|
|
10
|
+
)
|
|
11
|
+
from config import CliConfig
|
|
12
|
+
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_local_token_and_fetch_if_needed():
|
|
17
|
+
"""Authenticate with Teaspoons via browser login to Terra b2c"""
|
|
18
|
+
cli_config = CliConfig() # initialize the config from environment variables
|
|
19
|
+
token = _load_local_token(cli_config.token_file)
|
|
20
|
+
if token and _validate_token(token):
|
|
21
|
+
LOGGER.info("Already authenticated")
|
|
22
|
+
return
|
|
23
|
+
token = get_access_token_with_browser_open(cli_config.client_info)
|
|
24
|
+
_save_local_token(cli_config.token_file, token)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clear_local_token():
|
|
28
|
+
"""Clear the local authentication token"""
|
|
29
|
+
cli_config = CliConfig() # initialize the config from environment variables
|
|
30
|
+
_clear_local_token(cli_config.token_file)
|
|
31
|
+
LOGGER.info("Logged out")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# logic/pipelines_logic.py
|
|
2
|
+
|
|
3
|
+
from teaspoons_client import PipelinesApi
|
|
4
|
+
from teaspoons_client.models.pipeline_with_details import PipelineWithDetails
|
|
5
|
+
from teaspoons_client.models.pipeline import Pipeline
|
|
6
|
+
|
|
7
|
+
from client import ClientWrapper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_pipelines() -> list[Pipeline]:
|
|
11
|
+
"""List all pipelines, returning a list of dictionaries."""
|
|
12
|
+
with ClientWrapper() as api_client:
|
|
13
|
+
pipeline_client = PipelinesApi(api_client=api_client)
|
|
14
|
+
pipelines = pipeline_client.get_pipelines()
|
|
15
|
+
|
|
16
|
+
result = []
|
|
17
|
+
for pipeline in pipelines.results:
|
|
18
|
+
result.append(pipeline)
|
|
19
|
+
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_pipeline_info(pipeline_name: str) -> PipelineWithDetails:
|
|
24
|
+
"""Get the details of a pipeline, returning a dictionary."""
|
|
25
|
+
with ClientWrapper() as api_client:
|
|
26
|
+
pipeline_client = PipelinesApi(api_client=api_client)
|
|
27
|
+
return pipeline_client.get_pipeline_details(pipeline_name=pipeline_name)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# utils.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from teaspoons_client.exceptions import ApiException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _pretty_print(obj: BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Prints a pydantic model in a pretty format to the console
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
LOGGER.info(json.dumps(obj.model_dump(), indent=4))
|
|
21
|
+
except Exception:
|
|
22
|
+
LOGGER.error(obj)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def handle_api_exceptions(func):
|
|
26
|
+
@wraps(func)
|
|
27
|
+
def wrapper(*args, **kwargs):
|
|
28
|
+
try:
|
|
29
|
+
return func(*args, **kwargs)
|
|
30
|
+
except ApiException as e:
|
|
31
|
+
formatted_message = f"API call failed with status code {e.status} ({e.reason}): {json.loads(e.body)['message']}"
|
|
32
|
+
LOGGER.error(formatted_message)
|
|
33
|
+
exit(1)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
LOGGER.error(str(e))
|
|
36
|
+
exit(1)
|
|
37
|
+
|
|
38
|
+
return wrapper
|