orkestr-cli 0.1.0__py3-none-any.whl
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.
- orkestr_cli/__init__.py +0 -0
- orkestr_cli/api.py +195 -0
- orkestr_cli/config.py +79 -0
- orkestr_cli/main.py +511 -0
- orkestr_cli/output.py +59 -0
- orkestr_cli-0.1.0.dist-info/METADATA +74 -0
- orkestr_cli-0.1.0.dist-info/RECORD +10 -0
- orkestr_cli-0.1.0.dist-info/WHEEL +5 -0
- orkestr_cli-0.1.0.dist-info/entry_points.txt +2 -0
- orkestr_cli-0.1.0.dist-info/top_level.txt +1 -0
orkestr_cli/__init__.py
ADDED
|
File without changes
|
orkestr_cli/api.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""HTTP client for the Orkestr API."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from orkestr_cli import config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _client() -> httpx.Client:
|
|
12
|
+
headers = config.get_auth_header()
|
|
13
|
+
return httpx.Client(
|
|
14
|
+
base_url=config.get_api_url(),
|
|
15
|
+
headers=headers,
|
|
16
|
+
timeout=30.0,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _request(method: str, path: str, **kwargs) -> httpx.Response:
|
|
21
|
+
"""Execute a request with connection error handling."""
|
|
22
|
+
try:
|
|
23
|
+
with _client() as c:
|
|
24
|
+
return getattr(c, method)(path, **kwargs)
|
|
25
|
+
except httpx.ConnectError:
|
|
26
|
+
url = config.get_api_url()
|
|
27
|
+
click.secho(f"Connection refused: {url}", fg="red", err=True)
|
|
28
|
+
click.secho("Is the API running? Check with: orkestr set-url <url>", err=True)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
except httpx.TimeoutException:
|
|
31
|
+
click.secho("Request timed out.", fg="red", err=True)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
except httpx.HTTPError as e:
|
|
34
|
+
click.secho(f"HTTP error: {e}", fg="red", err=True)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _handle(resp: httpx.Response) -> dict | list | None:
|
|
39
|
+
if resp.status_code == 204:
|
|
40
|
+
return None
|
|
41
|
+
if resp.status_code == 401:
|
|
42
|
+
click.secho("Authentication required. Run: orkestr login <token>", fg="red", err=True)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
if resp.status_code >= 400:
|
|
45
|
+
try:
|
|
46
|
+
detail = resp.json().get("detail", resp.text)
|
|
47
|
+
except Exception:
|
|
48
|
+
detail = resp.text
|
|
49
|
+
click.secho(f"Error ({resp.status_code}): {detail}", fg="red", err=True)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
return resp.json()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get(path: str, **kwargs):
|
|
55
|
+
return _handle(_request("get", path, **kwargs))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _post(path: str, **kwargs):
|
|
59
|
+
return _handle(_request("post", path, **kwargs))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _patch(path: str, **kwargs):
|
|
63
|
+
return _handle(_request("patch", path, **kwargs))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _delete(path: str, **kwargs):
|
|
67
|
+
return _handle(_request("delete", path, **kwargs))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# -- Auth -------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def me() -> dict:
|
|
73
|
+
return _get("/api/auth/me")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# -- Helpers ----------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def resolve_project_id(name_or_id: str) -> str:
|
|
79
|
+
"""Accept a project name or UUID, return the full UUID."""
|
|
80
|
+
# Already looks like a UUID
|
|
81
|
+
if len(name_or_id) >= 32 or name_or_id.count("-") >= 4:
|
|
82
|
+
return name_or_id
|
|
83
|
+
# Search by name
|
|
84
|
+
data = list_projects(limit=100)
|
|
85
|
+
for p in data.get("projects", []):
|
|
86
|
+
if p.get("name") == name_or_id:
|
|
87
|
+
return p["id"]
|
|
88
|
+
click.secho(f"Project not found: {name_or_id}", fg="red", err=True)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# -- Projects ---------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def list_projects(skip: int = 0, limit: int = 20) -> dict:
|
|
95
|
+
return _get("/api/v1/projects", params={"skip": skip, "limit": limit})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_project(name: str, repo_url: str, branch: str = "main",
|
|
99
|
+
display_name: str | None = None, description: str | None = None,
|
|
100
|
+
env_vars: dict | None = None) -> dict:
|
|
101
|
+
payload = {"name": name, "repo_url": repo_url, "repo_branch": branch}
|
|
102
|
+
if display_name:
|
|
103
|
+
payload["display_name"] = display_name
|
|
104
|
+
if description:
|
|
105
|
+
payload["description"] = description
|
|
106
|
+
if env_vars:
|
|
107
|
+
payload["env_vars"] = env_vars
|
|
108
|
+
return _post("/api/v1/projects", json=payload)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_project(project_id: str) -> dict:
|
|
112
|
+
return _get(f"/api/v1/projects/{project_id}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def update_project(project_id: str, **fields) -> dict:
|
|
116
|
+
return _patch(f"/api/v1/projects/{project_id}", json=fields)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def delete_project(project_id: str):
|
|
120
|
+
_delete(f"/api/v1/projects/{project_id}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def analyze_project(project_id: str) -> dict:
|
|
124
|
+
return _post(f"/api/v1/projects/{project_id}/analyze")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# -- Environments -----------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def list_environments(project_id: str) -> list:
|
|
130
|
+
return _get(f"/api/v1/projects/{project_id}/environments")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def create_environment(project_id: str, name: str, branch: str,
|
|
134
|
+
env_vars: dict | None = None, auto_deploy: bool = True) -> dict:
|
|
135
|
+
payload = {"name": name, "branch": branch, "auto_deploy": auto_deploy}
|
|
136
|
+
if env_vars:
|
|
137
|
+
payload["env_vars"] = env_vars
|
|
138
|
+
return _post(f"/api/v1/projects/{project_id}/environments", json=payload)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def update_environment(project_id: str, env_id: str, **fields) -> dict:
|
|
142
|
+
return _patch(f"/api/v1/projects/{project_id}/environments/{env_id}", json=fields)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def delete_environment(project_id: str, env_id: str):
|
|
146
|
+
_delete(f"/api/v1/projects/{project_id}/environments/{env_id}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# -- Deployments ------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def trigger_deployment(project_id: str, environment_id: str | None = None) -> dict:
|
|
152
|
+
params = {}
|
|
153
|
+
if environment_id:
|
|
154
|
+
params["environment_id"] = environment_id
|
|
155
|
+
return _post(f"/api/v1/projects/{project_id}/deployments", params=params)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def list_deployments(project_id: str, skip: int = 0, limit: int = 20) -> dict:
|
|
159
|
+
return _get(f"/api/v1/projects/{project_id}/deployments",
|
|
160
|
+
params={"skip": skip, "limit": limit})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_deployment(deployment_id: str) -> dict:
|
|
164
|
+
return _get(f"/api/v1/deployments/{deployment_id}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_deployment_logs(deployment_id: str) -> dict:
|
|
168
|
+
return _get(f"/api/v1/deployments/{deployment_id}/logs")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def rollback_deployment(deployment_id: str) -> dict:
|
|
172
|
+
return _post(f"/api/v1/deployments/{deployment_id}/rollback")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# -- Monitoring -------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def get_stats(project_id: str, environment_id: str | None = None) -> dict:
|
|
178
|
+
params = {}
|
|
179
|
+
if environment_id:
|
|
180
|
+
params["environment_id"] = environment_id
|
|
181
|
+
return _get(f"/api/v1/projects/{project_id}/stats", params=params)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_logs(project_id: str, environment_id: str | None = None, lines: int = 200) -> dict:
|
|
185
|
+
params = {"lines": lines}
|
|
186
|
+
if environment_id:
|
|
187
|
+
params["environment_id"] = environment_id
|
|
188
|
+
return _get(f"/api/v1/projects/{project_id}/logs", params=params)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_health(project_id: str, environment_id: str | None = None) -> dict:
|
|
192
|
+
params = {}
|
|
193
|
+
if environment_id:
|
|
194
|
+
params["environment_id"] = environment_id
|
|
195
|
+
return _get(f"/api/v1/projects/{project_id}/health", params=params)
|
orkestr_cli/config.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Configuration and credential storage for the Orkestr CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = Path.home() / ".orkestr"
|
|
7
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
8
|
+
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
"api_url": "https://api.orkestr.eu",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_dir():
|
|
15
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
CONFIG_FILE.touch(exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load() -> dict:
|
|
20
|
+
_ensure_dir()
|
|
21
|
+
text = CONFIG_FILE.read_text().strip()
|
|
22
|
+
if not text:
|
|
23
|
+
return dict(DEFAULTS)
|
|
24
|
+
data = json.loads(text)
|
|
25
|
+
for k, v in DEFAULTS.items():
|
|
26
|
+
data.setdefault(k, v)
|
|
27
|
+
# Migrate old localhost default to production URL
|
|
28
|
+
if data.get("api_url") == "http://localhost:8000":
|
|
29
|
+
data["api_url"] = DEFAULTS["api_url"]
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save(data: dict):
|
|
34
|
+
_ensure_dir()
|
|
35
|
+
CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_api_url() -> str:
|
|
39
|
+
return load().get("api_url", DEFAULTS["api_url"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_auth_header() -> dict[str, str]:
|
|
43
|
+
"""Return the Authorization header dict, or empty dict if not logged in."""
|
|
44
|
+
cfg = load()
|
|
45
|
+
token = cfg.get("api_token") or cfg.get("access_token")
|
|
46
|
+
if not token:
|
|
47
|
+
return {}
|
|
48
|
+
prefix = "Bearer" if cfg.get("access_token") else "Bearer"
|
|
49
|
+
return {"Authorization": f"{prefix} {token}"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def store_tokens(access_token: str, refresh_token: str | None = None):
|
|
53
|
+
cfg = load()
|
|
54
|
+
cfg["access_token"] = access_token
|
|
55
|
+
if refresh_token:
|
|
56
|
+
cfg["refresh_token"] = refresh_token
|
|
57
|
+
cfg.pop("api_token", None)
|
|
58
|
+
save(cfg)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def store_api_token(token: str):
|
|
62
|
+
cfg = load()
|
|
63
|
+
cfg["api_token"] = token
|
|
64
|
+
cfg.pop("access_token", None)
|
|
65
|
+
cfg.pop("refresh_token", None)
|
|
66
|
+
save(cfg)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def clear_auth():
|
|
70
|
+
cfg = load()
|
|
71
|
+
cfg.pop("access_token", None)
|
|
72
|
+
cfg.pop("refresh_token", None)
|
|
73
|
+
cfg.pop("api_token", None)
|
|
74
|
+
save(cfg)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_logged_in() -> bool:
|
|
78
|
+
cfg = load()
|
|
79
|
+
return bool(cfg.get("access_token") or cfg.get("api_token"))
|
orkestr_cli/main.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Orkestr CLI — manage your projects, deployments, and infrastructure."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from orkestr_cli import api, config
|
|
6
|
+
from orkestr_cli.output import (
|
|
7
|
+
console,
|
|
8
|
+
format_dt,
|
|
9
|
+
print_json,
|
|
10
|
+
print_kv,
|
|
11
|
+
print_table,
|
|
12
|
+
status_color,
|
|
13
|
+
truncate,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Root group
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
@click.option("--api-url", envvar="ORKESTR_API_URL", help="Override API base URL.")
|
|
23
|
+
@click.option("--token", envvar="ORKESTR_API_TOKEN", help="API token for auth.")
|
|
24
|
+
@click.option("--json", "use_json", is_flag=True, help="Output raw JSON.")
|
|
25
|
+
@click.version_option("0.1.0", prog_name="orkestr")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def cli(ctx, api_url, token, use_json):
|
|
28
|
+
"""Orkestr CLI — deploy and manage your applications."""
|
|
29
|
+
ctx.ensure_object(dict)
|
|
30
|
+
ctx.obj["json"] = use_json
|
|
31
|
+
|
|
32
|
+
if api_url:
|
|
33
|
+
cfg = config.load()
|
|
34
|
+
cfg["api_url"] = api_url
|
|
35
|
+
config.save(cfg)
|
|
36
|
+
|
|
37
|
+
if token:
|
|
38
|
+
config.store_api_token(token)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Auth commands
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
@cli.command()
|
|
46
|
+
@click.option("--token", prompt="API token", hide_input=True, prompt_required=True,
|
|
47
|
+
help="API token (will be prompted if not provided).")
|
|
48
|
+
def login(token):
|
|
49
|
+
"""Authenticate with an API token.
|
|
50
|
+
|
|
51
|
+
Generate a token from the Orkestr dashboard under Settings > API Tokens.
|
|
52
|
+
"""
|
|
53
|
+
token = token.strip()
|
|
54
|
+
if not token:
|
|
55
|
+
click.secho("Token cannot be empty.", fg="red", err=True)
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
config.store_api_token(token)
|
|
58
|
+
data = api.me()
|
|
59
|
+
click.secho(f"Logged in as {data.get('username', 'unknown')}.", fg="green")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cli.command()
|
|
63
|
+
def logout():
|
|
64
|
+
"""Clear stored credentials."""
|
|
65
|
+
config.clear_auth()
|
|
66
|
+
click.secho("Logged out.", fg="green")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@cli.command()
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def whoami(ctx):
|
|
72
|
+
"""Show current user info."""
|
|
73
|
+
data = api.me()
|
|
74
|
+
if ctx.obj["json"]:
|
|
75
|
+
print_json(data)
|
|
76
|
+
return
|
|
77
|
+
print_kv([
|
|
78
|
+
("Username:", data.get("username", "-")),
|
|
79
|
+
("Email:", data.get("email", "-")),
|
|
80
|
+
("Plan:", data.get("plan", "-")),
|
|
81
|
+
("Active:", str(data.get("is_active", "-"))),
|
|
82
|
+
("Created:", format_dt(data.get("created_at"))),
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@cli.command("set-url")
|
|
87
|
+
@click.argument("url")
|
|
88
|
+
def set_url(url):
|
|
89
|
+
"""Set the API base URL."""
|
|
90
|
+
cfg = config.load()
|
|
91
|
+
cfg["api_url"] = url.rstrip("/")
|
|
92
|
+
config.save(cfg)
|
|
93
|
+
click.secho(f"API URL set to {cfg['api_url']}", fg="green")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Projects
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
@cli.group()
|
|
102
|
+
def projects():
|
|
103
|
+
"""Manage projects."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@projects.command("list")
|
|
107
|
+
@click.option("--limit", default=20, help="Max results.")
|
|
108
|
+
@click.pass_context
|
|
109
|
+
def projects_list(ctx, limit):
|
|
110
|
+
"""List all projects."""
|
|
111
|
+
data = api.list_projects(limit=limit)
|
|
112
|
+
if ctx.obj["json"]:
|
|
113
|
+
print_json(data)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
rows = []
|
|
117
|
+
for p in data.get("projects", []):
|
|
118
|
+
rows.append([
|
|
119
|
+
p.get("name", "-"),
|
|
120
|
+
p.get("display_name") or p.get("name", "-"),
|
|
121
|
+
p.get("framework") or "-",
|
|
122
|
+
status_color(p.get("status", "-")),
|
|
123
|
+
format_dt(p.get("created_at")),
|
|
124
|
+
])
|
|
125
|
+
print_table("Projects", ["Name", "Display Name", "Framework", "Status", "Created"], rows)
|
|
126
|
+
click.echo(f"\nTotal: {data.get('total', len(rows))}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@projects.command("create")
|
|
130
|
+
@click.argument("name")
|
|
131
|
+
@click.option("--repo", required=True, help="Git repository URL.")
|
|
132
|
+
@click.option("--branch", default="main", help="Default branch.")
|
|
133
|
+
@click.option("--display-name", help="Display name.")
|
|
134
|
+
@click.option("--description", help="Project description.")
|
|
135
|
+
@click.pass_context
|
|
136
|
+
def projects_create(ctx, name, repo, branch, display_name, description):
|
|
137
|
+
"""Create a new project."""
|
|
138
|
+
data = api.create_project(
|
|
139
|
+
name=name,
|
|
140
|
+
repo_url=repo,
|
|
141
|
+
branch=branch,
|
|
142
|
+
display_name=display_name,
|
|
143
|
+
description=description,
|
|
144
|
+
)
|
|
145
|
+
if ctx.obj["json"]:
|
|
146
|
+
print_json(data)
|
|
147
|
+
return
|
|
148
|
+
click.secho(f"Project '{data['name']}' created.", fg="green")
|
|
149
|
+
print_kv([
|
|
150
|
+
("ID:", data["id"]),
|
|
151
|
+
("Domain:", data.get("domain", "-")),
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@projects.command("show")
|
|
156
|
+
@click.argument("project")
|
|
157
|
+
@click.pass_context
|
|
158
|
+
def projects_show(ctx, project):
|
|
159
|
+
"""Show project details. Accepts a project name or UUID."""
|
|
160
|
+
project_id = api.resolve_project_id(project)
|
|
161
|
+
data = api.get_project(project_id)
|
|
162
|
+
if ctx.obj["json"]:
|
|
163
|
+
print_json(data)
|
|
164
|
+
return
|
|
165
|
+
print_kv([
|
|
166
|
+
("ID:", data["id"]),
|
|
167
|
+
("Name:", data.get("name", "-")),
|
|
168
|
+
("Display Name:", data.get("display_name") or "-"),
|
|
169
|
+
("Repo:", data.get("repo_url", "-")),
|
|
170
|
+
("Branch:", data.get("repo_branch", "-")),
|
|
171
|
+
("Framework:", data.get("framework") or "-"),
|
|
172
|
+
("Dockerfile:", str(data.get("has_dockerfile", "-"))),
|
|
173
|
+
("Status:", data.get("status", "-")),
|
|
174
|
+
("Domain:", data.get("domain") or "-"),
|
|
175
|
+
("Created:", format_dt(data.get("created_at"))),
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@projects.command("delete")
|
|
180
|
+
@click.argument("project")
|
|
181
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this project?")
|
|
182
|
+
def projects_delete(project):
|
|
183
|
+
"""Delete a project and its infrastructure. Accepts a project name or UUID."""
|
|
184
|
+
project_id = api.resolve_project_id(project)
|
|
185
|
+
api.delete_project(project_id)
|
|
186
|
+
click.secho("Project deleted.", fg="green")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@projects.command("analyze")
|
|
190
|
+
@click.argument("project")
|
|
191
|
+
@click.pass_context
|
|
192
|
+
def projects_analyze(ctx, project):
|
|
193
|
+
"""Analyze a project's repository. Accepts a project name or UUID."""
|
|
194
|
+
project_id = api.resolve_project_id(project)
|
|
195
|
+
data = api.analyze_project(project_id)
|
|
196
|
+
if ctx.obj["json"]:
|
|
197
|
+
print_json(data)
|
|
198
|
+
return
|
|
199
|
+
print_kv([
|
|
200
|
+
("Framework:", data.get("framework", "-")),
|
|
201
|
+
("Has Dockerfile:", str(data.get("has_dockerfile", "-"))),
|
|
202
|
+
("Detected Port:", str(data.get("detected_port", "-"))),
|
|
203
|
+
("Suggested CPU:", str(data.get("suggested_cpu", "-"))),
|
|
204
|
+
("Suggested Memory:", data.get("suggested_memory", "-")),
|
|
205
|
+
])
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@projects.command("update")
|
|
209
|
+
@click.argument("project")
|
|
210
|
+
@click.option("--display-name", help="New display name.")
|
|
211
|
+
@click.option("--description", help="New description.")
|
|
212
|
+
@click.option("--branch", help="New default branch.")
|
|
213
|
+
@click.pass_context
|
|
214
|
+
def projects_update(ctx, project, display_name, description, branch):
|
|
215
|
+
"""Update project settings. Accepts a project name or UUID."""
|
|
216
|
+
project_id = api.resolve_project_id(project)
|
|
217
|
+
fields = {}
|
|
218
|
+
if display_name is not None:
|
|
219
|
+
fields["display_name"] = display_name
|
|
220
|
+
if description is not None:
|
|
221
|
+
fields["description"] = description
|
|
222
|
+
if branch is not None:
|
|
223
|
+
fields["repo_branch"] = branch
|
|
224
|
+
if not fields:
|
|
225
|
+
click.secho("No fields to update.", fg="yellow")
|
|
226
|
+
return
|
|
227
|
+
data = api.update_project(project_id, **fields)
|
|
228
|
+
if ctx.obj["json"]:
|
|
229
|
+
print_json(data)
|
|
230
|
+
return
|
|
231
|
+
click.secho(f"Project '{data['name']}' updated.", fg="green")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
# Environments
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
@cli.group("envs")
|
|
239
|
+
def envs():
|
|
240
|
+
"""Manage environments."""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@envs.command("list")
|
|
244
|
+
@click.argument("project")
|
|
245
|
+
@click.pass_context
|
|
246
|
+
def envs_list(ctx, project):
|
|
247
|
+
"""List environments for a project."""
|
|
248
|
+
project_id = api.resolve_project_id(project)
|
|
249
|
+
data = api.list_environments(project_id)
|
|
250
|
+
if ctx.obj["json"]:
|
|
251
|
+
print_json(data)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
rows = []
|
|
255
|
+
for e in data:
|
|
256
|
+
rows.append([
|
|
257
|
+
e.get("name", "-"),
|
|
258
|
+
e.get("branch", "-"),
|
|
259
|
+
e.get("domain") or "-",
|
|
260
|
+
"Yes" if e.get("is_production") else "No",
|
|
261
|
+
"Yes" if e.get("auto_deploy") else "No",
|
|
262
|
+
status_color(e.get("status", "-")),
|
|
263
|
+
])
|
|
264
|
+
print_table("Environments", ["Name", "Branch", "Domain", "Prod", "Auto Deploy", "Status"], rows)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@envs.command("create")
|
|
268
|
+
@click.argument("project")
|
|
269
|
+
@click.argument("name")
|
|
270
|
+
@click.option("--branch", required=True, help="Git branch.")
|
|
271
|
+
@click.option("--auto-deploy/--no-auto-deploy", default=True, help="Auto deploy on push.")
|
|
272
|
+
@click.pass_context
|
|
273
|
+
def envs_create(ctx, project, name, branch, auto_deploy):
|
|
274
|
+
"""Create a new environment."""
|
|
275
|
+
project_id = api.resolve_project_id(project)
|
|
276
|
+
data = api.create_environment(project_id, name, branch, auto_deploy=auto_deploy)
|
|
277
|
+
if ctx.obj["json"]:
|
|
278
|
+
print_json(data)
|
|
279
|
+
return
|
|
280
|
+
click.secho(f"Environment '{data['name']}' created.", fg="green")
|
|
281
|
+
print_kv([
|
|
282
|
+
("ID:", data["id"]),
|
|
283
|
+
("Domain:", data.get("domain", "-")),
|
|
284
|
+
])
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@envs.command("update")
|
|
288
|
+
@click.argument("project")
|
|
289
|
+
@click.argument("env_id")
|
|
290
|
+
@click.option("--branch", help="New branch.")
|
|
291
|
+
@click.option("--auto-deploy/--no-auto-deploy", default=None, help="Auto deploy on push.")
|
|
292
|
+
@click.pass_context
|
|
293
|
+
def envs_update(ctx, project, env_id, branch, auto_deploy):
|
|
294
|
+
"""Update an environment."""
|
|
295
|
+
project_id = api.resolve_project_id(project)
|
|
296
|
+
fields = {}
|
|
297
|
+
if branch is not None:
|
|
298
|
+
fields["branch"] = branch
|
|
299
|
+
if auto_deploy is not None:
|
|
300
|
+
fields["auto_deploy"] = auto_deploy
|
|
301
|
+
if not fields:
|
|
302
|
+
click.secho("No fields to update.", fg="yellow")
|
|
303
|
+
return
|
|
304
|
+
data = api.update_environment(project_id, env_id, **fields)
|
|
305
|
+
if ctx.obj["json"]:
|
|
306
|
+
print_json(data)
|
|
307
|
+
return
|
|
308
|
+
click.secho(f"Environment '{data['name']}' updated.", fg="green")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@envs.command("delete")
|
|
312
|
+
@click.argument("project")
|
|
313
|
+
@click.argument("env_id")
|
|
314
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this environment?")
|
|
315
|
+
def envs_delete(project, env_id):
|
|
316
|
+
"""Delete an environment."""
|
|
317
|
+
project_id = api.resolve_project_id(project)
|
|
318
|
+
api.delete_environment(project_id, env_id)
|
|
319
|
+
click.secho("Environment deleted.", fg="green")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Deploy (shortcut)
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
@cli.command()
|
|
327
|
+
@click.argument("project")
|
|
328
|
+
@click.option("--env", "environment_id", help="Environment ID (defaults to production).")
|
|
329
|
+
@click.pass_context
|
|
330
|
+
def deploy(ctx, project, environment_id):
|
|
331
|
+
"""Trigger a deployment."""
|
|
332
|
+
project_id = api.resolve_project_id(project)
|
|
333
|
+
data = api.trigger_deployment(project_id, environment_id)
|
|
334
|
+
if ctx.obj["json"]:
|
|
335
|
+
print_json(data)
|
|
336
|
+
return
|
|
337
|
+
click.secho("Deployment triggered.", fg="green")
|
|
338
|
+
print_kv([
|
|
339
|
+
("Deployment ID:", data["id"]),
|
|
340
|
+
("Status:", data.get("status", "-")),
|
|
341
|
+
])
|
|
342
|
+
click.echo(f"\nTrack progress: orkestr deployments show {data['id']}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
# Deployments
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
@cli.group()
|
|
350
|
+
def deployments():
|
|
351
|
+
"""Manage deployments."""
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@deployments.command("list")
|
|
355
|
+
@click.argument("project")
|
|
356
|
+
@click.option("--limit", default=20, help="Max results.")
|
|
357
|
+
@click.pass_context
|
|
358
|
+
def deployments_list(ctx, project, limit):
|
|
359
|
+
"""List deployments for a project."""
|
|
360
|
+
project_id = api.resolve_project_id(project)
|
|
361
|
+
data = api.list_deployments(project_id, limit=limit)
|
|
362
|
+
if ctx.obj["json"]:
|
|
363
|
+
print_json(data)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
rows = []
|
|
367
|
+
for d in data.get("deployments", []):
|
|
368
|
+
rows.append([
|
|
369
|
+
d["id"][:8],
|
|
370
|
+
status_color(d.get("status", "-")),
|
|
371
|
+
d.get("trigger", "-"),
|
|
372
|
+
truncate(d.get("commit_sha") or "-", 8),
|
|
373
|
+
format_dt(d.get("created_at")),
|
|
374
|
+
])
|
|
375
|
+
print_table("Deployments", ["ID", "Status", "Trigger", "Commit", "Created"], rows)
|
|
376
|
+
click.echo(f"\nTotal: {data.get('total', len(rows))}")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@deployments.command("show")
|
|
380
|
+
@click.argument("deployment_id")
|
|
381
|
+
@click.pass_context
|
|
382
|
+
def deployments_show(ctx, deployment_id):
|
|
383
|
+
"""Show deployment details."""
|
|
384
|
+
data = api.get_deployment(deployment_id)
|
|
385
|
+
if ctx.obj["json"]:
|
|
386
|
+
print_json(data)
|
|
387
|
+
return
|
|
388
|
+
print_kv([
|
|
389
|
+
("ID:", data["id"]),
|
|
390
|
+
("Status:", data.get("status", "-")),
|
|
391
|
+
("Trigger:", data.get("trigger", "-")),
|
|
392
|
+
("Commit:", data.get("commit_sha") or "-"),
|
|
393
|
+
("Image:", data.get("docker_image") or "-"),
|
|
394
|
+
("Created:", format_dt(data.get("created_at"))),
|
|
395
|
+
("Finished:", format_dt(data.get("finished_at"))),
|
|
396
|
+
])
|
|
397
|
+
|
|
398
|
+
steps = data.get("steps", [])
|
|
399
|
+
if steps:
|
|
400
|
+
click.echo("\nPipeline Steps:")
|
|
401
|
+
for s in steps:
|
|
402
|
+
icon = {"completed": "[green]OK[/]", "failed": "[red]FAIL[/]", "running": "[yellow]...[/]"}.get(s.get("status"), " ")
|
|
403
|
+
console.print(f" {icon} {s.get('step_name', '-')}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@deployments.command("logs")
|
|
407
|
+
@click.argument("deployment_id")
|
|
408
|
+
@click.pass_context
|
|
409
|
+
def deployments_logs(ctx, deployment_id):
|
|
410
|
+
"""Show deployment pipeline logs."""
|
|
411
|
+
data = api.get_deployment_logs(deployment_id)
|
|
412
|
+
if ctx.obj["json"]:
|
|
413
|
+
print_json(data)
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
current_step = None
|
|
417
|
+
for entry in data.get("logs", []):
|
|
418
|
+
step = entry.get("step", "")
|
|
419
|
+
if step != current_step:
|
|
420
|
+
current_step = step
|
|
421
|
+
click.secho(f"\n--- {step} ---", fg="cyan", bold=True)
|
|
422
|
+
click.echo(entry.get("message", ""))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@deployments.command("rollback")
|
|
426
|
+
@click.argument("deployment_id")
|
|
427
|
+
@click.confirmation_option(prompt="Are you sure you want to rollback to this deployment?")
|
|
428
|
+
@click.pass_context
|
|
429
|
+
def deployments_rollback(ctx, deployment_id):
|
|
430
|
+
"""Rollback to a previous deployment's image."""
|
|
431
|
+
data = api.rollback_deployment(deployment_id)
|
|
432
|
+
if ctx.obj["json"]:
|
|
433
|
+
print_json(data)
|
|
434
|
+
return
|
|
435
|
+
click.secho("Rollback deployment triggered.", fg="green")
|
|
436
|
+
print_kv([
|
|
437
|
+
("Deployment ID:", data["id"]),
|
|
438
|
+
("Status:", data.get("status", "-")),
|
|
439
|
+
])
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
# Monitoring shortcuts
|
|
444
|
+
# ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
@cli.command()
|
|
447
|
+
@click.argument("project")
|
|
448
|
+
@click.option("--env", "environment_id", help="Environment ID.")
|
|
449
|
+
@click.option("--lines", default=200, help="Number of log lines.")
|
|
450
|
+
@click.pass_context
|
|
451
|
+
def logs(ctx, project, environment_id, lines):
|
|
452
|
+
"""Show application runtime logs."""
|
|
453
|
+
project_id = api.resolve_project_id(project)
|
|
454
|
+
data = api.get_logs(project_id, environment_id, lines)
|
|
455
|
+
if ctx.obj["json"]:
|
|
456
|
+
print_json(data)
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
click.secho(f"Container: {data.get('container', '-')}", fg="cyan")
|
|
460
|
+
click.echo()
|
|
461
|
+
for line in data.get("logs", "").strip().split("\n"):
|
|
462
|
+
click.echo(line)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@cli.command()
|
|
466
|
+
@click.argument("project")
|
|
467
|
+
@click.option("--env", "environment_id", help="Environment ID.")
|
|
468
|
+
@click.pass_context
|
|
469
|
+
def stats(ctx, project, environment_id):
|
|
470
|
+
"""Show container resource stats."""
|
|
471
|
+
project_id = api.resolve_project_id(project)
|
|
472
|
+
data = api.get_stats(project_id, environment_id)
|
|
473
|
+
if ctx.obj["json"]:
|
|
474
|
+
print_json(data)
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
print_kv([
|
|
478
|
+
("CPU %:", str(data.get("cpu_percent", "-"))),
|
|
479
|
+
("Memory:", f"{data.get('memory_usage', '-')} / {data.get('memory_limit', '-')}"),
|
|
480
|
+
("Memory %:", str(data.get("memory_percent", "-"))),
|
|
481
|
+
("Net I/O:", f"{data.get('network_rx', '-')} / {data.get('network_tx', '-')}"),
|
|
482
|
+
])
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@cli.command()
|
|
486
|
+
@click.argument("project")
|
|
487
|
+
@click.option("--env", "environment_id", help="Environment ID.")
|
|
488
|
+
@click.pass_context
|
|
489
|
+
def health(ctx, project, environment_id):
|
|
490
|
+
"""Show container health info."""
|
|
491
|
+
project_id = api.resolve_project_id(project)
|
|
492
|
+
data = api.get_health(project_id, environment_id)
|
|
493
|
+
if ctx.obj["json"]:
|
|
494
|
+
print_json(data)
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
print_kv([
|
|
498
|
+
("Status:", data.get("status", "-")),
|
|
499
|
+
("Uptime:", data.get("uptime", "-")),
|
|
500
|
+
("Restarts:", str(data.get("restart_count", "-"))),
|
|
501
|
+
("Image:", data.get("image", "-")),
|
|
502
|
+
("Started:", data.get("started_at", "-")),
|
|
503
|
+
])
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ---------------------------------------------------------------------------
|
|
507
|
+
# Entry point
|
|
508
|
+
# ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
if __name__ == "__main__":
|
|
511
|
+
cli()
|
orkestr_cli/output.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Output formatting helpers using Rich."""
|
|
2
|
+
|
|
3
|
+
import json as json_mod
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def print_json(data):
|
|
13
|
+
"""Print raw JSON output."""
|
|
14
|
+
click.echo(json_mod.dumps(data, indent=2, default=str))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def print_table(title: str, columns: list[str], rows: list[list[str]]):
|
|
18
|
+
"""Print a Rich table."""
|
|
19
|
+
table = Table(title=title, show_lines=False)
|
|
20
|
+
for col in columns:
|
|
21
|
+
table.add_column(col)
|
|
22
|
+
for row in rows:
|
|
23
|
+
table.add_row(*[str(c) for c in row])
|
|
24
|
+
console.print(table)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_kv(pairs: list[tuple[str, str]]):
|
|
28
|
+
"""Print key-value pairs."""
|
|
29
|
+
max_key = max(len(k) for k, _ in pairs) if pairs else 0
|
|
30
|
+
for key, value in pairs:
|
|
31
|
+
click.echo(f" {key:<{max_key + 2}} {value}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def truncate(text: str, length: int = 40) -> str:
|
|
35
|
+
if not text:
|
|
36
|
+
return "-"
|
|
37
|
+
return text[:length] + "..." if len(text) > length else text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_dt(dt_str: str | None) -> str:
|
|
41
|
+
if not dt_str:
|
|
42
|
+
return "-"
|
|
43
|
+
return dt_str[:19].replace("T", " ")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def status_color(status: str) -> str:
|
|
47
|
+
colors = {
|
|
48
|
+
"live": "green",
|
|
49
|
+
"running": "green",
|
|
50
|
+
"active": "green",
|
|
51
|
+
"building": "yellow",
|
|
52
|
+
"deploying": "yellow",
|
|
53
|
+
"queued": "cyan",
|
|
54
|
+
"failed": "red",
|
|
55
|
+
"deleted": "dim",
|
|
56
|
+
"stopped": "dim",
|
|
57
|
+
}
|
|
58
|
+
color = colors.get(status, "white")
|
|
59
|
+
return click.style(status, fg=color)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orkestr-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Orkestr Internal Developer Platform
|
|
5
|
+
Author-email: Orkestr <hello@orkestr.eu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://orkestr.eu
|
|
8
|
+
Project-URL: Documentation, https://orkestr.eu/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/orkestr/cli
|
|
10
|
+
Keywords: orkestr,cli,deploy,paas,idp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: click>=8.1
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
|
|
26
|
+
# orkestr-cli
|
|
27
|
+
|
|
28
|
+
CLI for the [Orkestr](https://orkestr.eu) Internal Developer Platform.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install orkestr-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Authenticate with your API token
|
|
40
|
+
orkestr login
|
|
41
|
+
|
|
42
|
+
# List your projects
|
|
43
|
+
orkestr projects list
|
|
44
|
+
|
|
45
|
+
# Deploy
|
|
46
|
+
orkestr deploy my-api
|
|
47
|
+
|
|
48
|
+
# View logs
|
|
49
|
+
orkestr logs my-api
|
|
50
|
+
|
|
51
|
+
# Check health
|
|
52
|
+
orkestr health my-api
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `orkestr login` | Authenticate with an API token |
|
|
60
|
+
| `orkestr logout` | Clear stored credentials |
|
|
61
|
+
| `orkestr whoami` | Show current user info |
|
|
62
|
+
| `orkestr projects list\|create\|show\|update\|delete\|analyze` | Manage projects |
|
|
63
|
+
| `orkestr envs list\|create\|update\|delete` | Manage environments |
|
|
64
|
+
| `orkestr deploy <project>` | Trigger a deployment |
|
|
65
|
+
| `orkestr deployments list\|show\|logs\|rollback` | Deployment history |
|
|
66
|
+
| `orkestr logs <project>` | Runtime container logs |
|
|
67
|
+
| `orkestr stats <project>` | CPU/memory/network stats |
|
|
68
|
+
| `orkestr health <project>` | Container health info |
|
|
69
|
+
|
|
70
|
+
All commands support `--json` for raw JSON output.
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
Full docs at [orkestr.eu/docs](https://orkestr.eu/docs)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
orkestr_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
orkestr_cli/api.py,sha256=sDcDRc1kWDcI6sNpZJUGxhBkvhti0ByNULzD-OYAkb4,6261
|
|
3
|
+
orkestr_cli/config.py,sha256=iC5sXHklxQ0QkaKSpZIk_AMQyTizRqNIznbsqmcL0LQ,1943
|
|
4
|
+
orkestr_cli/main.py,sha256=ugHYdD50LRfQMdU183sLOw7ImXa0uyRpOt6d6a1juqA,16476
|
|
5
|
+
orkestr_cli/output.py,sha256=KdP31LYFzyYDbOY25Ctq6Qg7jga5WtgPAHEOKDV_IIU,1466
|
|
6
|
+
orkestr_cli-0.1.0.dist-info/METADATA,sha256=9kFsFH6LWy0lFg2EUCMkm1dJe91m2Xzm1eDGDrEGyqk,2073
|
|
7
|
+
orkestr_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
orkestr_cli-0.1.0.dist-info/entry_points.txt,sha256=5SHqNcPp8OmqT6fzC2A8N5H8jp8PNrRaTE4UtvMyD1g,49
|
|
9
|
+
orkestr_cli-0.1.0.dist-info/top_level.txt,sha256=xTY9D1RhX1fP7vxK4RlK8owWvjijvZZNXNBoSfyH7LE,12
|
|
10
|
+
orkestr_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
orkestr_cli
|