kaas-cli 0.1.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.
- kaas_cli-0.1.1/LICENSE +19 -0
- kaas_cli-0.1.1/PKG-INFO +69 -0
- kaas_cli-0.1.1/README.md +46 -0
- kaas_cli-0.1.1/pyproject.toml +78 -0
- kaas_cli-0.1.1/src/kaas_cli/__init__.py +2 -0
- kaas_cli-0.1.1/src/kaas_cli/__main__.py +39 -0
- kaas_cli-0.1.1/src/kaas_cli/cli.py +205 -0
- kaas_cli-0.1.1/src/kaas_cli/client.py +617 -0
- kaas_cli-0.1.1/src/kaas_cli/config.py +36 -0
- kaas_cli-0.1.1/src/kaas_cli/constants.py +27 -0
- kaas_cli-0.1.1/src/kaas_cli/hello.py +2 -0
- kaas_cli-0.1.1/src/kaas_cli/py.typed +0 -0
- kaas_cli-0.1.1/src/kaas_cli/types.py +13 -0
kaas_cli-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2024 Runtimeverification, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files KaaS (K as a Service), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
kaas_cli-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kaas-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Command line utility for K as a Service
|
|
5
|
+
Author: Runtime Verification, Inc.
|
|
6
|
+
Author-email: contact@runtimeverification.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: gql[requests] (>=3.4.0,<4.0.0)
|
|
13
|
+
Requires-Dist: hurry-filesize (>=0.9,<0.10)
|
|
14
|
+
Requires-Dist: jmespath (>=1.0.1,<2.0.0)
|
|
15
|
+
Requires-Dist: pytest (>=8.0.1,<9.0.0)
|
|
16
|
+
Requires-Dist: pytest-watch (>=4.2.0,<5.0.0)
|
|
17
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
18
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
19
|
+
Requires-Dist: types-jmespath (>=1.0.2.20240106,<2.0.0.0)
|
|
20
|
+
Requires-Dist: urllib3 (>=2.1.0,<3.0.0)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# KaaS CLI
|
|
24
|
+
|
|
25
|
+
## For Developers
|
|
26
|
+
|
|
27
|
+
Prerequisites: `python >= 3.10`, `pip >= 20.0.2`, `poetry >= 1.3.2`.
|
|
28
|
+
|
|
29
|
+
### Installation
|
|
30
|
+
|
|
31
|
+
To install the package using `pip`, start by building it:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
make build
|
|
35
|
+
pip install dist/*.whl
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Configure the CLI by copying the example environment file and setting up the necessary environment variables:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cp .flaskenv.example .flaskenv
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then, edit the `.flaskenv` file to match your settings.
|
|
45
|
+
|
|
46
|
+
### Environment Variables
|
|
47
|
+
|
|
48
|
+
Here's an overview of the environment variables:
|
|
49
|
+
|
|
50
|
+
- **SERVER_URL**: The KaaS server API address for the main interaction within the CLI tool. This is a required field. For local development, use `http://localhost:5000`.
|
|
51
|
+
- **DEFAULT_DIRECTORY**: The folder path for artifacts. This is an optional field. You can leave it empty.
|
|
52
|
+
- **DEFAULT_PROJECT_ID**: Artifacts should be associated with a project ID. This is an optional field. You can leave it empty.
|
|
53
|
+
- **DEFAULT_TOKEN**: If the user is not the owner of the project, they are required to provide a security token. This is an optional field. You can leave it empty.
|
|
54
|
+
|
|
55
|
+
### Usage
|
|
56
|
+
|
|
57
|
+
After installing the dependencies with `poetry install`, you can spawn a shell using `poetry shell`, or alternatively, use `make`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
make shell
|
|
61
|
+
kaas-cli hello
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To verify the installation, run `kaas-cli hello`. If you see the message `Hello World!`, the CLI is set up correctly.
|
|
65
|
+
|
|
66
|
+
### Documentation
|
|
67
|
+
|
|
68
|
+
For detailed usage instructions of the `kaas-cli` tool, please refer to the official [documentation](https://docs.runtimeverification.com/kaas/guides/getting-started).
|
|
69
|
+
|
kaas_cli-0.1.1/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# KaaS CLI
|
|
2
|
+
|
|
3
|
+
## For Developers
|
|
4
|
+
|
|
5
|
+
Prerequisites: `python >= 3.10`, `pip >= 20.0.2`, `poetry >= 1.3.2`.
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
To install the package using `pip`, start by building it:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
make build
|
|
13
|
+
pip install dist/*.whl
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Configure the CLI by copying the example environment file and setting up the necessary environment variables:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cp .flaskenv.example .flaskenv
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then, edit the `.flaskenv` file to match your settings.
|
|
23
|
+
|
|
24
|
+
### Environment Variables
|
|
25
|
+
|
|
26
|
+
Here's an overview of the environment variables:
|
|
27
|
+
|
|
28
|
+
- **SERVER_URL**: The KaaS server API address for the main interaction within the CLI tool. This is a required field. For local development, use `http://localhost:5000`.
|
|
29
|
+
- **DEFAULT_DIRECTORY**: The folder path for artifacts. This is an optional field. You can leave it empty.
|
|
30
|
+
- **DEFAULT_PROJECT_ID**: Artifacts should be associated with a project ID. This is an optional field. You can leave it empty.
|
|
31
|
+
- **DEFAULT_TOKEN**: If the user is not the owner of the project, they are required to provide a security token. This is an optional field. You can leave it empty.
|
|
32
|
+
|
|
33
|
+
### Usage
|
|
34
|
+
|
|
35
|
+
After installing the dependencies with `poetry install`, you can spawn a shell using `poetry shell`, or alternatively, use `make`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
make shell
|
|
39
|
+
kaas-cli hello
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To verify the installation, run `kaas-cli hello`. If you see the message `Hello World!`, the CLI is set up correctly.
|
|
43
|
+
|
|
44
|
+
### Documentation
|
|
45
|
+
|
|
46
|
+
For detailed usage instructions of the `kaas-cli` tool, please refer to the official [documentation](https://docs.runtimeverification.com/kaas/guides/getting-started).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[project.urls]
|
|
2
|
+
Homepage = "https://github.com/runtimeverification/kaas"
|
|
3
|
+
Issues = "https://github.com/runtimeverification/kaas"
|
|
4
|
+
|
|
5
|
+
[build-system]
|
|
6
|
+
requires = ["poetry-core"]
|
|
7
|
+
build-backend = "poetry.core.masonry.api"
|
|
8
|
+
|
|
9
|
+
[tool.poetry]
|
|
10
|
+
name = "kaas-cli"
|
|
11
|
+
version = "0.1.1"
|
|
12
|
+
description = "Command line utility for K as a Service"
|
|
13
|
+
authors = [
|
|
14
|
+
"Runtime Verification, Inc. <contact@runtimeverification.com>",
|
|
15
|
+
]
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
|
|
18
|
+
[tool.poetry.dependencies]
|
|
19
|
+
python = "^3.10"
|
|
20
|
+
gql = { version = "^3.4.0", extras = [ "requests" ] }
|
|
21
|
+
urllib3 = "^2.1.0"
|
|
22
|
+
hurry-filesize = "^0.9"
|
|
23
|
+
pytest-watch = "^4.2.0"
|
|
24
|
+
python-dotenv = "^1.0.1"
|
|
25
|
+
pytest = "^8.0.1"
|
|
26
|
+
jmespath = "^1.0.1"
|
|
27
|
+
types-jmespath = "^1.0.2.20240106"
|
|
28
|
+
requests = "^2.31.0"
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.dev.dependencies]
|
|
31
|
+
autoflake = "*"
|
|
32
|
+
black = "*"
|
|
33
|
+
flake8 = "*"
|
|
34
|
+
flake8-bugbear = "*"
|
|
35
|
+
flake8-comprehensions = "*"
|
|
36
|
+
flake8-quotes = "*"
|
|
37
|
+
flake8-type-checking = "*"
|
|
38
|
+
isort = "*"
|
|
39
|
+
mypy = "*"
|
|
40
|
+
pep8-naming = "*"
|
|
41
|
+
pytest = "*"
|
|
42
|
+
pytest-cov = "*"
|
|
43
|
+
pytest-mock = "*"
|
|
44
|
+
pytest-xdist = "*"
|
|
45
|
+
pyupgrade = "*"
|
|
46
|
+
typer = "*"
|
|
47
|
+
types-requests = "*"
|
|
48
|
+
|
|
49
|
+
[tool.poetry.scripts]
|
|
50
|
+
kaas-cli = "kaas_cli.__main__:main"
|
|
51
|
+
|
|
52
|
+
[tool.isort]
|
|
53
|
+
profile = "black"
|
|
54
|
+
line_length = 120
|
|
55
|
+
|
|
56
|
+
[tool.autoflake]
|
|
57
|
+
recursive = true
|
|
58
|
+
expand-star-imports = true
|
|
59
|
+
remove-all-unused-imports = true
|
|
60
|
+
ignore-init-module-imports = true
|
|
61
|
+
remove-duplicate-keys = true
|
|
62
|
+
remove-unused-variables = true
|
|
63
|
+
|
|
64
|
+
[tool.black]
|
|
65
|
+
line-length = 120
|
|
66
|
+
skip-string-normalization = true
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
disallow_untyped_defs = true
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
[[tool.mypy.overrides]]
|
|
73
|
+
module = "hurry.*"
|
|
74
|
+
ignore_missing_imports = true
|
|
75
|
+
|
|
76
|
+
[[tool.mypy.overrides]]
|
|
77
|
+
module = "dotenv.*"
|
|
78
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .cli import cli # Import the Click group you've defined
|
|
8
|
+
from .config import CustomHelpOption
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_command_aliases() -> None:
|
|
15
|
+
# Collect aliases and their corresponding commands in a list
|
|
16
|
+
aliases_to_register = []
|
|
17
|
+
for _command_name, command in cli.commands.items():
|
|
18
|
+
if isinstance(command, CustomHelpOption):
|
|
19
|
+
# Ensure command.callback is not None before proceeding
|
|
20
|
+
if command.callback is not None:
|
|
21
|
+
for alias in command.aliases:
|
|
22
|
+
# Avoid adding an alias if it's already a command
|
|
23
|
+
if alias not in cli.commands:
|
|
24
|
+
# Add type assertion to ensure command.callback is not None
|
|
25
|
+
callback: Callable[..., Any] = command.callback
|
|
26
|
+
aliases_to_register.append((alias, callback))
|
|
27
|
+
|
|
28
|
+
# Register collected aliases as commands
|
|
29
|
+
for alias, callback in aliases_to_register:
|
|
30
|
+
cli.command(name=alias, cls=click.Command)(callback)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> None:
|
|
34
|
+
register_command_aliases()
|
|
35
|
+
cli()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .client import KaasClient
|
|
8
|
+
from .config import DEFAULT_DIRECTORY, DEFAULT_PROJECT_ID, DEFAULT_TOKEN, SERVER_URL, CustomHelpOption
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from click.core import Context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
@click.option("--url", "-u", default=SERVER_URL, show_default=True, help="Server URL")
|
|
16
|
+
@click.option("--token", "-t", default=DEFAULT_TOKEN, help="Personal access token for vault")
|
|
17
|
+
@click.option("--vault", "-v", default=DEFAULT_PROJECT_ID, help="Vault hash")
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def cli(ctx: Context, url: str, token: str | None, vault: str | None) -> None:
|
|
20
|
+
"""KaaS Command Line Interface"""
|
|
21
|
+
ctx.ensure_object(dict)
|
|
22
|
+
ctx.obj["client"] = KaasClient(url=url, token=token, vault=vault)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.command(cls=CustomHelpOption, help="Upload proofs to the remote server.")
|
|
26
|
+
@click.option(
|
|
27
|
+
"-d",
|
|
28
|
+
"--directory",
|
|
29
|
+
default=DEFAULT_DIRECTORY,
|
|
30
|
+
show_default=True,
|
|
31
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
32
|
+
help="Directory containing proofs to upload",
|
|
33
|
+
)
|
|
34
|
+
@click.option("--url", "-u", default=SERVER_URL, show_default=True, help="Server URL")
|
|
35
|
+
@click.option("--token", "-t", default=DEFAULT_TOKEN, help="Personal access token for vault")
|
|
36
|
+
@click.option("--vault", "-v", default=DEFAULT_PROJECT_ID, help="Vault hash")
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def upload(ctx: Context, directory: str, url: str | None, token: str | None, vault: str | None) -> None:
|
|
39
|
+
ctx.ensure_object(dict)
|
|
40
|
+
client: KaasClient = KaasClient(url=url, token=token, vault=vault)
|
|
41
|
+
ctx.obj["client"] = client
|
|
42
|
+
response_message = client.upload_files_s3(directory=directory)
|
|
43
|
+
click.echo(response_message)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cli.command(cls=CustomHelpOption, help="Download proofs from the remote server.")
|
|
47
|
+
@click.option(
|
|
48
|
+
"-d",
|
|
49
|
+
"--directory",
|
|
50
|
+
default=DEFAULT_DIRECTORY,
|
|
51
|
+
show_default=True,
|
|
52
|
+
type=click.Path(exists=False, file_okay=False, dir_okay=True),
|
|
53
|
+
help="Directory to save downloaded proofs",
|
|
54
|
+
)
|
|
55
|
+
@click.option("--url", "-u", default=SERVER_URL, show_default=True, help="Server URL")
|
|
56
|
+
@click.option("--token", "-t", default=DEFAULT_TOKEN, help="Personal access token for vault")
|
|
57
|
+
@click.option("--vault", "-v", default=DEFAULT_PROJECT_ID, help="Vault hash")
|
|
58
|
+
@click.argument("version_address", required=False)
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def download(
|
|
61
|
+
ctx: Context,
|
|
62
|
+
version_address: str | None,
|
|
63
|
+
directory: str, url: str | None, token: str | None, vault: str | None
|
|
64
|
+
) -> None: # type: ignore
|
|
65
|
+
ctx.ensure_object(dict)
|
|
66
|
+
client: KaasClient = KaasClient(url=url, token=token, vault=vault)
|
|
67
|
+
ctx.obj["client"] = client
|
|
68
|
+
if not version_address:
|
|
69
|
+
message = client.download_last_version(target_directory=directory)
|
|
70
|
+
if message:
|
|
71
|
+
click.echo(message)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
message = client.download_version(version_address, target_directory=directory)
|
|
75
|
+
if message:
|
|
76
|
+
click.echo(message)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cli.command(cls=CustomHelpOption, help="Say hello.")
|
|
80
|
+
@click.option("-n", "--name", default="World", show_default=True, help="Name to say hello to")
|
|
81
|
+
@click.pass_context
|
|
82
|
+
def hello(ctx: Context, name: str) -> None:
|
|
83
|
+
client: KaasClient = ctx.obj["client"]
|
|
84
|
+
|
|
85
|
+
response = client.hello(name=name)
|
|
86
|
+
click.echo(response)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cli.command(cls=CustomHelpOption, help="Login to the system.")
|
|
90
|
+
@click.pass_context
|
|
91
|
+
def login(ctx: Context) -> None:
|
|
92
|
+
client: KaasClient = ctx.obj["client"]
|
|
93
|
+
data = client.login()
|
|
94
|
+
click.echo(f"Your user code: {data.user_code}")
|
|
95
|
+
click.echo(f"Open the link and type your code {data.verification_uri}")
|
|
96
|
+
click.echo("Then hit 'enter'")
|
|
97
|
+
input_value = click.prompt("Press Enter to continue or type 'q' to quit", default="", show_default=False)
|
|
98
|
+
if input_value.lower() == 'q':
|
|
99
|
+
click.echo("You left authentication")
|
|
100
|
+
return
|
|
101
|
+
click.echo("You pressed Enter. The application continues...")
|
|
102
|
+
confirm_success = client.confirm_login(data.device_code).success
|
|
103
|
+
if not confirm_success:
|
|
104
|
+
click.echo("Authentication failed")
|
|
105
|
+
return
|
|
106
|
+
click.echo("Access token received. We store it in cache folder")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@cli.command(cls=CustomHelpOption, help="List local proofs.", aliases=['l', 'ls'])
|
|
110
|
+
@click.option(
|
|
111
|
+
"-d",
|
|
112
|
+
"--directory",
|
|
113
|
+
default=DEFAULT_DIRECTORY,
|
|
114
|
+
show_default=True,
|
|
115
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
116
|
+
help="Directory to list local proofs",
|
|
117
|
+
)
|
|
118
|
+
@click.option("--remote", is_flag=True, help="List remote proofs instead of local proofs", default=False)
|
|
119
|
+
@click.pass_context
|
|
120
|
+
def list(ctx: Context, directory: str = DEFAULT_DIRECTORY, remote: bool = False) -> None:
|
|
121
|
+
client: KaasClient = ctx.obj["client"]
|
|
122
|
+
if remote:
|
|
123
|
+
proofs = client.list_remote()
|
|
124
|
+
else:
|
|
125
|
+
proofs = client.list_local_proofs(directory=directory)
|
|
126
|
+
if not proofs:
|
|
127
|
+
click.echo('No proofs found')
|
|
128
|
+
for proof in proofs:
|
|
129
|
+
click.echo(proof)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def list_remote(ctx: Context) -> None:
|
|
133
|
+
client: KaasClient = ctx.obj["client"]
|
|
134
|
+
proofs = client.list_remote()
|
|
135
|
+
for proof in proofs:
|
|
136
|
+
click.echo(proof)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@cli.command(cls=CustomHelpOption, name="check-auth", help="Check authentication status.")
|
|
140
|
+
@click.pass_context
|
|
141
|
+
def check_auth(ctx: Context) -> None:
|
|
142
|
+
client: KaasClient = ctx.obj["client"]
|
|
143
|
+
is_authenticated = client.check()
|
|
144
|
+
if is_authenticated:
|
|
145
|
+
click.echo("You are currently authenticated.")
|
|
146
|
+
else:
|
|
147
|
+
click.echo("You are not authenticated. Please log in.")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@cli.command(cls=CustomHelpOption, name="list-vaults", help="List remote vaults owned by your KaaS account.")
|
|
151
|
+
@click.pass_context
|
|
152
|
+
def list_vaults(ctx: Context) -> None:
|
|
153
|
+
client: KaasClient = ctx.obj["client"]
|
|
154
|
+
vaults = client.list_vaults()
|
|
155
|
+
|
|
156
|
+
if not vaults:
|
|
157
|
+
click.echo("No vaults")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
for vault in vaults:
|
|
161
|
+
click.echo(vault)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@cli.command(cls=CustomHelpOption, name="list-keys", help="List remote keys owned by <VAULT_ADDRESS>")
|
|
165
|
+
@click.argument("vault_address", required=True)
|
|
166
|
+
@click.pass_context
|
|
167
|
+
def list_keys(ctx: Context, vault_address: str) -> None:
|
|
168
|
+
client: KaasClient = ctx.obj["client"]
|
|
169
|
+
keys = client.list_keys(vault_address)
|
|
170
|
+
|
|
171
|
+
if not keys:
|
|
172
|
+
click.echo(f"No keys found for vault {vault_address}")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
click.echo(f"Keys for vault {vault_address}:")
|
|
176
|
+
for key in keys:
|
|
177
|
+
click.echo(key)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@cli.command(cls=CustomHelpOption, name="list-versions",
|
|
181
|
+
help="List remote versions of a cached artifact, with <VAULT_ADDRESS> as the address of the vault.")
|
|
182
|
+
@click.argument("vault_address", required=True)
|
|
183
|
+
@click.pass_context
|
|
184
|
+
def list_versions(ctx: Context, vault_address: str) -> None:
|
|
185
|
+
client: KaasClient = ctx.obj["client"]
|
|
186
|
+
versions = client.list_versions(vault_address)
|
|
187
|
+
|
|
188
|
+
if not versions:
|
|
189
|
+
click.echo(f"No versions found for vault {vault_address}")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
click.echo(f"Versions for vault {vault_address}:")
|
|
193
|
+
for version in versions:
|
|
194
|
+
click.echo(version)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@cli.command(cls=CustomHelpOption, name="logout", help="Log out from the system.")
|
|
198
|
+
@click.pass_context
|
|
199
|
+
def logout(ctx: Context) -> None:
|
|
200
|
+
client: KaasClient = ctx.obj["client"]
|
|
201
|
+
logout_success = client.logout()
|
|
202
|
+
if logout_success:
|
|
203
|
+
click.echo("You have been logged out successfully.")
|
|
204
|
+
else:
|
|
205
|
+
click.echo("Logout failed. You may not be logged in.")
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import pickle
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Final, List
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
from zipfile import ZipFile
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import jmespath
|
|
18
|
+
import requests
|
|
19
|
+
from gql import Client, gql
|
|
20
|
+
from gql.transport.requests import RequestsHTTPTransport
|
|
21
|
+
from hurry.filesize import size
|
|
22
|
+
from requests import JSONDecodeError, Session
|
|
23
|
+
|
|
24
|
+
from kaas_cli.types import KaasCliException
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .types import File_Data, Metadata
|
|
28
|
+
|
|
29
|
+
from .constants import (
|
|
30
|
+
CONFIG_LOG_PATH,
|
|
31
|
+
CONFIG_SESSION_PATH,
|
|
32
|
+
DEVICE_LOGIN_URL,
|
|
33
|
+
FILES_LIST_URL,
|
|
34
|
+
FILES_URL,
|
|
35
|
+
GRAPHQL_URL,
|
|
36
|
+
UPLOAD_FAILURE_MESSAGE,
|
|
37
|
+
UPLOAD_SUCCESS_MESSAGE,
|
|
38
|
+
USER_URL,
|
|
39
|
+
VAULTS_KEY_URL,
|
|
40
|
+
VAULTS_ROOT_URL,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class DeviceAuth:
|
|
46
|
+
device_code: str
|
|
47
|
+
expires_in: int
|
|
48
|
+
interval: int
|
|
49
|
+
user_code: str
|
|
50
|
+
verification_uri: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Confirmation:
|
|
55
|
+
success: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CustomSession(Session):
|
|
59
|
+
def get(self, *args: Any, **kwargs: Any) -> Any:
|
|
60
|
+
response = super().get(*args, **kwargs)
|
|
61
|
+
try:
|
|
62
|
+
json_response = response.json()
|
|
63
|
+
if 'error' in json_response:
|
|
64
|
+
raise ValueError(json_response['error'], json_response.get('message', 'No message provided'))
|
|
65
|
+
return json_response
|
|
66
|
+
except JSONDecodeError as e:
|
|
67
|
+
logging.error(f"GET request JSON decode failed: {e}")
|
|
68
|
+
raise e
|
|
69
|
+
except ValueError as e:
|
|
70
|
+
logging.error(f"GET request failed: {e}")
|
|
71
|
+
click.echo(f"GET request failed: {e}")
|
|
72
|
+
raise e
|
|
73
|
+
|
|
74
|
+
def post(self, *args: Any, **kwargs: Any) -> Any:
|
|
75
|
+
response = super().post(*args, **kwargs)
|
|
76
|
+
try:
|
|
77
|
+
json_response = response.json()
|
|
78
|
+
if 'error' in json_response:
|
|
79
|
+
raise ValueError(json_response['error'], json_response.get('message', 'No message provided'))
|
|
80
|
+
return json_response
|
|
81
|
+
except JSONDecodeError as e:
|
|
82
|
+
logging.error(f"POST request JSON decode failed: {e}")
|
|
83
|
+
raise e
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
logging.error(f"POST request failed: {e}")
|
|
86
|
+
click.echo(f"POST request failed: {e}")
|
|
87
|
+
raise e
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AuthenticatedSession(CustomSession):
|
|
91
|
+
def __init__(self, access_token: str) -> None:
|
|
92
|
+
super().__init__()
|
|
93
|
+
self.access_token = access_token
|
|
94
|
+
self.headers.update({'Authorization': f'Bearer {self.access_token}'})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class KaasClient:
|
|
98
|
+
_client: Client
|
|
99
|
+
_session: CustomSession
|
|
100
|
+
_url: str
|
|
101
|
+
_token: str | None
|
|
102
|
+
_vault: str | None
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
url: str,
|
|
107
|
+
*,
|
|
108
|
+
token: str | None = None,
|
|
109
|
+
vault: str | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
self._url = url
|
|
112
|
+
self._token = token
|
|
113
|
+
self._vault = vault
|
|
114
|
+
|
|
115
|
+
self._configure_logging()
|
|
116
|
+
self._setup_client()
|
|
117
|
+
self._session = CustomSession()
|
|
118
|
+
if token:
|
|
119
|
+
self._session = AuthenticatedSession(token)
|
|
120
|
+
else:
|
|
121
|
+
self._load_session_if_exists()
|
|
122
|
+
|
|
123
|
+
def _setup_client(self) -> None:
|
|
124
|
+
"""Setup the GraphQL client."""
|
|
125
|
+
transport = RequestsHTTPTransport(
|
|
126
|
+
url=f'{self._url}{GRAPHQL_URL}',
|
|
127
|
+
verify=True,
|
|
128
|
+
)
|
|
129
|
+
self._client = Client(transport=transport, fetch_schema_from_transport=True)
|
|
130
|
+
|
|
131
|
+
def _configure_logging(self) -> None:
|
|
132
|
+
"""Configure logging for the application."""
|
|
133
|
+
if not CONFIG_LOG_PATH.exists():
|
|
134
|
+
CONFIG_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
CONFIG_LOG_PATH.touch()
|
|
136
|
+
logging.basicConfig(
|
|
137
|
+
filename=CONFIG_LOG_PATH,
|
|
138
|
+
filemode='a',
|
|
139
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
140
|
+
level=logging.DEBUG,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _load_session_if_exists(self) -> None:
|
|
144
|
+
"""Load session if the session file exists."""
|
|
145
|
+
if CONFIG_SESSION_PATH.exists():
|
|
146
|
+
self._load_session()
|
|
147
|
+
|
|
148
|
+
def hello(self, name: str | None = None) -> str:
|
|
149
|
+
response = self._client.execute(QUERY_HELLO, variable_values={'name': name} if name is not None else {})
|
|
150
|
+
return response['hello']
|
|
151
|
+
|
|
152
|
+
def _save_session(self, file_path: Path = CONFIG_SESSION_PATH) -> None:
|
|
153
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
with file_path.open('wb') as file:
|
|
155
|
+
pickle.dump(self._session, file)
|
|
156
|
+
|
|
157
|
+
def _load_session(self, file_path: Path = CONFIG_SESSION_PATH) -> None:
|
|
158
|
+
with file_path.open('rb') as file:
|
|
159
|
+
self._session = pickle.load(file)
|
|
160
|
+
|
|
161
|
+
def _remove_session(self, file_path: Path = CONFIG_SESSION_PATH) -> bool:
|
|
162
|
+
file_path.unlink()
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _list_local_files(self, directory: str) -> list[Path]:
|
|
166
|
+
return [path for path in Path(directory).glob('**/*') if path.is_file()]
|
|
167
|
+
|
|
168
|
+
def list_local_proofs(self, directory: str) -> list[dict[str, Any]]:
|
|
169
|
+
list_local_files = [
|
|
170
|
+
{
|
|
171
|
+
'name': self._read_proof(path / 'proof.json').get('id'),
|
|
172
|
+
'size': size(sum(f.stat().st_size for f in path.glob('**/*') if f.is_file())),
|
|
173
|
+
'last_update_date': self._read_proof(path / 'proof.json').get('last_update_date'),
|
|
174
|
+
}
|
|
175
|
+
for path in Path(directory).glob('**/*')
|
|
176
|
+
if path.is_dir() and len(path.name.split(':')) == 2
|
|
177
|
+
]
|
|
178
|
+
return list_local_files
|
|
179
|
+
|
|
180
|
+
def _read_proof(self, proof_path: Path) -> dict[str, Any]:
|
|
181
|
+
if not proof_path.exists():
|
|
182
|
+
return {'id': None, 'last_update_date': None}
|
|
183
|
+
|
|
184
|
+
with proof_path.open() as file:
|
|
185
|
+
data = json.load(file)
|
|
186
|
+
return {
|
|
187
|
+
'id': data.get('id'),
|
|
188
|
+
'last_update_date': datetime.fromtimestamp(os.path.getmtime(proof_path)).isoformat(),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def list_remote(self) -> Any:
|
|
192
|
+
try:
|
|
193
|
+
json_data = self._session.get(url=f'{self._url}{USER_URL}')
|
|
194
|
+
except Exception:
|
|
195
|
+
raise KaasCliException("List remote proofs failed") from None
|
|
196
|
+
return json_data
|
|
197
|
+
|
|
198
|
+
def _get_default_vault(self) -> str | None:
|
|
199
|
+
try:
|
|
200
|
+
json_data = self._session.get(url=f'{self._url}{USER_URL}')
|
|
201
|
+
except Exception:
|
|
202
|
+
raise KaasCliException("Get default vault failed") from None
|
|
203
|
+
vault_hash = jmespath.search('vaults[0].hash', json_data)
|
|
204
|
+
return vault_hash
|
|
205
|
+
|
|
206
|
+
def _get_upload_urls(self, metadata: dict[str, Any], vault: str) -> dict[str, Any]:
|
|
207
|
+
data = self._session.post(
|
|
208
|
+
url=f'{self._url}{FILES_URL.format(vault)}',
|
|
209
|
+
data=json.dumps(metadata),
|
|
210
|
+
headers={
|
|
211
|
+
'Content-Type': 'application/json',
|
|
212
|
+
},
|
|
213
|
+
)["data"]
|
|
214
|
+
return data
|
|
215
|
+
|
|
216
|
+
def _upload_presigned_url(self, files: dict[str, Any], directory: str = '') -> dict[str, Any]:
|
|
217
|
+
upload_results: Any = {}
|
|
218
|
+
for file_path_str, url in files.items():
|
|
219
|
+
file_path = Path(directory) / file_path_str
|
|
220
|
+
with file_path.open('rb') as file:
|
|
221
|
+
response = requests.put(url, data=file)
|
|
222
|
+
|
|
223
|
+
# Check if the upload was successful
|
|
224
|
+
if response.status_code in [200, 201]: # Success status codes can vary, e.g., 200 OK or 201 Created
|
|
225
|
+
print(f"Successfully uploaded: {file_path}")
|
|
226
|
+
upload_results[file_path_str] = {'success': True, 'status_code': response.status_code}
|
|
227
|
+
else:
|
|
228
|
+
print(f"Failed to upload {file_path.name} to {url}. Status code: {response.status_code}")
|
|
229
|
+
upload_results[file_path_str] = {
|
|
230
|
+
'success': False,
|
|
231
|
+
'status_code': response.status_code,
|
|
232
|
+
'error_message': response.text,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return upload_results
|
|
236
|
+
|
|
237
|
+
def archive_files(self, file_list: List[Path], archive_name: str, archive_format: str = 'zip',
|
|
238
|
+
root_dir: Path = None) -> str:
|
|
239
|
+
"""
|
|
240
|
+
Archives a list of files into a single archive file while preserving the directory structure.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
file_list (List[Path]): List of file paths to include in the archive.
|
|
244
|
+
archive_name (str): The name of the output archive file without extension.
|
|
245
|
+
archive_format (str): Format of the archive ('zip', 'tar', etc.). Default is 'zip'.
|
|
246
|
+
root_dir (Path): The root directory to use for preserving the relative paths. If None, the common parent of all files will be used.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
str: The path to the created archive file.
|
|
250
|
+
"""
|
|
251
|
+
if root_dir is None:
|
|
252
|
+
# Find the common parent directory of all files
|
|
253
|
+
common_paths = Path(file_list[0]).parents
|
|
254
|
+
for file_path in file_list[1:]:
|
|
255
|
+
common_paths = set(common_paths) & set(Path(file_path).parents)
|
|
256
|
+
root_dir = min(common_paths, key=lambda p: len(p.parts))
|
|
257
|
+
|
|
258
|
+
# Ensure the archive name does not include an extension
|
|
259
|
+
archive_name = Path(archive_name).with_suffix('')
|
|
260
|
+
|
|
261
|
+
# Create a temporary directory to hold the files to be archived
|
|
262
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
263
|
+
temp_dir_path = Path(temp_dir)
|
|
264
|
+
for file_path in file_list:
|
|
265
|
+
if file_path.is_file():
|
|
266
|
+
# Calculate relative path to the root_dir
|
|
267
|
+
relative_path = file_path.relative_to(root_dir)
|
|
268
|
+
# Create any necessary directories
|
|
269
|
+
(temp_dir_path / relative_path).parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
# Copy file to the new location preserving the folder structure
|
|
271
|
+
shutil.copy(file_path, temp_dir_path / relative_path)
|
|
272
|
+
|
|
273
|
+
# Create the archive from the temporary directory
|
|
274
|
+
archive_path = shutil.make_archive(archive_name, archive_format, root_dir=temp_dir_path)
|
|
275
|
+
|
|
276
|
+
# Move the archive to the desired directory and return the new path
|
|
277
|
+
final_archive_path = Path(archive_name).with_suffix(f'.{archive_format}')
|
|
278
|
+
shutil.move(archive_path, final_archive_path)
|
|
279
|
+
|
|
280
|
+
return str(final_archive_path)
|
|
281
|
+
|
|
282
|
+
def get_archive_metadata(self, archive_path: str) -> dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Extracts metadata from an archive file.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
archive_path (str): Path to the archive file.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
dict[str, Any]: Metadata extracted from the archive file.
|
|
291
|
+
"""
|
|
292
|
+
# Initialize an empty dictionary for metadata
|
|
293
|
+
metadata = {}
|
|
294
|
+
|
|
295
|
+
# Create a Path object from the archive_path
|
|
296
|
+
archive_file = Path(archive_path)
|
|
297
|
+
|
|
298
|
+
# Check if the archive file exists
|
|
299
|
+
if not archive_file.exists():
|
|
300
|
+
raise FileNotFoundError(f"The archive file {archive_path} does not exist.")
|
|
301
|
+
# metadata[file_uuid] = {'filename': file_name_str, 'updated_at': updated_at}
|
|
302
|
+
# Try to extract metadata from the archive file
|
|
303
|
+
try:
|
|
304
|
+
metadata[archive_file.name] = {
|
|
305
|
+
'filename': archive_file.name,
|
|
306
|
+
'updated_at': datetime.fromtimestamp(archive_file.stat().st_mtime).isoformat(),
|
|
307
|
+
}
|
|
308
|
+
except Exception as e:
|
|
309
|
+
# Handle any exception that might occur during metadata extraction
|
|
310
|
+
raise e
|
|
311
|
+
|
|
312
|
+
return metadata
|
|
313
|
+
|
|
314
|
+
def get_version_from_digest(self, file_list) -> str:
|
|
315
|
+
"""
|
|
316
|
+
Reads a JSON file named 'digest' from the provided file list, extracts hash values from specified fields, and returns them as a string.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
file_list (list[Path]): List of file paths.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
str: A string containing the hash values from the 'digest' file.
|
|
323
|
+
"""
|
|
324
|
+
# Find the 'digest' file in the file list
|
|
325
|
+
digest_file = next((file for file in file_list if file.name == 'digest'), None)
|
|
326
|
+
if digest_file is None:
|
|
327
|
+
raise FileNotFoundError("No 'digest' file found in the provided file list.")
|
|
328
|
+
|
|
329
|
+
# Read the 'digest' JSON file
|
|
330
|
+
with open(digest_file, 'r') as file:
|
|
331
|
+
data = json.load(file)
|
|
332
|
+
|
|
333
|
+
# Extract the hash values from specified fields
|
|
334
|
+
kompilation_hash = data.get('kompilation', 'Not found')
|
|
335
|
+
foundry_hash = data.get('foundry', 'Not found')
|
|
336
|
+
|
|
337
|
+
# Return the hash values as a string
|
|
338
|
+
return f"{kompilation_hash}{foundry_hash}"
|
|
339
|
+
|
|
340
|
+
def upload_files_s3(self, directory: str) -> str:
|
|
341
|
+
file_list = self._list_local_files(directory)
|
|
342
|
+
if not file_list or len(file_list) == 0:
|
|
343
|
+
raise ValueError(f'No files to upload in dir: {directory}')
|
|
344
|
+
|
|
345
|
+
archive_name = self.get_version_from_digest(file_list)
|
|
346
|
+
archive_path = self.archive_files(file_list, archive_name, archive_format='zip', root_dir=Path(directory))
|
|
347
|
+
metadata = self.get_archive_metadata(archive_path)
|
|
348
|
+
print(f"Uploading {metadata} to S3...")
|
|
349
|
+
vault: str | None = self._vault or self._get_default_vault()
|
|
350
|
+
if vault is None:
|
|
351
|
+
return UPLOAD_FAILURE_MESSAGE
|
|
352
|
+
urls = self._get_upload_urls(metadata=metadata, vault=vault)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
self._upload_presigned_url(urls)
|
|
356
|
+
print(UPLOAD_SUCCESS_MESSAGE)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
raise KaasCliException(f"Failed to upload {archive_path} to S3: {e}") from None
|
|
359
|
+
# remove the archive file after uploading
|
|
360
|
+
os.remove(archive_path)
|
|
361
|
+
|
|
362
|
+
def process_archive(self, archive_path: str, extract_to: str) -> None:
|
|
363
|
+
"""
|
|
364
|
+
Extracts files from an archive to a specified directory and then removes the archive.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
archive_path (str): The path to the archive file.
|
|
368
|
+
extract_to (str): The directory to extract the files to.
|
|
369
|
+
"""
|
|
370
|
+
# Ensure the target directory exists
|
|
371
|
+
os.makedirs(extract_to, exist_ok=True)
|
|
372
|
+
|
|
373
|
+
# Extract the archive
|
|
374
|
+
with ZipFile(archive_path, 'r') as zip_ref:
|
|
375
|
+
zip_ref.extractall(extract_to)
|
|
376
|
+
|
|
377
|
+
# Remove the archive file after extraction
|
|
378
|
+
os.remove(archive_path)
|
|
379
|
+
print(f"Extracted and removed archive {archive_path}")
|
|
380
|
+
|
|
381
|
+
def download_version(self, version_address: str, target_directory: str) -> Any:
|
|
382
|
+
vault_address = version_address.split('/')[0]
|
|
383
|
+
version = version_address.split('/')[1]
|
|
384
|
+
urls = self._get_download_url(vault_address)['data']
|
|
385
|
+
url = urls[version_address]['url']
|
|
386
|
+
if not url:
|
|
387
|
+
return f'Version {version_address} not found'
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
downloaded_file_path = self.replace_path(version, target_directory)
|
|
391
|
+
|
|
392
|
+
self.download_file(url, self.replace_path(version, target_directory))
|
|
393
|
+
self.process_archive(downloaded_file_path, target_directory)
|
|
394
|
+
except Exception:
|
|
395
|
+
raise KaasCliException(f"Download {version} failed") from None
|
|
396
|
+
|
|
397
|
+
return f'Version {version} downloaded to {target_directory}'
|
|
398
|
+
|
|
399
|
+
def download_last_version(self, target_directory: str) -> Any:
|
|
400
|
+
vault_hash = self._vault or self._get_default_vault()
|
|
401
|
+
if not vault_hash:
|
|
402
|
+
return "Can't find default vault"
|
|
403
|
+
|
|
404
|
+
urls = self._get_download_url(vault_hash)
|
|
405
|
+
|
|
406
|
+
urls_data = urls.get('data', {})
|
|
407
|
+
if not urls_data:
|
|
408
|
+
return 'Something went wrong'
|
|
409
|
+
|
|
410
|
+
# Find the file with the latest date
|
|
411
|
+
latest_file = max(urls_data.items(), key=lambda x: x[1]['lastModified'])[0]
|
|
412
|
+
url = urls_data[latest_file]['url']
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
downloaded_file_path = self.replace_path(latest_file, target_directory)
|
|
416
|
+
self.download_file(url, self.replace_path(latest_file, target_directory))
|
|
417
|
+
self.process_archive(downloaded_file_path, target_directory)
|
|
418
|
+
except Exception:
|
|
419
|
+
raise KaasCliException(f"Download {latest_file} failed") from None
|
|
420
|
+
|
|
421
|
+
return f'Latest version of {latest_file} downloaded to {target_directory}'
|
|
422
|
+
|
|
423
|
+
def process_archive(self, archive_path: str, extract_to: str) -> None:
|
|
424
|
+
"""
|
|
425
|
+
Extracts files from an archive to a specified directory and then removes the archive.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
archive_path (str): The path to the archive file.
|
|
429
|
+
extract_to (str): The directory to extract the files to.
|
|
430
|
+
"""
|
|
431
|
+
# Ensure the target directory exists
|
|
432
|
+
os.makedirs(extract_to, exist_ok=True)
|
|
433
|
+
|
|
434
|
+
# Extract the archive
|
|
435
|
+
with ZipFile(archive_path, 'r') as zip_ref:
|
|
436
|
+
zip_ref.extractall(extract_to)
|
|
437
|
+
|
|
438
|
+
# Remove the archive file after extraction
|
|
439
|
+
os.remove(archive_path)
|
|
440
|
+
print(f"Extracted and removed archive {archive_path}")
|
|
441
|
+
|
|
442
|
+
def download_last_version(self, target_directory: str) -> Any:
|
|
443
|
+
vault_hash = self._vault or self._get_default_vault()
|
|
444
|
+
if not vault_hash:
|
|
445
|
+
return "Can't find default vault"
|
|
446
|
+
|
|
447
|
+
urls = self._get_download_url(vault_hash)
|
|
448
|
+
|
|
449
|
+
urls_data = urls.get('data', {})
|
|
450
|
+
if not urls_data:
|
|
451
|
+
return 'Something went wrong'
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# Find the file with the latest date
|
|
455
|
+
latest_file = max(urls_data.items(), key=lambda x: x[1]['lastModified'])[0]
|
|
456
|
+
url = urls_data[latest_file]['url']
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
downloaded_file_path = self.replace_path(latest_file, target_directory)
|
|
460
|
+
self.download_file(url, self.replace_path(latest_file, target_directory))
|
|
461
|
+
self.process_archive(downloaded_file_path, target_directory)
|
|
462
|
+
except Exception:
|
|
463
|
+
raise KaasCliException(f"Download {latest_file} failed") from None
|
|
464
|
+
|
|
465
|
+
return f'Latest version of {latest_file} downloaded to {target_directory}'
|
|
466
|
+
|
|
467
|
+
def download_vault(self, target_directory: str) -> Any:
|
|
468
|
+
vault_hash = self._vault or self._get_default_vault()
|
|
469
|
+
if not vault_hash:
|
|
470
|
+
return "Can't find default vault"
|
|
471
|
+
|
|
472
|
+
urls = self._get_download_url(vault_hash)
|
|
473
|
+
|
|
474
|
+
urls_data = urls.get('data', {})
|
|
475
|
+
if not urls_data:
|
|
476
|
+
return 'Something went wrong'
|
|
477
|
+
|
|
478
|
+
for path in urls_data:
|
|
479
|
+
print(path)
|
|
480
|
+
|
|
481
|
+
url = urls_data[path]
|
|
482
|
+
try:
|
|
483
|
+
self.download_file(url, self.replace_path(path, target_directory))
|
|
484
|
+
except Exception:
|
|
485
|
+
raise KaasCliException(f"Download {path} failed") from None
|
|
486
|
+
|
|
487
|
+
return f'{len(urls_data)} Files downloaded to {target_directory}'
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def _get_upload_data(file_list: list[Path], directory: Path) -> tuple[File_Data, Metadata]:
|
|
491
|
+
data: File_Data = {}
|
|
492
|
+
metadata = {}
|
|
493
|
+
|
|
494
|
+
for file_path in file_list:
|
|
495
|
+
file_uuid = str(uuid4())
|
|
496
|
+
file_name_str = Path(file_path).relative_to(directory).as_posix()
|
|
497
|
+
with file_path.open('rb') as file_object:
|
|
498
|
+
data[f'file_{file_uuid}'] = (file_name_str, file_object.read(), None)
|
|
499
|
+
updated_at = datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d %H:%M:%S')
|
|
500
|
+
# Ensure the file path is converted to a string
|
|
501
|
+
metadata[file_uuid] = {'filename': file_name_str, 'updated_at': updated_at}
|
|
502
|
+
|
|
503
|
+
data['metadata'] = (None, json.dumps(metadata), 'application/json')
|
|
504
|
+
|
|
505
|
+
return data, metadata
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
def _get_metadata(file_list: list[Path], directory: Path) -> Metadata:
|
|
509
|
+
data, metadata = KaasClient._get_upload_data(file_list, Path(directory))
|
|
510
|
+
return metadata
|
|
511
|
+
|
|
512
|
+
def login(self) -> DeviceAuth:
|
|
513
|
+
try:
|
|
514
|
+
data = self._session.post(url=f'{self._url}{DEVICE_LOGIN_URL}')
|
|
515
|
+
except Exception:
|
|
516
|
+
raise KaasCliException("Login failed") from None
|
|
517
|
+
return DeviceAuth(**data)
|
|
518
|
+
|
|
519
|
+
def confirm_login(self, device_code: str) -> Confirmation:
|
|
520
|
+
try:
|
|
521
|
+
data = self._session.get(url=f'{self._url}{DEVICE_LOGIN_URL}', params={'device_code': device_code})
|
|
522
|
+
except Exception:
|
|
523
|
+
raise KaasCliException("Login failed") from None
|
|
524
|
+
self._session = AuthenticatedSession(data['token'])
|
|
525
|
+
self._save_session()
|
|
526
|
+
return Confirmation(True)
|
|
527
|
+
|
|
528
|
+
def check(self) -> Confirmation:
|
|
529
|
+
data = self._session.get(url=f'{self._url}{USER_URL}')
|
|
530
|
+
if data:
|
|
531
|
+
return Confirmation(True)
|
|
532
|
+
else:
|
|
533
|
+
return Confirmation(False)
|
|
534
|
+
|
|
535
|
+
def _get_download_url(self, vault_hash: str) -> dict:
|
|
536
|
+
data = self._session.get(url=f'{self._url}{FILES_URL.format(vault_hash)}')
|
|
537
|
+
return data
|
|
538
|
+
|
|
539
|
+
def download_file(self, url: str, folder_path: str) -> None:
|
|
540
|
+
file_path = Path(folder_path)
|
|
541
|
+
|
|
542
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
543
|
+
with requests.get(url, stream=True) as response:
|
|
544
|
+
# Check if the request was successful
|
|
545
|
+
response.raise_for_status()
|
|
546
|
+
# Open a local file with write-binary mode in the specified folder
|
|
547
|
+
with open(file_path, 'wb') as file:
|
|
548
|
+
# Write the content of the response to the file in chunks
|
|
549
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
550
|
+
file.write(chunk)
|
|
551
|
+
|
|
552
|
+
def replace_path(self, input_string: str, target_directory: str) -> str:
|
|
553
|
+
|
|
554
|
+
# Find the index of the first dash '/'
|
|
555
|
+
dash_index = input_string.find('/')
|
|
556
|
+
|
|
557
|
+
# Replace everything up to the first dash with 'test'
|
|
558
|
+
result = target_directory + input_string[dash_index:]
|
|
559
|
+
|
|
560
|
+
return result
|
|
561
|
+
|
|
562
|
+
def read_new_files(self, files: list[str], target_directory: str) -> list[str]:
|
|
563
|
+
new_files = []
|
|
564
|
+
for file_name in files:
|
|
565
|
+
file_path = Path(target_directory, file_name)
|
|
566
|
+
if not file_path.exists():
|
|
567
|
+
new_files.append(file_name)
|
|
568
|
+
return new_files
|
|
569
|
+
|
|
570
|
+
def pull_remote_files(self, files: list[str]) -> list[str]:
|
|
571
|
+
try:
|
|
572
|
+
data = self._session.get(url=f'{self._url}{FILES_LIST_URL}', params={'files': files})
|
|
573
|
+
except Exception:
|
|
574
|
+
raise KaasCliException("Pull remote files failed") from None
|
|
575
|
+
return data
|
|
576
|
+
|
|
577
|
+
def list_vaults(self) -> list[str]:
|
|
578
|
+
try:
|
|
579
|
+
data = self._session.get(url=f'{self._url}{VAULTS_ROOT_URL}')
|
|
580
|
+
except Exception:
|
|
581
|
+
raise KaasCliException("List vaults failed") from None
|
|
582
|
+
return data
|
|
583
|
+
|
|
584
|
+
def list_keys(self, vault: str) -> list[str]:
|
|
585
|
+
try:
|
|
586
|
+
data = self._session.get(url=f'{self._url}{VAULTS_KEY_URL.format(vault)}')['data']
|
|
587
|
+
except Exception:
|
|
588
|
+
raise KaasCliException("List vaults failed") from None
|
|
589
|
+
return data
|
|
590
|
+
|
|
591
|
+
def list_versions(self, vault: str) -> list[str]:
|
|
592
|
+
try:
|
|
593
|
+
data = self._get_download_url(vault)
|
|
594
|
+
print(data)
|
|
595
|
+
except Exception:
|
|
596
|
+
raise KaasCliException("List vaults failed") from None
|
|
597
|
+
return data
|
|
598
|
+
|
|
599
|
+
def logout(self) -> bool:
|
|
600
|
+
"""
|
|
601
|
+
Log out the user by clearing the session and authentication token.
|
|
602
|
+
Returns True if the logout was successful, False otherwise.
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
return self._remove_session()
|
|
606
|
+
except Exception as e:
|
|
607
|
+
logging.error(f"Logout failed: {e}")
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
QUERY_HELLO: Final = gql(
|
|
612
|
+
"""
|
|
613
|
+
query Hello($name: String!) {
|
|
614
|
+
hello(name: $name)
|
|
615
|
+
}
|
|
616
|
+
"""
|
|
617
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from .constants import DEFAULT_K_OUT_FOLDER, DEFAULT_PROD_SERVER_URL
|
|
12
|
+
|
|
13
|
+
# Load environment variables from .flaskenv file
|
|
14
|
+
load_dotenv('.flaskenv')
|
|
15
|
+
|
|
16
|
+
DEFAULT_DIRECTORY = os.getenv('DEFAULT_DIRECTORY') or DEFAULT_K_OUT_FOLDER
|
|
17
|
+
SERVER_URL = os.getenv('SERVER_URL') or DEFAULT_PROD_SERVER_URL
|
|
18
|
+
DEFAULT_PROJECT_ID = os.getenv('DEFAULT_PROJECT_ID')
|
|
19
|
+
DEFAULT_TOKEN = os.getenv('DEFAULT_TOKEN')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CustomHelpOption(click.Command):
|
|
23
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
24
|
+
self.aliases = kwargs.pop('aliases', [])
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
def format_aliases(self) -> str:
|
|
28
|
+
return f"Aliases: {', '.join(repr(alias) for alias in self.aliases)}" if self.aliases else ""
|
|
29
|
+
|
|
30
|
+
def get_short_help_str(self, limit: int = 45) -> str:
|
|
31
|
+
short_help = super().get_short_help_str(limit)
|
|
32
|
+
aliases_text = self.format_aliases()
|
|
33
|
+
return f"{short_help}. {aliases_text}" if aliases_text else short_help
|
|
34
|
+
|
|
35
|
+
def get_help(self, ctx):
|
|
36
|
+
return super().get_help(ctx)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
CONFIG_DIR_PATH: Final = Path(__file__).parent.parent / 'config'
|
|
5
|
+
CONFIG_FILE_PATH: Final = CONFIG_DIR_PATH / 'config.ini'
|
|
6
|
+
CONFIG_LOG_PATH: Final = CONFIG_DIR_PATH / 'kaas.log'
|
|
7
|
+
CONFIG_SESSION_PATH: Final = CONFIG_DIR_PATH / 'session.pkl'
|
|
8
|
+
|
|
9
|
+
GRAPHQL_URL: Final = '/graphql'
|
|
10
|
+
FILES_ROOT_URL: Final = '/api/files'
|
|
11
|
+
FILES_URL: Final = '/api/files/{}/url'
|
|
12
|
+
FILES_LIST_URL: Final = '/api/files/list'
|
|
13
|
+
FILES_UPLOAD_URL: Final = '/api/files/upload'
|
|
14
|
+
|
|
15
|
+
DEVICE_LOGIN_URL: Final = '/api/login/github/device'
|
|
16
|
+
USER_URL: Final = '/api/user/'
|
|
17
|
+
|
|
18
|
+
VAULTS_ROOT_URL: Final = '/api/vaults/'
|
|
19
|
+
VAULTS_KEY_URL: Final = '/api/vaults/{}/keys'
|
|
20
|
+
|
|
21
|
+
UPLOAD_SUCCESS_MESSAGE: Final = 'Data successfully uploaded'
|
|
22
|
+
UPLOAD_FAILURE_MESSAGE: Final = 'Failed to upload file'
|
|
23
|
+
|
|
24
|
+
DEFAULT_DEV_SERVER_URL: Final = 'http://127.0.0.1:5000'
|
|
25
|
+
DEFAULT_PROD_SERVER_URL: Final = 'https://kaas.runtimeverification.com/'
|
|
26
|
+
|
|
27
|
+
DEFAULT_K_OUT_FOLDER: Final = './kout'
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import IO, Any, Optional
|
|
4
|
+
|
|
5
|
+
from click import ClickException, echo
|
|
6
|
+
|
|
7
|
+
File_Data = dict[str, tuple[Optional[str], Any, Optional[str]]]
|
|
8
|
+
Metadata = dict[str, Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KaasCliException(ClickException):
|
|
12
|
+
def show(self, file: IO[Any] | None = None) -> None:
|
|
13
|
+
echo(f'{self.message}', file=file)
|