kscale 0.0.13__tar.gz → 0.1.1__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {kscale-0.0.13/kscale.egg-info → kscale-0.1.1}/PKG-INFO +19 -10
- {kscale-0.0.13 → kscale-0.1.1}/README.md +0 -6
- {kscale-0.0.13 → kscale-0.1.1}/kscale/__init__.py +1 -1
- {kscale-0.0.13 → kscale-0.1.1}/kscale/api.py +0 -3
- kscale-0.1.1/kscale/cli.py +32 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/conf.py +4 -1
- {kscale-0.0.13 → kscale-0.1.1}/kscale/requirements.txt +11 -2
- kscale-0.1.1/kscale/web/api.py +14 -0
- kscale-0.1.1/kscale/web/cli/robot.py +100 -0
- kscale-0.1.1/kscale/web/cli/robot_class.py +113 -0
- kscale-0.1.1/kscale/web/cli/token.py +33 -0
- kscale-0.1.1/kscale/web/cli/user.py +33 -0
- kscale-0.1.1/kscale/web/clients/__init__.py +0 -0
- kscale-0.1.1/kscale/web/clients/base.py +314 -0
- kscale-0.1.1/kscale/web/clients/client.py +11 -0
- kscale-0.1.1/kscale/web/clients/robot.py +39 -0
- kscale-0.1.1/kscale/web/clients/robot_class.py +121 -0
- kscale-0.1.1/kscale/web/clients/user.py +10 -0
- kscale-0.1.1/kscale/web/gen/__init__.py +0 -0
- kscale-0.1.1/kscale/web/gen/api.py +73 -0
- kscale-0.1.1/kscale/web/utils.py +31 -0
- {kscale-0.0.13 → kscale-0.1.1/kscale.egg-info}/PKG-INFO +19 -10
- {kscale-0.0.13 → kscale-0.1.1}/kscale.egg-info/SOURCES.txt +11 -5
- {kscale-0.0.13 → kscale-0.1.1}/kscale.egg-info/entry_points.txt +1 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale.egg-info/requires.txt +9 -2
- {kscale-0.0.13 → kscale-0.1.1}/pyproject.toml +2 -2
- {kscale-0.0.13 → kscale-0.1.1}/setup.py +1 -18
- kscale-0.0.13/kscale/cli.py +0 -25
- kscale-0.0.13/kscale/web/api.py +0 -98
- kscale-0.0.13/kscale/web/gen/api.py +0 -612
- kscale-0.0.13/kscale/web/kernels.py +0 -207
- kscale-0.0.13/kscale/web/krec.py +0 -175
- kscale-0.0.13/kscale/web/pybullet.py +0 -188
- kscale-0.0.13/kscale/web/urdf.py +0 -185
- kscale-0.0.13/kscale/web/utils.py +0 -48
- kscale-0.0.13/kscale/web/www_client.py +0 -134
- {kscale-0.0.13 → kscale-0.1.1}/LICENSE +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/MANIFEST.in +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/artifacts/__init__.py +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/artifacts/plane.obj +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/artifacts/plane.urdf +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/py.typed +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/requirements-dev.txt +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/utils/__init__.py +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/utils/api_base.py +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/utils/checksum.py +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/utils/cli.py +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale/web/__init__.py +0 -0
- {kscale-0.0.13/kscale/web/gen → kscale-0.1.1/kscale/web/cli}/__init__.py +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale.egg-info/dependency_links.txt +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale.egg-info/not-zip-safe +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/kscale.egg-info/top_level.txt +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/setup.cfg +0 -0
- {kscale-0.0.13 → kscale-0.1.1}/tests/test_dummy.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: kscale
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: The kscale project
|
5
5
|
Home-page: https://github.com/kscalelabs/kscale
|
6
6
|
Author: Benjamin Bolte
|
@@ -9,11 +9,18 @@ Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
10
10
|
Requires-Dist: omegaconf
|
11
11
|
Requires-Dist: email_validator
|
12
|
+
Requires-Dist: aiohttp
|
13
|
+
Requires-Dist: cryptography
|
12
14
|
Requires-Dist: httpx
|
13
|
-
Requires-Dist: requests
|
14
15
|
Requires-Dist: pydantic
|
15
|
-
Requires-Dist:
|
16
|
+
Requires-Dist: pyjwt
|
17
|
+
Requires-Dist: requests
|
18
|
+
Requires-Dist: yarl
|
16
19
|
Requires-Dist: aiofiles
|
20
|
+
Requires-Dist: click
|
21
|
+
Requires-Dist: colorlogging
|
22
|
+
Requires-Dist: tabulate
|
23
|
+
Requires-Dist: async-lru
|
17
24
|
Requires-Dist: krec
|
18
25
|
Provides-Extra: dev
|
19
26
|
Requires-Dist: black; extra == "dev"
|
@@ -22,12 +29,14 @@ Requires-Dist: mypy; extra == "dev"
|
|
22
29
|
Requires-Dist: pytest; extra == "dev"
|
23
30
|
Requires-Dist: ruff; extra == "dev"
|
24
31
|
Requires-Dist: datamodel-code-generator; extra == "dev"
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
Dynamic: author
|
33
|
+
Dynamic: description
|
34
|
+
Dynamic: description-content-type
|
35
|
+
Dynamic: home-page
|
36
|
+
Dynamic: provides-extra
|
37
|
+
Dynamic: requires-dist
|
38
|
+
Dynamic: requires-python
|
39
|
+
Dynamic: summary
|
31
40
|
|
32
41
|
<div align="center">
|
33
42
|
|
@@ -1,9 +1,3 @@
|
|
1
|
-
<p align="center">
|
2
|
-
<picture>
|
3
|
-
<img alt="K-Scale Open Source Robotics" src="https://media.kscale.dev/kscale-open-source-header.png" style="max-width: 100%;">
|
4
|
-
</picture>
|
5
|
-
</p>
|
6
|
-
|
7
1
|
<div align="center">
|
8
2
|
|
9
3
|
[![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/kscalelabs/ksim/blob/main/LICENSE)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"""Defines the top-level KOL CLI."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import click
|
6
|
+
import colorlogging
|
7
|
+
|
8
|
+
from kscale.utils.cli import recursive_help
|
9
|
+
from kscale.web.cli.robot import cli as robot_cli
|
10
|
+
from kscale.web.cli.robot_class import cli as robot_class_cli
|
11
|
+
from kscale.web.cli.token import cli as token_cli
|
12
|
+
from kscale.web.cli.user import cli as user_cli
|
13
|
+
|
14
|
+
|
15
|
+
@click.group()
|
16
|
+
def cli() -> None:
|
17
|
+
"""Command line interface for interacting with the K-Scale web API."""
|
18
|
+
colorlogging.configure()
|
19
|
+
|
20
|
+
# Suppress aiohttp access logging
|
21
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
22
|
+
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
23
|
+
|
24
|
+
|
25
|
+
cli.add_command(token_cli, "token")
|
26
|
+
cli.add_command(user_cli, "user")
|
27
|
+
cli.add_command(robot_class_cli, "robots")
|
28
|
+
cli.add_command(robot_cli, "robot")
|
29
|
+
|
30
|
+
if __name__ == "__main__":
|
31
|
+
# python -m kscale.cli
|
32
|
+
print(recursive_help(cli))
|
@@ -8,6 +8,9 @@ from pathlib import Path
|
|
8
8
|
|
9
9
|
from omegaconf import II, OmegaConf
|
10
10
|
|
11
|
+
# This is the public API endpoint for the K-Scale WWW API.
|
12
|
+
DEFAULT_API_ROOT = "https://api.kscale.dev"
|
13
|
+
|
11
14
|
|
12
15
|
def get_path() -> Path:
|
13
16
|
if "KSCALE_CONFIG_DIR" in os.environ:
|
@@ -17,7 +20,7 @@ def get_path() -> Path:
|
|
17
20
|
|
18
21
|
@dataclass
|
19
22
|
class WWWSettings:
|
20
|
-
|
23
|
+
api_root: str = field(default=DEFAULT_API_ROOT)
|
21
24
|
cache_dir: str = field(default=II("oc.env:KSCALE_CACHE_DIR,'~/.kscale/cache/'"))
|
22
25
|
|
23
26
|
|
@@ -5,13 +5,22 @@ omegaconf
|
|
5
5
|
email_validator
|
6
6
|
|
7
7
|
# HTTP requests
|
8
|
+
aiohttp
|
9
|
+
cryptography
|
8
10
|
httpx
|
9
|
-
requests
|
10
11
|
pydantic
|
12
|
+
pyjwt
|
13
|
+
requests
|
14
|
+
yarl
|
11
15
|
|
12
16
|
# CLI
|
13
|
-
click
|
14
17
|
aiofiles
|
18
|
+
click
|
19
|
+
colorlogging
|
20
|
+
tabulate
|
21
|
+
|
22
|
+
# Async
|
23
|
+
async-lru
|
15
24
|
|
16
25
|
# K-Scale
|
17
26
|
krec
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""Defines a common interface for the K-Scale WWW API."""
|
2
|
+
|
3
|
+
from kscale.utils.api_base import APIBase
|
4
|
+
from kscale.web.clients.client import WWWClient
|
5
|
+
from kscale.web.gen.api import ProfileResponse
|
6
|
+
|
7
|
+
|
8
|
+
class WebAPI(APIBase):
|
9
|
+
async def www_client(self) -> WWWClient:
|
10
|
+
return WWWClient()
|
11
|
+
|
12
|
+
async def get_profile_info(self) -> ProfileResponse:
|
13
|
+
client = await self.www_client()
|
14
|
+
return await client.get_profile_info()
|
@@ -0,0 +1,100 @@
|
|
1
|
+
"""Defines the CLI for getting information about robots."""
|
2
|
+
|
3
|
+
import click
|
4
|
+
from tabulate import tabulate
|
5
|
+
|
6
|
+
from kscale.utils.cli import coro
|
7
|
+
from kscale.web.clients.robot import RobotClient
|
8
|
+
|
9
|
+
|
10
|
+
@click.group()
|
11
|
+
def cli() -> None:
|
12
|
+
"""Get information about robots."""
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
@cli.command()
|
17
|
+
@coro
|
18
|
+
async def list() -> None:
|
19
|
+
client = RobotClient()
|
20
|
+
robots = await client.get_all_robots()
|
21
|
+
if robots:
|
22
|
+
table_data = [
|
23
|
+
[
|
24
|
+
click.style(robot.id, fg="blue"),
|
25
|
+
click.style(robot.robot_name, fg="green"),
|
26
|
+
click.style(robot.class_id, fg="yellow"),
|
27
|
+
robot.description or "N/A",
|
28
|
+
]
|
29
|
+
for robot in robots
|
30
|
+
]
|
31
|
+
click.echo(tabulate(table_data, headers=["ID", "Name", "Class", "Description"], tablefmt="simple"))
|
32
|
+
else:
|
33
|
+
click.echo(click.style("No robots found", fg="red"))
|
34
|
+
|
35
|
+
|
36
|
+
@cli.command()
|
37
|
+
@click.option("-u", "--user-id", type=str, default="me")
|
38
|
+
@coro
|
39
|
+
async def user(user_id: str = "me") -> None:
|
40
|
+
client = RobotClient()
|
41
|
+
robots = await client.get_user_robots(user_id)
|
42
|
+
if robots:
|
43
|
+
table_data = [
|
44
|
+
[
|
45
|
+
click.style(robot.id, fg="blue"),
|
46
|
+
click.style(robot.robot_name, fg="green"),
|
47
|
+
click.style(robot.class_id, fg="yellow"),
|
48
|
+
robot.description or "N/A",
|
49
|
+
]
|
50
|
+
for robot in robots
|
51
|
+
]
|
52
|
+
click.echo(tabulate(table_data, headers=["ID", "Name", "Class", "Description"], tablefmt="simple"))
|
53
|
+
else:
|
54
|
+
click.echo(click.style("No robots found", fg="red"))
|
55
|
+
|
56
|
+
|
57
|
+
@cli.command()
|
58
|
+
@click.argument("robot_id")
|
59
|
+
@coro
|
60
|
+
async def id(robot_id: str) -> None:
|
61
|
+
client = RobotClient()
|
62
|
+
robot = await client.get_robot_by_id(robot_id)
|
63
|
+
click.echo("Robot:")
|
64
|
+
click.echo(f" ID: {click.style(robot.id, fg='blue')}")
|
65
|
+
click.echo(f" Name: {click.style(robot.robot_name, fg='green')}")
|
66
|
+
click.echo(f" Class: {click.style(robot.class_name, fg='yellow')}")
|
67
|
+
click.echo(f" Description: {click.style(robot.description or 'N/A', fg='yellow')}")
|
68
|
+
|
69
|
+
|
70
|
+
@cli.command()
|
71
|
+
@click.argument("robot_name")
|
72
|
+
@coro
|
73
|
+
async def name(robot_name: str) -> None:
|
74
|
+
client = RobotClient()
|
75
|
+
robot = await client.get_robot_by_name(robot_name)
|
76
|
+
click.echo("Robot:")
|
77
|
+
click.echo(f" ID: {click.style(robot.id, fg='blue')}")
|
78
|
+
click.echo(f" Name: {click.style(robot.robot_name, fg='green')}")
|
79
|
+
click.echo(f" Class: {click.style(robot.class_name, fg='yellow')}")
|
80
|
+
click.echo(f" Description: {click.style(robot.description or 'N/A', fg='yellow')}")
|
81
|
+
|
82
|
+
|
83
|
+
@cli.command()
|
84
|
+
@click.argument("class_name")
|
85
|
+
@click.argument("name")
|
86
|
+
@click.option("-c", "--class-name", type=str, required=True)
|
87
|
+
@click.option("-d", "--description", type=str, default=None)
|
88
|
+
@coro
|
89
|
+
async def add(name: str, class_name: str, description: str | None = None) -> None:
|
90
|
+
client = RobotClient()
|
91
|
+
robot = await client.add_robot(name, class_name, description)
|
92
|
+
click.echo("Robot added:")
|
93
|
+
click.echo(f" ID: {click.style(robot.id, fg='blue')}")
|
94
|
+
click.echo(f" Name: {click.style(robot.robot_name, fg='green')}")
|
95
|
+
click.echo(f" Class: {click.style(robot.class_name, fg='yellow')}")
|
96
|
+
click.echo(f" Description: {click.style(robot.description or 'N/A', fg='yellow')}")
|
97
|
+
|
98
|
+
|
99
|
+
if __name__ == "__main__":
|
100
|
+
cli()
|
@@ -0,0 +1,113 @@
|
|
1
|
+
"""Defines the CLI for getting information about robot classes."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import click
|
6
|
+
from tabulate import tabulate
|
7
|
+
|
8
|
+
from kscale.utils.cli import coro
|
9
|
+
from kscale.web.clients.robot_class import RobotClassClient
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
@click.group()
|
15
|
+
def cli() -> None:
|
16
|
+
"""Get information about robot classes."""
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
@cli.command()
|
21
|
+
@coro
|
22
|
+
async def list() -> None:
|
23
|
+
"""Lists all robot classes."""
|
24
|
+
client = RobotClassClient()
|
25
|
+
robot_classes = await client.get_robot_classes()
|
26
|
+
if robot_classes:
|
27
|
+
# Prepare table data
|
28
|
+
table_data = [
|
29
|
+
[
|
30
|
+
click.style(rc.id, fg="blue"),
|
31
|
+
click.style(rc.class_name, fg="green"),
|
32
|
+
rc.description or "N/A",
|
33
|
+
]
|
34
|
+
for rc in robot_classes
|
35
|
+
]
|
36
|
+
click.echo(tabulate(table_data, headers=["ID", "Name", "Description"], tablefmt="simple"))
|
37
|
+
else:
|
38
|
+
click.echo(click.style("No robot classes found", fg="red"))
|
39
|
+
|
40
|
+
|
41
|
+
@cli.command()
|
42
|
+
@click.argument("name")
|
43
|
+
@click.option("-d", "--description", type=str, default=None)
|
44
|
+
@coro
|
45
|
+
async def add(
|
46
|
+
name: str,
|
47
|
+
description: str | None = None,
|
48
|
+
) -> None:
|
49
|
+
"""Adds a new robot class."""
|
50
|
+
async with RobotClassClient() as client:
|
51
|
+
robot_class = await client.create_robot_class(name, description)
|
52
|
+
click.echo("Robot class created:")
|
53
|
+
click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
|
54
|
+
click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
|
55
|
+
click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
|
56
|
+
|
57
|
+
|
58
|
+
@cli.command()
|
59
|
+
@click.argument("current_name")
|
60
|
+
@click.option("-n", "--name", type=str, default=None)
|
61
|
+
@click.option("-d", "--description", type=str, default=None)
|
62
|
+
@coro
|
63
|
+
async def update(current_name: str, name: str | None = None, description: str | None = None) -> None:
|
64
|
+
"""Updates a robot class."""
|
65
|
+
async with RobotClassClient() as client:
|
66
|
+
robot_class = await client.update_robot_class(current_name, name, description)
|
67
|
+
click.echo("Robot class updated:")
|
68
|
+
click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
|
69
|
+
click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
|
70
|
+
click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
|
71
|
+
|
72
|
+
|
73
|
+
@cli.command()
|
74
|
+
@click.argument("name")
|
75
|
+
@coro
|
76
|
+
async def delete(name: str) -> None:
|
77
|
+
"""Deletes a robot class."""
|
78
|
+
async with RobotClassClient() as client:
|
79
|
+
await client.delete_robot_class(name)
|
80
|
+
click.echo(f"Robot class deleted: {click.style(name, fg='red')}")
|
81
|
+
|
82
|
+
|
83
|
+
@cli.group()
|
84
|
+
def urdf() -> None:
|
85
|
+
"""Handle the robot class URDF."""
|
86
|
+
pass
|
87
|
+
|
88
|
+
|
89
|
+
@urdf.command()
|
90
|
+
@click.argument("class_name")
|
91
|
+
@click.argument("urdf_file")
|
92
|
+
@coro
|
93
|
+
async def upload(class_name: str, urdf_file: str) -> None:
|
94
|
+
"""Uploads a URDF file to a robot class."""
|
95
|
+
async with RobotClassClient() as client:
|
96
|
+
response = await client.upload_robot_class_urdf(class_name, urdf_file)
|
97
|
+
click.echo("URDF uploaded:")
|
98
|
+
click.echo(f" Filename: {click.style(response.filename, fg='green')}")
|
99
|
+
|
100
|
+
|
101
|
+
@urdf.command()
|
102
|
+
@click.argument("class_name")
|
103
|
+
@click.option("--no-cache", is_flag=True, default=False)
|
104
|
+
@coro
|
105
|
+
async def download(class_name: str, no_cache: bool) -> None:
|
106
|
+
"""Downloads a URDF file from a robot class."""
|
107
|
+
async with RobotClassClient() as client:
|
108
|
+
urdf_file = await client.download_robot_class_urdf(class_name, cache=not no_cache)
|
109
|
+
click.echo(f"URDF downloaded: {click.style(urdf_file, fg='green')}")
|
110
|
+
|
111
|
+
|
112
|
+
if __name__ == "__main__":
|
113
|
+
cli()
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Defines the CLI for interacting with K-Scale's OpenID Connect server."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from kscale.utils.cli import coro
|
8
|
+
from kscale.web.clients.base import BaseClient
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
@click.group()
|
14
|
+
def cli() -> None:
|
15
|
+
"""Retrieve an OICD token from the K-Scale authentication server."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
@cli.command()
|
20
|
+
@coro
|
21
|
+
async def get() -> None:
|
22
|
+
"""Get a bearer token from OpenID Connect."""
|
23
|
+
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
24
|
+
async with BaseClient() as client:
|
25
|
+
try:
|
26
|
+
token = await client.get_bearer_token()
|
27
|
+
logger.info("Bearer token: %s", token)
|
28
|
+
except Exception:
|
29
|
+
logger.exception("Error getting bearer token")
|
30
|
+
|
31
|
+
|
32
|
+
if __name__ == "__main__":
|
33
|
+
cli()
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Defines the CLI for getting information about the current user."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from kscale.utils.cli import coro
|
8
|
+
from kscale.web.clients.user import UserClient
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
@click.group()
|
14
|
+
def cli() -> None:
|
15
|
+
"""Get information about the currently-authenticated user."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
@cli.command()
|
20
|
+
@coro
|
21
|
+
async def me() -> None:
|
22
|
+
client = UserClient()
|
23
|
+
profile = await client.get_profile_info()
|
24
|
+
logger.info("Email: %s", profile.email)
|
25
|
+
logger.info("Email verified: %s", profile.email_verified)
|
26
|
+
logger.info("User ID: %s", profile.user.user_id)
|
27
|
+
logger.info("Is admin: %s", profile.user.is_admin)
|
28
|
+
logger.info("Can upload: %s", profile.user.can_upload)
|
29
|
+
logger.info("Can test: %s", profile.user.can_test)
|
30
|
+
|
31
|
+
|
32
|
+
if __name__ == "__main__":
|
33
|
+
cli()
|
File without changes
|