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.

@@ -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
+ ![TWC CLI](https://github.com/timeweb-cloud/twc/blob/master/artwork/logo.svg)
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ twc=twc.__main__:cli
3
+