twc-cli 1.0.0rc0__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.
Potentially problematic release.
This version of twc-cli might be problematic. Click here for more details.
- CHANGELOG.md +5 -0
- COPYING +22 -0
- twc/__init__.py +5 -0
- twc/__main__.py +21 -0
- twc/__version__.py +13 -0
- twc/api/__init__.py +2 -0
- twc/api/client.py +591 -0
- twc/api/exceptions.py +26 -0
- twc/commands/__init__.py +286 -0
- twc/commands/account.py +178 -0
- twc/commands/config.py +46 -0
- twc/commands/server.py +2148 -0
- twc/commands/ssh_key.py +296 -0
- twc/fmt.py +188 -0
- twc_cli-1.0.0rc0.dist-info/COPYING +22 -0
- twc_cli-1.0.0rc0.dist-info/METADATA +80 -0
- twc_cli-1.0.0rc0.dist-info/RECORD +19 -0
- twc_cli-1.0.0rc0.dist-info/WHEEL +4 -0
- twc_cli-1.0.0rc0.dist-info/entry_points.txt +3 -0
twc/commands/ssh_key.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""SSH-key management commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from click_aliases import ClickAliasedGroup
|
|
8
|
+
|
|
9
|
+
from twc import fmt
|
|
10
|
+
from . import (
|
|
11
|
+
create_client,
|
|
12
|
+
handle_request,
|
|
13
|
+
options,
|
|
14
|
+
GLOBAL_OPTIONS,
|
|
15
|
+
OUTPUT_FORMAT_OPTION,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@handle_request
|
|
20
|
+
def _ssh_key_list(client):
|
|
21
|
+
return client.get_ssh_keys()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@handle_request
|
|
25
|
+
def _ssh_key_get(client, *args):
|
|
26
|
+
return client.get_ssh_key(*args)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@handle_request
|
|
30
|
+
def _ssh_key_new(client, **kwargs):
|
|
31
|
+
return client.add_new_ssh_key(**kwargs)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@handle_request
|
|
35
|
+
def _ssh_key_edit(client, *args, **kwargs):
|
|
36
|
+
return client.update_ssh_key(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@handle_request
|
|
40
|
+
def _ssh_key_add(client, *args, **kwargs):
|
|
41
|
+
return client.add_ssh_key_to_server(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@handle_request
|
|
45
|
+
def _ssh_key_remove(client, *args):
|
|
46
|
+
return client.delete_ssh_key(*args)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@handle_request
|
|
50
|
+
def _ssh_key_remove_from_server(client, *args):
|
|
51
|
+
return client.delete_ssh_key_from_server(*args)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------- #
|
|
55
|
+
# $ twc ssh-key #
|
|
56
|
+
# ------------------------------------------------------------- #
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@click.group("ssh-key", cls=ClickAliasedGroup)
|
|
60
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
61
|
+
def ssh_key():
|
|
62
|
+
"""Manage SSH-keys."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------- #
|
|
66
|
+
# $ twc ssh-key list #
|
|
67
|
+
# ------------------------------------------------------------- #
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_ssh_keys(response: object):
|
|
71
|
+
table = fmt.Table()
|
|
72
|
+
table.header(["ID", "NAME", "DEFAULT", "SERVERS"])
|
|
73
|
+
keys = response.json()["ssh_keys"]
|
|
74
|
+
for key in keys:
|
|
75
|
+
table.row(
|
|
76
|
+
[
|
|
77
|
+
key["id"],
|
|
78
|
+
key["name"],
|
|
79
|
+
key["is_default"],
|
|
80
|
+
", ".join([str(k["id"]) for k in key["used_by"]]),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
table.print()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@ssh_key.command("list", aliases=["ls"], help="List SSH-keys.")
|
|
87
|
+
@options(GLOBAL_OPTIONS)
|
|
88
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
89
|
+
def ssh_key_list(config, profile, verbose, output_format):
|
|
90
|
+
client = create_client(config, profile)
|
|
91
|
+
response = _ssh_key_list(client)
|
|
92
|
+
fmt.printer(
|
|
93
|
+
response,
|
|
94
|
+
output_format=output_format,
|
|
95
|
+
func=print_ssh_keys,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------- #
|
|
100
|
+
# $ twc ssh-key get #
|
|
101
|
+
# ------------------------------------------------------------- #
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def print_ssh_key(response: object):
|
|
105
|
+
table = fmt.Table()
|
|
106
|
+
table.header(["ID", "NAME", "DEFAULT", "SERVERS"])
|
|
107
|
+
key = response.json()["ssh_key"]
|
|
108
|
+
table.row(
|
|
109
|
+
[
|
|
110
|
+
key["id"],
|
|
111
|
+
key["name"],
|
|
112
|
+
key["is_default"],
|
|
113
|
+
", ".join([str(k["id"]) for k in key["used_by"]]),
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
table.print()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@ssh_key.command("get", help="Get SSH-key by ID.")
|
|
120
|
+
@options(GLOBAL_OPTIONS)
|
|
121
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
122
|
+
@click.argument("ssh_key_id", required=True)
|
|
123
|
+
def ssh_key_get(config, profile, verbose, output_format, ssh_key_id):
|
|
124
|
+
client = create_client(config, profile)
|
|
125
|
+
response = _ssh_key_get(client, ssh_key_id)
|
|
126
|
+
fmt.printer(
|
|
127
|
+
response,
|
|
128
|
+
output_format=output_format,
|
|
129
|
+
func=print_ssh_key,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------- #
|
|
134
|
+
# $ twc ssh-key new #
|
|
135
|
+
# ------------------------------------------------------------- #
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@ssh_key.command("new", help="Add new SSH-key.")
|
|
139
|
+
@options(GLOBAL_OPTIONS)
|
|
140
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
141
|
+
@click.option("--name", help="SSH-key display name.")
|
|
142
|
+
@click.option(
|
|
143
|
+
"--default",
|
|
144
|
+
type=bool,
|
|
145
|
+
default=False,
|
|
146
|
+
show_default=True,
|
|
147
|
+
help="If True add this key to all new Cloud Servers.",
|
|
148
|
+
)
|
|
149
|
+
@click.argument("public_key_file", type=click.Path(exists=True), required=True)
|
|
150
|
+
def ssh_key_new(
|
|
151
|
+
config, profile, verbose, output_format, name, default, public_key_file
|
|
152
|
+
):
|
|
153
|
+
if not name:
|
|
154
|
+
name = os.path.basename(public_key_file)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
with open(public_key_file, "r", encoding="utf-8") as pubkey:
|
|
158
|
+
body = pubkey.read().strip()
|
|
159
|
+
except (OSError, IOError, FileNotFoundError) as error:
|
|
160
|
+
sys.exit(f"Error: {error}")
|
|
161
|
+
|
|
162
|
+
client = create_client(config, profile)
|
|
163
|
+
response = _ssh_key_new(
|
|
164
|
+
client,
|
|
165
|
+
name=name,
|
|
166
|
+
is_default=default,
|
|
167
|
+
body=body,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
fmt.printer(response, output_format=output_format, func=print_ssh_key)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------- #
|
|
174
|
+
# $ twc ssh-key edit #
|
|
175
|
+
# ------------------------------------------------------------- #
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@ssh_key.command("edit", help="Edit SSH-key and they properties.")
|
|
179
|
+
@options(GLOBAL_OPTIONS)
|
|
180
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
181
|
+
@click.option("--name", default=None, help="SSH-key display name.")
|
|
182
|
+
@click.option(
|
|
183
|
+
"--default",
|
|
184
|
+
type=bool,
|
|
185
|
+
default=False,
|
|
186
|
+
show_default=True,
|
|
187
|
+
help="If True add this key to all new Cloud Servers.",
|
|
188
|
+
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--file",
|
|
191
|
+
"public_key_file",
|
|
192
|
+
type=click.Path(),
|
|
193
|
+
default=None,
|
|
194
|
+
help="Public key file.",
|
|
195
|
+
)
|
|
196
|
+
@click.argument("ssh_key_id", required=True)
|
|
197
|
+
def ssh_key_edit(
|
|
198
|
+
config,
|
|
199
|
+
profile,
|
|
200
|
+
verbose,
|
|
201
|
+
output_format,
|
|
202
|
+
name,
|
|
203
|
+
default,
|
|
204
|
+
public_key_file,
|
|
205
|
+
ssh_key_id,
|
|
206
|
+
):
|
|
207
|
+
client = create_client(config, profile)
|
|
208
|
+
payload = {}
|
|
209
|
+
|
|
210
|
+
if name:
|
|
211
|
+
payload.update({"name": name})
|
|
212
|
+
|
|
213
|
+
if default:
|
|
214
|
+
payload.update({"is_default": default})
|
|
215
|
+
|
|
216
|
+
if public_key_file:
|
|
217
|
+
try:
|
|
218
|
+
with open(public_key_file, "r", encoding="utf-8") as pubkey:
|
|
219
|
+
body = pubkey.read().strip()
|
|
220
|
+
payload.update({"body": body})
|
|
221
|
+
except (OSError, IOError, FileNotFoundError) as error:
|
|
222
|
+
sys.exit(f"Error: {error}")
|
|
223
|
+
|
|
224
|
+
if not payload:
|
|
225
|
+
raise click.UsageError(
|
|
226
|
+
"Nothing to do. Set one of ['--name', '--file', '--default']"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
response = _ssh_key_edit(client, ssh_key_id, data=payload)
|
|
230
|
+
|
|
231
|
+
fmt.printer(response, output_format=output_format, func=print_ssh_key)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ------------------------------------------------------------- #
|
|
235
|
+
# $ twc ssh-key add #
|
|
236
|
+
# ------------------------------------------------------------- #
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@ssh_key.command(
|
|
240
|
+
"add", aliases=["copy"], help="Copy SSH-keys to Cloud Server."
|
|
241
|
+
)
|
|
242
|
+
@options(GLOBAL_OPTIONS)
|
|
243
|
+
@click.option(
|
|
244
|
+
"--to-server",
|
|
245
|
+
"server_id",
|
|
246
|
+
type=int,
|
|
247
|
+
help="Cloud Server ID.",
|
|
248
|
+
)
|
|
249
|
+
@click.argument("ssh_key_ids", nargs=-1, required=True)
|
|
250
|
+
def ssh_key_add(config, profile, verbose, server_id, ssh_key_ids):
|
|
251
|
+
client = create_client(config, profile)
|
|
252
|
+
response = _ssh_key_add(client, server_id, ssh_key_ids=list(ssh_key_ids))
|
|
253
|
+
if response.status_code == 204:
|
|
254
|
+
print(server_id)
|
|
255
|
+
else:
|
|
256
|
+
fmt.printer(response)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------- #
|
|
260
|
+
# $ twc ssh-key remove #
|
|
261
|
+
# ------------------------------------------------------------- #
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@ssh_key.command("remove", aliases=["rm"], help="Remove SSH-keys.")
|
|
265
|
+
@options(GLOBAL_OPTIONS)
|
|
266
|
+
@click.option(
|
|
267
|
+
"--from-server",
|
|
268
|
+
"server_id",
|
|
269
|
+
type=int,
|
|
270
|
+
help="Remove SSH-key from Cloud Server instead of remove key itself.",
|
|
271
|
+
)
|
|
272
|
+
@click.confirmation_option(
|
|
273
|
+
prompt="If you do not specify '--from-server' option "
|
|
274
|
+
"SSH-key will be deleted\nfrom all servers where it was added "
|
|
275
|
+
"and also deleted itself.\nContinue?"
|
|
276
|
+
)
|
|
277
|
+
@click.argument("ssh_key_ids", nargs=-1, required=True)
|
|
278
|
+
def ssh_key_remove(config, profile, verbose, server_id, ssh_key_ids):
|
|
279
|
+
client = create_client(config, profile)
|
|
280
|
+
if server_id:
|
|
281
|
+
if len(ssh_key_ids) >= 2:
|
|
282
|
+
raise click.UsageError("Cannot remove multiple keys from server.")
|
|
283
|
+
response = _ssh_key_remove_from_server(
|
|
284
|
+
client, server_id, list(ssh_key_ids)[0]
|
|
285
|
+
)
|
|
286
|
+
if response.status_code == 204:
|
|
287
|
+
print(list(ssh_key_ids)[0])
|
|
288
|
+
else:
|
|
289
|
+
fmt.printer(response)
|
|
290
|
+
else:
|
|
291
|
+
for key_id in ssh_key_ids:
|
|
292
|
+
response = _ssh_key_remove(client, key_id)
|
|
293
|
+
if response.status_code == 204:
|
|
294
|
+
print(key_id)
|
|
295
|
+
else:
|
|
296
|
+
fmt.printer(response)
|
twc/fmt.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Format console output."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Table:
|
|
12
|
+
"""Print table. Example::
|
|
13
|
+
|
|
14
|
+
>>> t = Table()
|
|
15
|
+
>>> t.header(['KEY', 'VALUE']) # header is optional
|
|
16
|
+
>>> t.row(['key 1', 'value 1'])
|
|
17
|
+
>>> t.row(['key 2', 'value 2'])
|
|
18
|
+
>>> t.rows(
|
|
19
|
+
... [
|
|
20
|
+
... ['key 3', 'value 3'],
|
|
21
|
+
... ['key 4', 'value 4']
|
|
22
|
+
... ]
|
|
23
|
+
>>> )
|
|
24
|
+
>>> t.print()
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, whitespace: str = "\t"):
|
|
29
|
+
self.__rows = []
|
|
30
|
+
self.__whitespace = whitespace
|
|
31
|
+
|
|
32
|
+
def header(self, columns: list):
|
|
33
|
+
"""Add `columns` list as first element in `rows` list."""
|
|
34
|
+
self.__rows.insert(0, [str(col) for col in columns])
|
|
35
|
+
|
|
36
|
+
def row(self, row: list):
|
|
37
|
+
"""Add new row to table."""
|
|
38
|
+
self.__rows.append([str(col) for col in row])
|
|
39
|
+
|
|
40
|
+
def rows(self, rows: list):
|
|
41
|
+
"""Add multiple rows to table."""
|
|
42
|
+
for row in rows:
|
|
43
|
+
self.row(row)
|
|
44
|
+
|
|
45
|
+
def print(self):
|
|
46
|
+
"""Print table content to terminal."""
|
|
47
|
+
widths = [max(map(len, col)) for col in zip(*self.__rows)]
|
|
48
|
+
for row in self.__rows:
|
|
49
|
+
click.echo(
|
|
50
|
+
self.__whitespace.join(
|
|
51
|
+
(val.ljust(width) for val, width in zip(row, widths))
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Printer:
|
|
57
|
+
"""Display data in different formats."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, data: object):
|
|
60
|
+
self._data = data
|
|
61
|
+
|
|
62
|
+
def raw(self):
|
|
63
|
+
"""Print raw API response text (mostly raw JSON)."""
|
|
64
|
+
click.echo(self._data.text)
|
|
65
|
+
|
|
66
|
+
def colorize(self, data: str, lang: str = "json"):
|
|
67
|
+
"""Print colorized output. Fallback to non-color."""
|
|
68
|
+
try:
|
|
69
|
+
from pygments import highlight
|
|
70
|
+
from pygments.lexers import JsonLexer, YamlLexer
|
|
71
|
+
from pygments.formatters import TerminalFormatter
|
|
72
|
+
|
|
73
|
+
if lang == "json":
|
|
74
|
+
lexer = JsonLexer()
|
|
75
|
+
elif lang == "yaml":
|
|
76
|
+
lexer = YamlLexer()
|
|
77
|
+
|
|
78
|
+
click.echo(highlight(data, lexer, TerminalFormatter()).strip())
|
|
79
|
+
except ImportError:
|
|
80
|
+
click.echo(data)
|
|
81
|
+
|
|
82
|
+
def pretty_json(self):
|
|
83
|
+
"""Print colorized JSON output. Fallback to non-color output if
|
|
84
|
+
Pygments not installed and fallback to raw output on JSONDecodeError.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
json_data = json.dumps(
|
|
88
|
+
self._data.json(), indent=4, sort_keys=True, ensure_ascii=False
|
|
89
|
+
)
|
|
90
|
+
self.colorize(json_data, lang="json")
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
self.raw()
|
|
93
|
+
|
|
94
|
+
def pretty_yaml(self):
|
|
95
|
+
"""Print colorized YAML output. Fallback to non-color output if
|
|
96
|
+
Pygments not installed and fallback to raw output on YAMLError.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
yaml_data = yaml.dump(
|
|
100
|
+
self._data.json(), sort_keys=True, allow_unicode=True
|
|
101
|
+
)
|
|
102
|
+
self.colorize(yaml_data, lang="yaml")
|
|
103
|
+
except yaml.YAMLError:
|
|
104
|
+
self.raw()
|
|
105
|
+
|
|
106
|
+
def print(self, output_format: str = "raw", func=None, **kwargs):
|
|
107
|
+
"""Print `requests.Response` object.
|
|
108
|
+
- If `output_format` is 'raw' print raw HTTP body text.
|
|
109
|
+
- If `output_format` is 'json' print colorized JSON.
|
|
110
|
+
- If `output_format` is 'yaml' print colorized YAML.
|
|
111
|
+
- If function `func` is passed use it to print response data.
|
|
112
|
+
"""
|
|
113
|
+
if output_format == "raw":
|
|
114
|
+
self.raw()
|
|
115
|
+
elif output_format == "json":
|
|
116
|
+
self.pretty_json()
|
|
117
|
+
elif output_format == "yaml":
|
|
118
|
+
self.pretty_yaml()
|
|
119
|
+
else:
|
|
120
|
+
try:
|
|
121
|
+
if func:
|
|
122
|
+
func(self._data, **kwargs)
|
|
123
|
+
except KeyError: # fallback to 'json' or 'raw' on error
|
|
124
|
+
click.echo(
|
|
125
|
+
"Error: Cannot represent output. Fallback to JSON.",
|
|
126
|
+
err=True,
|
|
127
|
+
)
|
|
128
|
+
self.pretty_json()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def printer(response: object, output_format: str = "raw", func=None, **kwargs):
|
|
132
|
+
"""Print `requests.Response` object.
|
|
133
|
+
This is the same as `Printer(*args, **kwargs).print()`.
|
|
134
|
+
"""
|
|
135
|
+
to_print = Printer(response)
|
|
136
|
+
if func:
|
|
137
|
+
to_print.print(output_format, func=func, **kwargs)
|
|
138
|
+
else:
|
|
139
|
+
to_print.print(output_format, **kwargs)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def query_dict(data: dict, keys: list):
|
|
143
|
+
"""Return value of dict by list of keys. For example::
|
|
144
|
+
|
|
145
|
+
>>> mydict = {'server':{'os':{'name':'ubuntu','version':'22.04',}}}
|
|
146
|
+
>>> query = 'server.os.name'
|
|
147
|
+
>>> result = query_dict(mydict, query.split('.'))
|
|
148
|
+
|
|
149
|
+
In result: 'ubuntu'
|
|
150
|
+
"""
|
|
151
|
+
exp = ""
|
|
152
|
+
for key in keys:
|
|
153
|
+
if re.match(r"^[a-zA-Z0-9]+$", key):
|
|
154
|
+
exp = exp + f"['{key}']"
|
|
155
|
+
try:
|
|
156
|
+
# pylint: disable=eval-used
|
|
157
|
+
return eval(f"{data}{exp}", {"__builtins__": {}}, {})
|
|
158
|
+
except TypeError:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def filter_list(objects: list, filters: str) -> list:
|
|
163
|
+
"""Filter list of objects. Return filtered list.
|
|
164
|
+
|
|
165
|
+
`filters` is a string with following format::
|
|
166
|
+
|
|
167
|
+
``<key>:<value>,<key>:<value>``
|
|
168
|
+
|
|
169
|
+
Key-Value pairs count is unlimited. Available filter keys and
|
|
170
|
+
values depends on passed object.
|
|
171
|
+
"""
|
|
172
|
+
if not re.match(r"^(([a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+),?)+$", filters):
|
|
173
|
+
sys.exit("Error: Invalid filter format")
|
|
174
|
+
|
|
175
|
+
for key_val in filters.split(","):
|
|
176
|
+
try:
|
|
177
|
+
key, val = key_val.split(":")
|
|
178
|
+
objects = list(
|
|
179
|
+
filter(
|
|
180
|
+
# pylint: disable=cell-var-from-loop
|
|
181
|
+
# This is fine
|
|
182
|
+
lambda x: str(query_dict(x, key.split("."))) == val,
|
|
183
|
+
objects,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
except (KeyError, ValueError):
|
|
187
|
+
return []
|
|
188
|
+
return objects
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2023 TWC Developers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: twc-cli
|
|
3
|
+
Version: 1.0.0rc0
|
|
4
|
+
Summary: Timeweb Cloud Command Line Interface.
|
|
5
|
+
Home-page: https://github.com/timeweb-cloud/twc
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: ge
|
|
8
|
+
Author-email: dev@timeweb.cloud
|
|
9
|
+
Requires-Python: >=3.7,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
18
|
+
Requires-Dist: click-aliases (>=1.0.1,<2.0.0)
|
|
19
|
+
Requires-Dist: pygments (>=2.14.0,<3.0.0)
|
|
20
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
21
|
+
Requires-Dist: requests (>=2.28.1,<3.0.0)
|
|
22
|
+
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
23
|
+
Project-URL: Repository, https://github.com/timeweb-cloud/twc
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
Timeweb Cloud Command Line Interface and simple SDK 💫
|
|
29
|
+
|
|
30
|
+
> [Документация на русском](https://github.com/timeweb-cloud/twc/blob/master/docs/ru/README.md) 🇷🇺
|
|
31
|
+
|
|
32
|
+
# Installation
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install twc-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
# Getting started
|
|
39
|
+
|
|
40
|
+
Get Timeweb Cloud [access token](https://timeweb.cloud/my/api-keys) and
|
|
41
|
+
configure **twc** with command:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
twc config
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Enter your access token and hit `Enter`.
|
|
48
|
+
|
|
49
|
+
Configuration done! Let's use:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
twc --help
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
# Shell completion
|
|
56
|
+
|
|
57
|
+
## Bash
|
|
58
|
+
|
|
59
|
+
Add this to **~/.bashrc**:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
eval "$(_TWC_COMPLETE=bash_source twc)"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Zsh
|
|
66
|
+
|
|
67
|
+
Add this to **~/.zshrc**:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
eval "$(_TWC_COMPLETE=zsh_source twc)"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Fish
|
|
74
|
+
|
|
75
|
+
Add this to **~/.config/fish/completions/tw.fish**:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
eval (env _TWC_COMPLETE=fish_source twc)
|
|
79
|
+
```
|
|
80
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
CHANGELOG.md,sha256=DqT4vfLwfbjWLzfhXd2ijzshUHX_xT8BZcGU9eub37o,39
|
|
2
|
+
COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
|
|
3
|
+
twc/__init__.py,sha256=NwPAMNw3NuHdWGQvWS9_lromVF6VM194oVOipojfJns,113
|
|
4
|
+
twc/__main__.py,sha256=ZqBKuNFBa-Q-hsRk7jhZPT5H2Lg7l99Hk_phl53S6U0,459
|
|
5
|
+
twc/__version__.py,sha256=PSBZ7n33LNwjZEyFsYWX2QQn2SbLMNXgZLUZg9iAjuQ,420
|
|
6
|
+
twc/api/__init__.py,sha256=1OFp1sNrikcJH_JjkhGjnnKpyfg-SCOH_RBkgG-Mqw8,59
|
|
7
|
+
twc/api/client.py,sha256=yfQJJoDyM86IZe281HTnVPa85u7wEvkDjAIpmlgn0NE,21064
|
|
8
|
+
twc/api/exceptions.py,sha256=-IKU8HEaFvmvvHd3NaiF6ticnONrGz7L9TYSTF9d5Lg,851
|
|
9
|
+
twc/commands/__init__.py,sha256=6vih8qQcZXh5FX4Wv-mplTw6ooz_kMBkGNmaczFKAsg,8319
|
|
10
|
+
twc/commands/account.py,sha256=FsqjIiPjCCQ6yvrea3ElzsgbZ1B8nTzNOpTuzlPFcME,5368
|
|
11
|
+
twc/commands/config.py,sha256=93awq8dnjs7hB6Ffo8PAj03yMR1M_SI23TFPlNry8A8,1471
|
|
12
|
+
twc/commands/server.py,sha256=G5w4JOPQT_mR2ez1MhXXWahQtSLEVycSueu4VJkbbAU,65333
|
|
13
|
+
twc/commands/ssh_key.py,sha256=6ucRbeOegiLENuqHTdqb7X0bK4__n-_i1Iog6VS0IOk,8218
|
|
14
|
+
twc/fmt.py,sha256=Hso-xiWXPhz6aFLL5BVLrgtr3Wr7DBESqetJqYA9g90,5716
|
|
15
|
+
twc_cli-1.0.0rc0.dist-info/COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
|
|
16
|
+
twc_cli-1.0.0rc0.dist-info/METADATA,sha256=Wc1z9T0TyQGJzsAXgGoFee8MAxuXraRTBzRVxGRzei0,1777
|
|
17
|
+
twc_cli-1.0.0rc0.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
|
18
|
+
twc_cli-1.0.0rc0.dist-info/entry_points.txt,sha256=tmTaVRhm8BkNrXC_9XJMum7O9wFVOvkXcBetxmahWvE,40
|
|
19
|
+
twc_cli-1.0.0rc0.dist-info/RECORD,,
|