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/__init__.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""TWC CLI commands package.
|
|
2
|
+
|
|
3
|
+
__init__.py module contains common functions, decorators and variables.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import toml
|
|
12
|
+
|
|
13
|
+
from twc.__version__ import __version__, __pyversion__
|
|
14
|
+
from twc.api import TimewebCloud
|
|
15
|
+
from twc.api import (
|
|
16
|
+
UnauthorizedError,
|
|
17
|
+
NonJSONResponseError,
|
|
18
|
+
BadResponseError,
|
|
19
|
+
UnexpectedResponseError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# -----------------------------------------------------------------------
|
|
24
|
+
# Configuration
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
USER_AGENT = f"TWC-CLI/{__version__} Python {__pyversion__}"
|
|
28
|
+
DEFAULT_CONFIG = {"default": {"token": ""}}
|
|
29
|
+
DEFAULT_CONFIGURATOR_ID = 11
|
|
30
|
+
REGIONS_WITH_CONFIGURATOR = ["ru-1"]
|
|
31
|
+
REGIONS_WITH_IPV6 = ["ru-1"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_default_config_file() -> str:
|
|
35
|
+
if os.name == "nt":
|
|
36
|
+
env_home = "USERPROFILE"
|
|
37
|
+
else:
|
|
38
|
+
env_home = "HOME"
|
|
39
|
+
return os.path.join(os.getenv(env_home), ".twcrc")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_config(filepath: str = get_default_config_file()):
|
|
43
|
+
"""Load configuration from TOML config file."""
|
|
44
|
+
try:
|
|
45
|
+
with open(filepath, "r", encoding="utf-8") as file:
|
|
46
|
+
return toml.load(file)
|
|
47
|
+
except (OSError, FileNotFoundError) as error:
|
|
48
|
+
sys.exit(f"Error: {filepath}: {error}: Try run 'twc config'")
|
|
49
|
+
except toml.TomlDecodeError as error:
|
|
50
|
+
sys.exit(f"Error: {filepath}: {error}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def set_value_from_config(ctx, param, value):
|
|
54
|
+
"""Callback for Click to load option values from configuration file."""
|
|
55
|
+
if value is None:
|
|
56
|
+
if not os.path.exists(ctx.params["config"]):
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
value = load_config(ctx.params["config"])[ctx.params["profile"]][
|
|
60
|
+
param.name
|
|
61
|
+
]
|
|
62
|
+
except KeyError:
|
|
63
|
+
return None
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# -----------------------------------------------------------------------
|
|
68
|
+
# Logger
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def set_logger(ctx, param, value):
|
|
72
|
+
"""Click callback for '--verbose' option. Set logger settings.
|
|
73
|
+
Log HTTP requests and anything else.
|
|
74
|
+
"""
|
|
75
|
+
if value:
|
|
76
|
+
logging.basicConfig(
|
|
77
|
+
level=logging.DEBUG,
|
|
78
|
+
format="%(asctime)s:%(levelname)s:%(name)s: %(message)s",
|
|
79
|
+
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def log(message: str, *args, **kwargs):
|
|
84
|
+
"""Print DEBUG log message"""
|
|
85
|
+
logging.debug(message, *args, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def log_request(response: object):
|
|
89
|
+
"""Log data from `requests.PreparedRequest` and `requests.Response`
|
|
90
|
+
objects. Hide auth Bearer token.
|
|
91
|
+
"""
|
|
92
|
+
request_headers = response.request.headers
|
|
93
|
+
request_headers.update(
|
|
94
|
+
{"Authorization": "Bearer <SENSITIVE_DATA_DELETED>"}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
for key in list(request_headers.keys()):
|
|
98
|
+
log(f"Request header: {key}: {request_headers[key]}")
|
|
99
|
+
|
|
100
|
+
if response.request.method in ["POST", "PUT", "PATCH"]:
|
|
101
|
+
log(f"Request body (raw): {response.request.body}")
|
|
102
|
+
|
|
103
|
+
for key in list(response.headers.keys()):
|
|
104
|
+
log(f"Response header: {key}: {response.headers[key]}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# -----------------------------------------------------------------------
|
|
108
|
+
# CLI
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
GLOBAL_OPTIONS = [
|
|
112
|
+
click.help_option("--help"),
|
|
113
|
+
click.version_option(__version__, "--version", prog_name="twc"),
|
|
114
|
+
click.option(
|
|
115
|
+
"--verbose",
|
|
116
|
+
"-v",
|
|
117
|
+
is_flag=True,
|
|
118
|
+
envvar="TWC_DEBUG",
|
|
119
|
+
callback=set_logger,
|
|
120
|
+
help="Enable verbose mode.",
|
|
121
|
+
),
|
|
122
|
+
click.option(
|
|
123
|
+
"--config",
|
|
124
|
+
"-c",
|
|
125
|
+
metavar="FILE",
|
|
126
|
+
envvar="TWC_CONFIG_FILE",
|
|
127
|
+
default=get_default_config_file(),
|
|
128
|
+
is_eager=True,
|
|
129
|
+
show_default=True,
|
|
130
|
+
help="Use config file.",
|
|
131
|
+
),
|
|
132
|
+
click.option(
|
|
133
|
+
"--profile",
|
|
134
|
+
"-p",
|
|
135
|
+
envvar="TWC_PROFILE",
|
|
136
|
+
default="default",
|
|
137
|
+
show_default=True,
|
|
138
|
+
is_eager=True,
|
|
139
|
+
help="Use profile.",
|
|
140
|
+
),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
OUTPUT_FORMAT_OPTION = [
|
|
144
|
+
click.option(
|
|
145
|
+
"--output",
|
|
146
|
+
"-o",
|
|
147
|
+
"output_format",
|
|
148
|
+
type=click.Choice(["default", "raw", "json", "yaml"]),
|
|
149
|
+
envvar="TWC_OUTPUT_FORMAT",
|
|
150
|
+
callback=set_value_from_config,
|
|
151
|
+
help="Output format.",
|
|
152
|
+
)
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def options(options_list: list):
|
|
157
|
+
"""Add multiple options to command."""
|
|
158
|
+
|
|
159
|
+
def wrapper(func):
|
|
160
|
+
for option in reversed(options_list):
|
|
161
|
+
func = option(func)
|
|
162
|
+
return func
|
|
163
|
+
|
|
164
|
+
return wrapper
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class MutuallyExclusiveOption(click.Option):
|
|
168
|
+
"""Add mutually exclusive options support for Click. Example::
|
|
169
|
+
|
|
170
|
+
@click.option(
|
|
171
|
+
"--dry",
|
|
172
|
+
is_flag=True,
|
|
173
|
+
cls=MutuallyExclusiveOption,
|
|
174
|
+
mutually_exclusive=["wet"],
|
|
175
|
+
)
|
|
176
|
+
@click.option("--wet", is_flag=True)
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, *args, **kwargs):
|
|
180
|
+
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
|
|
181
|
+
help = kwargs.get("help", "") # pylint: disable=redefined-builtin
|
|
182
|
+
if self.mutually_exclusive:
|
|
183
|
+
kwargs["help"] = help + (
|
|
184
|
+
" NOTE: This argument is mutually exclusive with "
|
|
185
|
+
f"arguments: [{', '.join(self.mutually_exclusive)}]."
|
|
186
|
+
)
|
|
187
|
+
super().__init__(*args, **kwargs)
|
|
188
|
+
|
|
189
|
+
def handle_parse_result(self, ctx, opts, args):
|
|
190
|
+
if self.mutually_exclusive.intersection(opts) and self.name in opts:
|
|
191
|
+
raise click.UsageError(
|
|
192
|
+
f"Illegal usage: '{self.name}' is mutually exclusive with "
|
|
193
|
+
f"arguments: [{', '.join(self.mutually_exclusive)}]."
|
|
194
|
+
)
|
|
195
|
+
return super().handle_parse_result(ctx, opts, args)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def confirm_action(question: str, default: str = "no"):
|
|
199
|
+
"""Ask a yes/no question via input() and return their answer.
|
|
200
|
+
|
|
201
|
+
The "answer" return value is True for "yes" or False for "no".
|
|
202
|
+
"""
|
|
203
|
+
valid = {
|
|
204
|
+
"yes": True,
|
|
205
|
+
"y": True,
|
|
206
|
+
"ye": True,
|
|
207
|
+
"no": False,
|
|
208
|
+
"n": False,
|
|
209
|
+
}
|
|
210
|
+
if default is None:
|
|
211
|
+
prompt = "[y/n]"
|
|
212
|
+
elif default == "yes":
|
|
213
|
+
prompt = "[Y/n]"
|
|
214
|
+
elif default == "no":
|
|
215
|
+
prompt = "[y/N]"
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"Invalid default answer: '{default}'")
|
|
218
|
+
|
|
219
|
+
while True:
|
|
220
|
+
choice = input(f"{question} {prompt}: ").lower()
|
|
221
|
+
if default is not None and choice == "":
|
|
222
|
+
return valid[default]
|
|
223
|
+
if choice in valid:
|
|
224
|
+
return valid[choice]
|
|
225
|
+
sys.exit("Please respond with 'yes' or 'no' (or 'y' or 'n').")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# -----------------------------------------------------------------------
|
|
229
|
+
# API interaction
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def handle_request(func):
|
|
233
|
+
def wrapper(*args, **kwargs):
|
|
234
|
+
try:
|
|
235
|
+
response = func(*args, **kwargs)
|
|
236
|
+
|
|
237
|
+
log_request(response)
|
|
238
|
+
if not response.text:
|
|
239
|
+
log("No response body")
|
|
240
|
+
|
|
241
|
+
if response.status_code not in [200, 201, 204]:
|
|
242
|
+
raise BadResponseError
|
|
243
|
+
except UnauthorizedError:
|
|
244
|
+
print(
|
|
245
|
+
"Error: Unauthorized. "
|
|
246
|
+
+ "Please check your API access token. Try run 'twc config'",
|
|
247
|
+
file=sys.stderr,
|
|
248
|
+
)
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
except BadResponseError:
|
|
251
|
+
resp = response.json()
|
|
252
|
+
print("Error occurred. Details:", file=sys.stderr)
|
|
253
|
+
print(f"Status code: {resp['status_code']}", file=sys.stderr)
|
|
254
|
+
print(f" Error code: {resp['error_code']}", file=sys.stderr)
|
|
255
|
+
if isinstance(resp["message"], list):
|
|
256
|
+
for message in resp["message"]:
|
|
257
|
+
print(f" Message: {message}", file=sys.stderr)
|
|
258
|
+
else:
|
|
259
|
+
print(f" Message: {resp['message']}", file=sys.stderr)
|
|
260
|
+
print(f"Response ID: {resp['response_id']}", file=sys.stderr)
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
except (NonJSONResponseError, UnexpectedResponseError) as error:
|
|
263
|
+
sys.exit(f"Error: {error}")
|
|
264
|
+
return response
|
|
265
|
+
|
|
266
|
+
return wrapper
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def create_client(config, profile):
|
|
270
|
+
"""Create API client instance with access token."""
|
|
271
|
+
|
|
272
|
+
log(f"TWC CLI {__version__} Python {__pyversion__}")
|
|
273
|
+
log(f"Args: {sys.argv[1:]}")
|
|
274
|
+
|
|
275
|
+
env_token = os.getenv("TWC_TOKEN")
|
|
276
|
+
|
|
277
|
+
if env_token:
|
|
278
|
+
log("Get Timeweb Cloud token from environment variable")
|
|
279
|
+
return TimewebCloud(env_token, user_agent=USER_AGENT)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
log(f"Configuration: config file={config}; profile={profile}")
|
|
283
|
+
token = load_config(config)[profile]["token"]
|
|
284
|
+
return TimewebCloud(token, user_agent=USER_AGENT)
|
|
285
|
+
except KeyError:
|
|
286
|
+
sys.exit(f"Profile '{profile}' not found in {config}")
|
twc/commands/account.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Account management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from twc import fmt
|
|
6
|
+
from . import (
|
|
7
|
+
create_client,
|
|
8
|
+
handle_request,
|
|
9
|
+
options,
|
|
10
|
+
GLOBAL_OPTIONS,
|
|
11
|
+
OUTPUT_FORMAT_OPTION,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@handle_request
|
|
16
|
+
def _account_finances(client):
|
|
17
|
+
return client.get_account_finances()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@handle_request
|
|
21
|
+
def _account_status(client):
|
|
22
|
+
return client.get_account_status()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@handle_request
|
|
26
|
+
def _restrictions_status(client):
|
|
27
|
+
return client.get_account_restrictions()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------- #
|
|
31
|
+
# $ twc account #
|
|
32
|
+
# ------------------------------------------------------------- #
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.group()
|
|
36
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
37
|
+
def account():
|
|
38
|
+
"""Manage Timeweb Cloud account."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------- #
|
|
42
|
+
# $ twc account status #
|
|
43
|
+
# ------------------------------------------------------------- #
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def print_account_status(response: object):
|
|
47
|
+
table = fmt.Table()
|
|
48
|
+
status = response.json()["status"]
|
|
49
|
+
translated_keys = {
|
|
50
|
+
"company_info": "Company",
|
|
51
|
+
"ym_client_id": "Yandex.Metrika client ID",
|
|
52
|
+
"is_blocked": "Blocked",
|
|
53
|
+
"is_permanent_blocked": "Permanently blocked",
|
|
54
|
+
"is_send_bill_letters": "Send bill emails",
|
|
55
|
+
"last_password_changed_at": "Password changed at",
|
|
56
|
+
}
|
|
57
|
+
for key in status.keys():
|
|
58
|
+
try:
|
|
59
|
+
if key == "company_info":
|
|
60
|
+
table.row([translated_keys[key], ":", status[key]["name"]])
|
|
61
|
+
else:
|
|
62
|
+
table.row([translated_keys[key], ":", status[key]])
|
|
63
|
+
except KeyError:
|
|
64
|
+
pass
|
|
65
|
+
table.print()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@account.command("status", help="Get account status.")
|
|
69
|
+
@options(GLOBAL_OPTIONS)
|
|
70
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
71
|
+
def account_status(config, profile, verbose, output_format):
|
|
72
|
+
client = create_client(config, profile)
|
|
73
|
+
response = _account_status(client)
|
|
74
|
+
fmt.printer(
|
|
75
|
+
response, output_format=output_format, func=print_account_status
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------- #
|
|
80
|
+
# $ twc account finances #
|
|
81
|
+
# ------------------------------------------------------------- #
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_account_finances(response: object):
|
|
85
|
+
table = fmt.Table()
|
|
86
|
+
finances = response.json()["finances"]
|
|
87
|
+
translated_keys = {
|
|
88
|
+
"balance": "Balance",
|
|
89
|
+
"currency": "Currency",
|
|
90
|
+
"discount_end_date_at": "Discount ends at",
|
|
91
|
+
"discount_percent": "Discount",
|
|
92
|
+
"hourly_cost": "Hourly cost",
|
|
93
|
+
"hourly_fee": "Hourly fee",
|
|
94
|
+
"monthly_cost": "Monthly cost",
|
|
95
|
+
"monthly_fee": "Monthly fee",
|
|
96
|
+
"total_paid": "Total paid",
|
|
97
|
+
"hours_left": "Hours left",
|
|
98
|
+
"autopay_card_info": "Autopay Card",
|
|
99
|
+
}
|
|
100
|
+
for key in finances.keys():
|
|
101
|
+
try:
|
|
102
|
+
table.row([translated_keys[key], ":", finances[key]])
|
|
103
|
+
except KeyError:
|
|
104
|
+
pass
|
|
105
|
+
table.print()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@account.command("finances", help="Get finances.")
|
|
109
|
+
@options(GLOBAL_OPTIONS)
|
|
110
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
111
|
+
def account_finances(config, profile, verbose, output_format):
|
|
112
|
+
client = create_client(config, profile)
|
|
113
|
+
response = _account_finances(client)
|
|
114
|
+
fmt.printer(
|
|
115
|
+
response, output_format=output_format, func=print_account_finances
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------- #
|
|
120
|
+
# $ twc account access #
|
|
121
|
+
# ------------------------------------------------------------- #
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@account.group()
|
|
125
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
126
|
+
def access():
|
|
127
|
+
"""Manage account access restrictions."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------- #
|
|
131
|
+
# $ twc account access restrictions #
|
|
132
|
+
# ------------------------------------------------------------- #
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def print_restrictions_status(response: object, by_ip: bool, by_country: bool):
|
|
136
|
+
restrictions = response.json()
|
|
137
|
+
|
|
138
|
+
if not by_ip and not by_country:
|
|
139
|
+
by_ip = by_country = True
|
|
140
|
+
|
|
141
|
+
if by_ip:
|
|
142
|
+
if restrictions["is_ip_restrictions_enabled"]:
|
|
143
|
+
click.echo("IP restrictions: enabled")
|
|
144
|
+
click.echo("Allowed IPs:")
|
|
145
|
+
for ip_addr in restrictions["white_list"]["ips"]:
|
|
146
|
+
click.echo(f" - {ip_addr}")
|
|
147
|
+
else:
|
|
148
|
+
click.echo("IP restrictions: disabled")
|
|
149
|
+
|
|
150
|
+
if by_country:
|
|
151
|
+
if restrictions["is_country_restrictions_enabled"]:
|
|
152
|
+
click.echo("Country restrictions: enabled")
|
|
153
|
+
click.echo("Allowed countries:")
|
|
154
|
+
for country in restrictions["white_list"]["countries"]:
|
|
155
|
+
click.echo(f" - {country}")
|
|
156
|
+
else:
|
|
157
|
+
click.echo("Country restrictions: disabled")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@access.command("restrictions", help="View access restrictions status.")
|
|
161
|
+
@options(GLOBAL_OPTIONS)
|
|
162
|
+
@options(OUTPUT_FORMAT_OPTION)
|
|
163
|
+
@click.option("--by-ip", is_flag=True, help="Display IP restrictions.")
|
|
164
|
+
@click.option(
|
|
165
|
+
"--by-country", is_flag=True, help="Display country restrictions."
|
|
166
|
+
)
|
|
167
|
+
def restrictions_status(
|
|
168
|
+
config, profile, verbose, output_format, by_ip, by_country
|
|
169
|
+
):
|
|
170
|
+
client = create_client(config, profile)
|
|
171
|
+
response = _restrictions_status(client)
|
|
172
|
+
fmt.printer(
|
|
173
|
+
response,
|
|
174
|
+
output_format=output_format,
|
|
175
|
+
func=print_restrictions_status,
|
|
176
|
+
by_ip=by_ip,
|
|
177
|
+
by_country=by_country,
|
|
178
|
+
)
|
twc/commands/config.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""CLI configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import ctypes
|
|
6
|
+
|
|
7
|
+
import toml
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from . import options, get_default_config_file, GLOBAL_OPTIONS, DEFAULT_CONFIG
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_config(filepath: str = get_default_config_file()):
|
|
14
|
+
"""Create new configuration file."""
|
|
15
|
+
if os.path.exists(filepath):
|
|
16
|
+
sys.exit(f"File '{filepath}' already exists.")
|
|
17
|
+
else:
|
|
18
|
+
click.echo("Create new configuration file. Enter your API token.")
|
|
19
|
+
while True:
|
|
20
|
+
token = input("Token: ")
|
|
21
|
+
if token:
|
|
22
|
+
DEFAULT_CONFIG.update({"default": {"token": token}})
|
|
23
|
+
break
|
|
24
|
+
click.echo("Please enter token. Press ^C to cancel.")
|
|
25
|
+
try:
|
|
26
|
+
with open(filepath, "w", encoding="utf-8") as file:
|
|
27
|
+
toml.dump(DEFAULT_CONFIG, file)
|
|
28
|
+
if os.name == "nt":
|
|
29
|
+
hidden_file_attr = 0x02
|
|
30
|
+
ctypes.windll.kernel32.SetFileAttributesW(
|
|
31
|
+
filepath, hidden_file_attr
|
|
32
|
+
)
|
|
33
|
+
click.echo(f"Success! Configuration is saved in {filepath}")
|
|
34
|
+
except OSError as error:
|
|
35
|
+
sys.exit(f"Error: {error}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------- #
|
|
39
|
+
# $ twc config #
|
|
40
|
+
# ------------------------------------------------------------- #
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.command("config", help="Make new configuration file.")
|
|
44
|
+
@options(GLOBAL_OPTIONS[:2])
|
|
45
|
+
def config():
|
|
46
|
+
make_config()
|