twc-cli 2.12.0__tar.gz → 2.13.1__tar.gz

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.

Files changed (35) hide show
  1. {twc_cli-2.12.0 → twc_cli-2.13.1}/CHANGELOG.md +13 -1
  2. {twc_cli-2.12.0 → twc_cli-2.13.1}/PKG-INFO +4 -4
  3. {twc_cli-2.12.0 → twc_cli-2.13.1}/pyproject.toml +2 -2
  4. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/__main__.py +2 -0
  5. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/__version__.py +1 -1
  6. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/api/client.py +41 -2
  7. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/__init__.py +1 -0
  8. twc_cli-2.13.1/twc/commands/apps.py +337 -0
  9. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/database.py +1 -1
  10. {twc_cli-2.12.0 → twc_cli-2.13.1}/COPYING +0 -0
  11. {twc_cli-2.12.0 → twc_cli-2.13.1}/README.md +0 -0
  12. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/__init__.py +0 -0
  13. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/api/__init__.py +0 -0
  14. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/api/base.py +0 -0
  15. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/api/exceptions.py +0 -0
  16. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/api/types.py +0 -0
  17. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/apiwrap.py +0 -0
  18. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/account.py +0 -0
  19. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/balancer.py +0 -0
  20. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/common.py +0 -0
  21. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/config.py +0 -0
  22. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/domain.py +0 -0
  23. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/firewall.py +0 -0
  24. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/floating_ip.py +0 -0
  25. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/image.py +0 -0
  26. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/kubernetes.py +0 -0
  27. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/project.py +0 -0
  28. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/server.py +0 -0
  29. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/ssh_key.py +0 -0
  30. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/storage.py +0 -0
  31. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/commands/vpc.py +0 -0
  32. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/fmt.py +0 -0
  33. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/typerx.py +0 -0
  34. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/utils.py +0 -0
  35. {twc_cli-2.12.0 → twc_cli-2.13.1}/twc/vars.py +0 -0
@@ -2,7 +2,19 @@
2
2
 
3
3
  В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
4
4
 
5
- # Версия 2.12.0 (2025.xx.xx)
5
+ # Версия 2.13.1 (2025.06.19)
6
+
7
+ ## Исправлено
8
+
9
+ - Исправлена ошибка приводящая к `invalid_admin_privilege` при передаче пустого списка привилегий для юзера БД в команде `twc database create`.
10
+
11
+ # Версия 2.13.0 (2025.06.17)
12
+
13
+ ## Добавлено
14
+
15
+ - Добавлена новая команда `twc apps` для управления сервисом Cloud Apps.
16
+
17
+ # Версия 2.12.0 (2025.04.11)
6
18
 
7
19
  ## Добавлено
8
20
 
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: twc-cli
3
- Version: 2.12.0
3
+ Version: 2.13.1
4
4
  Summary: Timeweb Cloud Command Line Interface.
5
- Home-page: https://github.com/timeweb-cloud/twc
6
5
  License: MIT
7
6
  Author: ge
8
7
  Author-email: dev@timeweb.cloud
@@ -17,10 +16,11 @@ Classifier: Programming Language :: Python :: 3.13
17
16
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
18
17
  Requires-Dist: pygments (>=2.18.0,<3.0.0)
19
18
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
20
- Requires-Dist: requests (>=2.32.3,<3.0.0)
19
+ Requires-Dist: requests (>=2.32.4,<3.0.0)
21
20
  Requires-Dist: shellingham (>=1.5.4,<2.0.0)
22
21
  Requires-Dist: toml (>=0.10.2,<0.11.0)
23
22
  Requires-Dist: typer-slim (>=0.12.3,<0.13.0)
23
+ Project-URL: Homepage, https://github.com/timeweb-cloud/twc
24
24
  Project-URL: Repository, https://github.com/timeweb-cloud/twc
25
25
  Description-Content-Type: text/markdown
26
26
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "twc-cli"
3
- version = "2.12.0"
3
+ version = "2.13.1"
4
4
  description = "Timeweb Cloud Command Line Interface."
5
5
  authors = ["ge <dev@timeweb.cloud>"]
6
6
  homepage = "https://github.com/timeweb-cloud/twc"
@@ -12,7 +12,7 @@ packages = [{ include = "twc", from = "." }]
12
12
 
13
13
  [tool.poetry.dependencies]
14
14
  python = "^3.8.19"
15
- requests = "^2.32.3"
15
+ requests = "^2.32.4"
16
16
  typer-slim = "^0.12.3"
17
17
  shellingham = "^1.5.4"
18
18
  colorama = "^0.4.6"
@@ -22,6 +22,7 @@ from .commands import (
22
22
  firewall,
23
23
  floating_ip,
24
24
  whoami,
25
+ apps,
25
26
  )
26
27
  from .commands.common import version_callback, version_option, verbose_option
27
28
 
@@ -33,6 +34,7 @@ cli = TyperAlias(
33
34
  )
34
35
  cli.add_typer(config, name="config")
35
36
  cli.add_typer(account, name="account")
37
+ cli.add_typer(apps, name="apps")
36
38
  cli.add_typer(server, name="server", aliases=["servers", "s"])
37
39
  cli.add_typer(ssh_key, name="ssh-key", aliases=["ssh-keys", "k"])
38
40
  cli.add_typer(image, name="image", aliases=["images", "i"])
@@ -12,5 +12,5 @@
12
12
  import sys
13
13
 
14
14
 
15
- __version__ = "2.12.0"
15
+ __version__ = "2.13.1"
16
16
  __pyversion__ = sys.version.replace("\n", "")
@@ -2,10 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network
5
6
  from typing import Optional, Union, List
6
- from uuid import UUID
7
7
  from pathlib import Path
8
- from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network
8
+ from uuid import UUID
9
+
10
+ import yaml
9
11
 
10
12
  from .base import TimewebCloudBase
11
13
  from .types import (
@@ -34,6 +36,43 @@ from .types import (
34
36
  class TimewebCloud(TimewebCloudBase):
35
37
  """Timeweb Cloud API client class."""
36
38
 
39
+ # -----------------------------------------------------------------------
40
+ # Apps
41
+
42
+ def get_apps(self):
43
+ """Return Timeweb Cloud apps list."""
44
+ return self._request("GET", f"{self.api_url}/apps")
45
+
46
+ def create_app(self, yaml_config_path: str):
47
+ """Create Timeweb Cloud app."""
48
+
49
+ with open(yaml_config_path, "r", encoding="utf-8") as f:
50
+ payload = yaml.safe_load(f)
51
+ payload = payload["app"]
52
+ return self._request("POST", f"{self.api_url}/apps", json=payload)
53
+
54
+ def get_app(self, app_id: int):
55
+ """Return Timeweb Cloud app."""
56
+ return self._request("GET", f"{self.api_url}/apps/{app_id}")
57
+
58
+ def get_vcs_providers(self):
59
+ """Return Timeweb Cloud vcs providers."""
60
+ return self._request("GET", f"{self.api_url}/vcs-provider")
61
+
62
+ def get_repositories(self, vcs_provider_id: str):
63
+ """Return Timeweb Cloud user vcs repositories."""
64
+ return self._request(
65
+ "GET", f"{self.api_url}/vcs-provider/{vcs_provider_id}"
66
+ )
67
+
68
+ def get_apps_tarifs(self):
69
+ """Return Timeweb Cloud Apps tarifs"""
70
+ return self._request("GET", f"{self.api_url}/presets/apps")
71
+
72
+ def delete_app(self, app_id: int):
73
+ """Delete Timeweb Cloud app."""
74
+ return self._request("DELETE", f"{self.api_url}/apps/{app_id}")
75
+
37
76
  # -----------------------------------------------------------------------
38
77
  # Account
39
78
 
@@ -1,6 +1,7 @@
1
1
  """Commands."""
2
2
 
3
3
  from .account import account, whoami
4
+ from .apps import apps
4
5
  from .config import config
5
6
  from .server import server
6
7
  from .ssh_key import ssh_key
@@ -0,0 +1,337 @@
1
+ """Manage apps."""
2
+
3
+ from typing import Optional
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from requests import Response
8
+
9
+ from twc import fmt
10
+ from twc.typerx import TyperAlias
11
+ from twc.apiwrap import create_client
12
+ from .common import (
13
+ verbose_option,
14
+ config_option,
15
+ profile_option,
16
+ filter_option,
17
+ output_format_option,
18
+ )
19
+
20
+
21
+ apps = TyperAlias(help=__doc__)
22
+
23
+
24
+ # ------------------------------------------------------------- #
25
+ # $ twc apps list #
26
+ # ------------------------------------------------------------- #
27
+
28
+
29
+ def print_apps(response: Response, filters: Optional[str]):
30
+ """Print table with apps list."""
31
+ # pylint: disable=invalid-name
32
+ _apps = response.json()["apps"]
33
+ if filters:
34
+ _apps = fmt.filter_list(_apps, filters)
35
+ table = fmt.Table()
36
+ table.header(
37
+ [
38
+ "ID",
39
+ "NAME",
40
+ "STATUS",
41
+ "TYPE",
42
+ "IPV4",
43
+ ]
44
+ )
45
+ for app in _apps:
46
+ table.row(
47
+ [
48
+ app["id"],
49
+ app["name"],
50
+ app["status"],
51
+ app["type"],
52
+ app["ip"],
53
+ ]
54
+ )
55
+ table.print()
56
+
57
+
58
+ @apps.command("list", "ls")
59
+ def apps_list(
60
+ verbose: Optional[bool] = verbose_option,
61
+ config: Optional[Path] = config_option,
62
+ profile: Optional[str] = profile_option,
63
+ output_format: Optional[str] = output_format_option,
64
+ filters: Optional[str] = filter_option,
65
+ ):
66
+ """List apps."""
67
+ client = create_client(config, profile)
68
+ response = client.get_apps()
69
+ fmt.printer(
70
+ response,
71
+ output_format=output_format,
72
+ filters=filters,
73
+ func=print_apps,
74
+ )
75
+
76
+
77
+ @apps.command("create")
78
+ def app_create(
79
+ yml_config_path: str,
80
+ verbose: Optional[bool] = verbose_option,
81
+ config: Optional[Path] = config_option,
82
+ profile: Optional[str] = profile_option,
83
+ output_format: Optional[str] = output_format_option,
84
+ status: Optional[bool] = typer.Option(
85
+ False,
86
+ "--status",
87
+ help="Display status and exit with 0 if status is 'started'.",
88
+ ),
89
+ ):
90
+ """Create app"""
91
+ client = create_client(config, profile)
92
+ response = client.create_app(yml_config_path)
93
+ fmt.printer(
94
+ response,
95
+ output_format=output_format,
96
+ func=lambda response: print(response.json()["app"]["id"]),
97
+ )
98
+
99
+
100
+ def print_vcs_providers(response: Response):
101
+ """Print table with vcs list."""
102
+ # pylint: disable=invalid-name
103
+ providers = response.json()["providers"]
104
+ table = fmt.Table()
105
+ table.header(
106
+ [
107
+ "LOGIN",
108
+ "PROVIDER",
109
+ "PROVIDER_ID",
110
+ ]
111
+ )
112
+ for provider in providers:
113
+ table.row(
114
+ [
115
+ provider["login"],
116
+ provider["provider"],
117
+ provider["provider_id"],
118
+ ]
119
+ )
120
+ table.print()
121
+
122
+
123
+ @apps.command("get-vcs-providers")
124
+ def app_get_vcs_providers(
125
+ verbose: Optional[bool] = verbose_option,
126
+ config: Optional[Path] = config_option,
127
+ profile: Optional[str] = profile_option,
128
+ output_format: Optional[str] = output_format_option,
129
+ ):
130
+ """Get VCS providers."""
131
+ client = create_client(config, profile)
132
+ response = client.get_vcs_providers()
133
+ fmt.printer(
134
+ response, output_format=output_format, func=print_vcs_providers
135
+ )
136
+
137
+
138
+ def print_vcs_repositories(response: Response):
139
+ """Print table with vcs list."""
140
+ # pylint: disable=invalid-name
141
+ providers = response.json()["repositories"]
142
+ table = fmt.Table()
143
+ table.header(["FULL NAME", "ID", "NAME", "URL", "IS PRIVATE"])
144
+ for provider in providers:
145
+ table.row(
146
+ [
147
+ provider["full_name"],
148
+ provider["id"],
149
+ provider["name"],
150
+ provider["url"],
151
+ provider["is_private"],
152
+ ]
153
+ )
154
+ table.print()
155
+
156
+
157
+ @apps.command("get-repositories")
158
+ def app_get_repositories(
159
+ vcs_provider_id: str,
160
+ verbose: Optional[bool] = verbose_option,
161
+ config: Optional[Path] = config_option,
162
+ profile: Optional[str] = profile_option,
163
+ output_format: Optional[str] = output_format_option,
164
+ ):
165
+ """Get repositories."""
166
+ client = create_client(config, profile)
167
+ response = client.get_repositories(vcs_provider_id)
168
+ fmt.printer(
169
+ response, output_format=output_format, func=print_vcs_repositories
170
+ )
171
+
172
+
173
+ def print_apps_tarifs(response: Response, typ: str):
174
+ """Print Timeweb Cloud Apps tarifs."""
175
+ # pylint: disable=invalid-name
176
+ if typ == "backend":
177
+ key = "backend_presets"
178
+ elif typ == "frontend":
179
+ key = "frontend_presets"
180
+ else:
181
+ raise KeyError("Tarifs can be only backend or frontend")
182
+
183
+ providers = response.json()[key]
184
+ table = fmt.Table()
185
+ if key == "backend_presets":
186
+ table.header(
187
+ [
188
+ "CPU",
189
+ "CPU FREQUENCY",
190
+ "DESCRIPTION",
191
+ "DESCRIPTION SHORT",
192
+ "DISK",
193
+ "ID",
194
+ "LOCATION",
195
+ "PRICE",
196
+ "RAM",
197
+ ]
198
+ )
199
+ for provider in providers:
200
+ table.row(
201
+ [
202
+ provider["cpu"],
203
+ provider["cpu_frequency"],
204
+ provider["description"],
205
+ provider["description_short"],
206
+ provider["disk"],
207
+ provider["id"],
208
+ provider["location"],
209
+ provider["price"],
210
+ provider["ram"],
211
+ ]
212
+ )
213
+ elif key == "frontend_presets":
214
+ table.header(
215
+ [
216
+ "DESCRIPTION",
217
+ "DESCRIPTION_SHORT",
218
+ "DISK",
219
+ "ID",
220
+ "LOCATION",
221
+ "PRICE",
222
+ "REQUESTS",
223
+ ]
224
+ )
225
+ for provider in providers:
226
+ table.row(
227
+ [
228
+ provider["description"],
229
+ provider["description_short"],
230
+ provider["disk"],
231
+ provider["id"],
232
+ provider["location"],
233
+ provider["price"],
234
+ provider["requests"],
235
+ ]
236
+ )
237
+ table.print()
238
+
239
+
240
+ @apps.command("list-presets")
241
+ def get_apps_tarifs(
242
+ _type: str = typer.Argument(..., metavar="TYPE"),
243
+ verbose: Optional[bool] = verbose_option,
244
+ config: Optional[Path] = config_option,
245
+ profile: Optional[str] = profile_option,
246
+ output_format: Optional[str] = output_format_option,
247
+ ):
248
+ """Get tarifs; backend or frontend"""
249
+ client = create_client(config, profile)
250
+ response = client.get_apps_tarifs()
251
+ fmt.printer(
252
+ response,
253
+ output_format=output_format,
254
+ typ=_type,
255
+ func=print_apps_tarifs,
256
+ )
257
+
258
+
259
+ def print_app_delete_response(response: Response, app_id: int):
260
+ """Print table with apps list."""
261
+ # pylint: disable=invalid-name
262
+ table = fmt.Table()
263
+ table.header(
264
+ [
265
+ "ID",
266
+ ]
267
+ )
268
+ table.row(
269
+ [
270
+ app_id,
271
+ ]
272
+ )
273
+ table.print()
274
+
275
+
276
+ @apps.command("delete")
277
+ def delete_app(
278
+ app_id: int,
279
+ verbose: Optional[bool] = verbose_option,
280
+ config: Optional[Path] = config_option,
281
+ profile: Optional[str] = profile_option,
282
+ output_format: Optional[str] = output_format_option,
283
+ ):
284
+ """Delete apps."""
285
+ client = create_client(config, profile)
286
+ response = client.delete_app(app_id)
287
+ fmt.printer(
288
+ response,
289
+ output_format=output_format,
290
+ app_id=app_id,
291
+ func=print_app_delete_response,
292
+ )
293
+
294
+
295
+ def get_app(response: Response):
296
+ """Print table with apps list."""
297
+ # pylint: disable=invalid-name
298
+ app = response.json()["app"]
299
+ table = fmt.Table()
300
+ table.header(
301
+ [
302
+ "ID",
303
+ "LOCATION",
304
+ "STATUS",
305
+ "TYPE",
306
+ "IPV4",
307
+ ]
308
+ )
309
+ table.row(
310
+ [
311
+ app["id"],
312
+ app["location"],
313
+ app["status"],
314
+ app["type"],
315
+ app["ip"],
316
+ ]
317
+ )
318
+ table.print()
319
+
320
+
321
+ @apps.command("get")
322
+ def app_get(
323
+ app_id: int,
324
+ verbose: Optional[bool] = verbose_option,
325
+ config: Optional[Path] = config_option,
326
+ profile: Optional[str] = profile_option,
327
+ output_format: Optional[str] = output_format_option,
328
+ status: Optional[bool] = typer.Option(
329
+ False,
330
+ "--status",
331
+ help="Display status and exit with 0 if status is 'started'.",
332
+ ),
333
+ ):
334
+ """Get database info."""
335
+ client = create_client(config, profile)
336
+ response = client.get_app(app_id)
337
+ fmt.printer(response, output_format=output_format, func=get_app)
@@ -315,7 +315,7 @@ def database_create(
315
315
  "%", help="User host for MySQL, Postgres"
316
316
  ),
317
317
  user_privileges: Optional[str] = typer.Option(
318
- [],
318
+ None,
319
319
  help="Comma-separated list of user privileges.",
320
320
  callback=dbms_parameters_callback,
321
321
  ),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes