twc-cli 2.6.0__py3-none-any.whl → 2.7.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.

Potentially problematic release.


This version of twc-cli might be problematic. Click here for more details.

CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  В этом файле описаны все значимые изменения в Timeweb Cloud CLI. В выпусках мы придерживается правил [семантического версионирования](https://semver.org/lang/ru/).
4
4
 
5
+ # Версия 2.7.0 (2024.11.02)
6
+
7
+ ## Добавлено
8
+
9
+ - Добавлена новая команда `twc whoami`, которая показывает логин аккаунта, в котором в текущий момент авторизован CLI.
10
+
11
+ ## Изменено
12
+
13
+ - Команда `twc account status` теперь также отображает логин аккаунта.
14
+
15
+ ## Исправлено
16
+
17
+ - Устранено падение программы при листинге образов (`twc image list`) и при создании сервера из образа.
18
+ - Исправлена ошибка при создании сервера с приватной сетью в зоне доступности SPB-3.
19
+ - Исправлены ошибки в командах `twc storage subdomain gencert` и `twc storage subdomain remove`, возникшие из-за нарушения обратной совместимости эндпоинтов API.
20
+ - Исправлена ошибка валидации приватного адреса при созаднии сервера — ошибочно запрещалось использовать 4-й по порядку адрес в подсети.
21
+ - Исправлена ошибка разбора JSON при выводе списка ресурсов в проекте.
22
+ - Исправлены ошибки добавления TXT записи для домена в команде `twc domain record add`.
23
+ - Исправлена ошибка парсинка субдомена в команде `twc domain record add`.
24
+ - Исправлены ошибки обновления DNS-записей на поддоменах.
25
+
5
26
  # Версия 2.6.0 (2024.08.14)
6
27
 
7
28
  ## Добавлено
twc/__main__.py CHANGED
@@ -21,6 +21,7 @@ from .commands import (
21
21
  vpc,
22
22
  firewall,
23
23
  floating_ip,
24
+ whoami,
24
25
  )
25
26
  from .commands.common import version_callback, version_option, verbose_option
26
27
 
@@ -46,6 +47,7 @@ cli.add_typer(domain, name="domain", aliases=["domains", "d"])
46
47
  cli.add_typer(vpc, name="vpc", aliases=["vpcs", "network", "networks"])
47
48
  cli.add_typer(firewall, name="firewall", aliases=["fw"])
48
49
  cli.add_typer(floating_ip, name="ip", aliases=["ips"])
50
+ cli.add_typer(whoami, name="whoami", aliases=[])
49
51
 
50
52
 
51
53
  @cli.command("version")
twc/__version__.py CHANGED
@@ -12,5 +12,5 @@
12
12
  import sys
13
13
 
14
14
 
15
- __version__ = "2.6.0"
15
+ __version__ = "2.7.0"
16
16
  __pyversion__ = sys.version.replace("\n", "")
twc/api/client.py CHANGED
@@ -469,14 +469,11 @@ class TimewebCloud(TimewebCloudBase):
469
469
  # -----------------------------------------------------------------------
470
470
  # Images
471
471
 
472
- def get_images(
473
- self, limit: int = 100, offset: int = 0, with_deleted: bool = False
474
- ):
472
+ def get_images(self, limit: int = 100, offset: int = 0):
475
473
  """Get list of images."""
476
474
  params = {
477
475
  "limit": limit,
478
476
  "offset": offset,
479
- "with_deleted": with_deleted,
480
477
  }
481
478
  return self._request("GET", f"{self.api_url}/images", params=params)
482
479
 
@@ -1416,6 +1413,8 @@ class TimewebCloud(TimewebCloudBase):
1416
1413
  value: str,
1417
1414
  subdomain: Optional[str] = None,
1418
1415
  priority: Optional[int] = None,
1416
+ *,
1417
+ null_subdomain: bool = False,
1419
1418
  ):
1420
1419
  """Add DNS record to domain."""
1421
1420
  payload = {
@@ -1424,6 +1423,8 @@ class TimewebCloud(TimewebCloudBase):
1424
1423
  **({"subdomain": subdomain} if subdomain else {}),
1425
1424
  **({"priority": priority} if priority else {}),
1426
1425
  }
1426
+ if null_subdomain:
1427
+ payload["subdomain"] = None
1427
1428
  return self._request(
1428
1429
  "POST",
1429
1430
  f"{self.api_url}/domains/{fqdn}/dns-records",
twc/apiwrap.py CHANGED
@@ -4,7 +4,7 @@ import os
4
4
  import sys
5
5
  import textwrap
6
6
  from pathlib import Path
7
- from logging import debug
7
+ from logging import debug, warning
8
8
 
9
9
  from .api import TimewebCloud
10
10
  from .api import exceptions as exc
@@ -70,6 +70,11 @@ def create_client(config: Path, profile: str, **kwargs) -> TimewebCloud:
70
70
  """
71
71
  token = os.getenv("TWC_TOKEN")
72
72
  log_settings = os.getenv("TWC_LOG")
73
+ api_endpoint = os.getenv("TWC_ENDPOINT")
74
+
75
+ if api_endpoint:
76
+ warning("Using API URL from environment: %s", api_endpoint)
77
+ kwargs["api_base_url"] = api_endpoint
73
78
 
74
79
  if log_settings:
75
80
  for param in log_settings.split(","):
twc/commands/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Commands."""
2
2
 
3
- from .account import account
3
+ from .account import account, whoami
4
4
  from .config import config
5
5
  from .server import server
6
6
  from .ssh_key import ssh_key
twc/commands/account.py CHANGED
@@ -18,6 +18,35 @@ from .common import (
18
18
  )
19
19
 
20
20
 
21
+ whoami = TyperAlias(help="Display current login.", no_args_is_help=False)
22
+
23
+
24
+ # ------------------------------------------------------------- #
25
+ # $ twc whoami #
26
+ # ------------------------------------------------------------- #
27
+
28
+
29
+ @whoami.callback(invoke_without_command=True)
30
+ def whoami_callback(
31
+ verbose: Optional[bool] = verbose_option,
32
+ config: Optional[Path] = config_option,
33
+ profile: Optional[str] = profile_option,
34
+ output_format: Optional[str] = output_format_option,
35
+ ):
36
+ """Display current login."""
37
+ client = create_client(config, profile)
38
+ response = client.get_account_status()
39
+ login = response.json()["status"]["login"]
40
+ fmt.printer(
41
+ {"login": login, "profile": profile},
42
+ output_format=output_format,
43
+ func=lambda data: print(data["login"]),
44
+ )
45
+
46
+
47
+ # ------------------------------------------------------------- #
48
+
49
+
21
50
  account = TyperAlias(help=__doc__)
22
51
  account_access = TyperAlias(help="Manage account access restrictions.")
23
52
  account.add_typer(account_access, name="access")
@@ -33,6 +62,7 @@ def print_account_status(response: Response):
33
62
  status = response.json()["status"]
34
63
  output = dedent(
35
64
  f"""
65
+ Login: {status["login"]}
36
66
  Provider: {status["company_info"]["name"]}
37
67
  Yandex.Metrika ID: {status["ym_client_id"]}
38
68
  Blocked: {status["is_blocked"]}
twc/commands/domain.py CHANGED
@@ -195,7 +195,7 @@ def domain_delete(
195
195
  # ------------------------------------------------------------- #
196
196
 
197
197
 
198
- @domain.command("add")
198
+ @domain.command("add", "create")
199
199
  def domain_add(
200
200
  domain_name: str,
201
201
  verbose: Optional[bool] = verbose_option,
@@ -316,7 +316,7 @@ def domain_remove_dns_record(
316
316
  # ------------------------------------------------------------- #
317
317
 
318
318
 
319
- @domain_record.command("add")
319
+ @domain_record.command("add", "create")
320
320
  def domain_add_dns_record(
321
321
  domain_name: str,
322
322
  verbose: Optional[bool] = verbose_option,
@@ -346,29 +346,39 @@ def domain_add_dns_record(
346
346
  """Add dns record for domain or subdomain."""
347
347
  client = create_client(config, profile)
348
348
 
349
+ null_subdomain = False
350
+
349
351
  if second_ld:
350
352
  offset = 3
351
353
  else:
352
354
  offset = 2
353
355
 
354
356
  subdomain = domain_name
357
+ original_domain_name = domain_name
355
358
  domain_name = ".".join(domain_name.split(".")[-offset:])
356
359
 
357
360
  if subdomain == domain_name:
358
361
  subdomain = None
359
362
 
360
- # API issue: see text below
361
- # API can add TXT record (only TXT, why?) with non-existent subdomain,
362
- # but 'subdomain' option must not be passed as FQDN!
363
- # API issue: You cannot create subdomains with underscore. Why?
364
- # Use previous described bug for this! Pass your subdomain with
365
- # underscores to this function.
366
363
  if record_type.lower() == "txt":
367
- # 'ftp.example.org' --> 'ftp'
368
- subdomain = ".".join(subdomain.split(".")[:-offset])
364
+ if subdomain is None:
365
+ null_subdomain = True
366
+ else:
367
+ # 'ftp.example.org' --> 'ftp'
368
+ subdomain = ".".join(subdomain.split(".")[:-offset])
369
+ else:
370
+ subdomain = ".".join(original_domain_name.split(".")[:-offset])
371
+ if subdomain != "":
372
+ domain_name = original_domain_name
373
+ subdomain = None
369
374
 
370
375
  response = client.add_domain_dns_record(
371
- domain_name, record_type, value, subdomain, priority
376
+ domain_name,
377
+ record_type,
378
+ value,
379
+ subdomain,
380
+ priority,
381
+ null_subdomain=null_subdomain,
372
382
  )
373
383
  fmt.printer(
374
384
  response,
@@ -424,6 +434,12 @@ def domain_update_dns_records(
424
434
  if subdomain == domain_name:
425
435
  subdomain = None
426
436
 
437
+ if record_type.lower() == "txt" and subdomain is not None:
438
+ subdomain = ".".join(subdomain.split(".")[:-offset])
439
+ elif subdomain is not None:
440
+ domain_name = subdomain
441
+ subdomain = None
442
+
427
443
  response = client.update_domain_dns_record(
428
444
  domain_name, record_id, record_type, value, subdomain, priority
429
445
  )
@@ -439,7 +455,7 @@ def domain_update_dns_records(
439
455
  # ------------------------------------------------------------- #
440
456
 
441
457
 
442
- @domain_subdomain.command("add")
458
+ @domain_subdomain.command("add", "create")
443
459
  def domain_add_subdomain(
444
460
  subdomain: str = typer.Argument(..., metavar="FQDN"),
445
461
  verbose: Optional[bool] = verbose_option,
twc/commands/project.py CHANGED
@@ -188,17 +188,11 @@ def print_resources(response: Response):
188
188
  for key in resource_keys:
189
189
  if resources[key]:
190
190
  for resource in resources[key]:
191
- try:
192
- location = resource["location"]
193
- except KeyError:
194
- # Balancers, clusters, and databases has no 'location' field.
195
- # These services is available only in 'ru-1' location.
196
- location = "ru-1"
197
191
  table.row(
198
192
  [
199
193
  resource["id"],
200
- resource["name"],
201
- location,
194
+ resource.get("name", resource.get("fqdn")),
195
+ resource.get("location", "ru-1"),
202
196
  key[:-1], # this is resource name e.g. 'server'
203
197
  ]
204
198
  )
twc/commands/server.py CHANGED
@@ -584,9 +584,7 @@ def server_create(
584
584
  net = IPv4Network(
585
585
  client.get_vpc(network).json()["vpc"]["subnet_v4"]
586
586
  )
587
- if IPv4Address(private_ip) > IPv4Address(
588
- int(net.network_address) + 4
589
- ):
587
+ if IPv4Address(private_ip) >= net.network_address + 4:
590
588
  payload["network"]["ip"] = private_ip
591
589
  else:
592
590
  # First 3 addresses is reserved for networks OVN based networks
twc/commands/storage.py CHANGED
@@ -500,7 +500,6 @@ def storage_subdomain_remove(
500
500
  verbose: Optional[bool] = verbose_option,
501
501
  config: Optional[Path] = config_option,
502
502
  profile: Optional[str] = profile_option,
503
- output_format: Optional[str] = output_format_option,
504
503
  yes: Optional[bool] = yes_option,
505
504
  ):
506
505
  """Remove subdomains."""
@@ -509,11 +508,11 @@ def storage_subdomain_remove(
509
508
  client = create_client(config, profile)
510
509
  bucket_id = resolve_bucket_id(client, bucket)
511
510
  response = client.delete_bucket_subdomains(bucket_id, subdomains)
512
- fmt.printer(
513
- response,
514
- output_format=output_format,
515
- func=print_subdomains_state,
516
- )
511
+ if response.status_code == 204:
512
+ for subdomain in subdomains:
513
+ print(subdomain)
514
+ else:
515
+ sys.exit(fmt.printer(response))
517
516
 
518
517
 
519
518
  # ------------------------------------------------------------- #
@@ -532,7 +531,7 @@ def storage_subdomain_gencert(
532
531
  client = create_client(config, profile)
533
532
  for subdomain in subdomains:
534
533
  response = client.gen_cert_for_bucket_subdomain(subdomain)
535
- if response.status_code == 201:
534
+ if response.status_code == 204:
536
535
  print(subdomain)
537
536
  else:
538
537
  sys.exit(fmt.printer(response))
twc/fmt.py CHANGED
@@ -67,6 +67,9 @@ class Printer:
67
67
 
68
68
  def raw(self):
69
69
  """Print raw API response text (mostly raw JSON)."""
70
+ if isinstance(self._data, dict):
71
+ typer.echo(json.dumps(self._data))
72
+ return
70
73
  typer.echo(self._data.text)
71
74
 
72
75
  def colorize(self, data: str, lexer: object = JsonLexer()):
@@ -77,9 +80,12 @@ class Printer:
77
80
  """Print colorized JSON output. Fallback to non-color output if
78
81
  Pygments not installed and fallback to raw output on JSONDecodeError.
79
82
  """
83
+ data = self._data
84
+ if not isinstance(self._data, dict):
85
+ data = self._data.json()
80
86
  try:
81
87
  json_data = json.dumps(
82
- self._data.json(), indent=4, sort_keys=True, ensure_ascii=False
88
+ data, indent=4, sort_keys=True, ensure_ascii=False
83
89
  )
84
90
  self.colorize(json_data, lexer=JsonLexer())
85
91
  except json.JSONDecodeError:
@@ -89,10 +95,11 @@ class Printer:
89
95
  """Print colorized YAML output. Fallback to non-color output if
90
96
  Pygments not installed and fallback to raw output on YAMLError.
91
97
  """
98
+ data = self._data
99
+ if not isinstance(self._data, dict):
100
+ data = self._data.json()
92
101
  try:
93
- yaml_data = yaml.dump(
94
- self._data.json(), sort_keys=True, allow_unicode=True
95
- )
102
+ yaml_data = yaml.dump(data, sort_keys=True, allow_unicode=True)
96
103
  self.colorize(yaml_data, lexer=YamlLexer())
97
104
  except yaml.YAMLError:
98
105
  self.raw()
twc/vars.py CHANGED
@@ -11,4 +11,4 @@ CONTROL_PANEL_URL = "https://timeweb.cloud/my"
11
11
  REGIONS_WITH_IPV6 = ["ru-1", "pl-1"]
12
12
  REGIONS_WITH_IMAGES = ["ru-1", "ru-3", "kz-1", "pl-1", "nl-1"]
13
13
  REGIONS_WITH_LAN = ["ru-1", "ru-3", "nl-1", "pl-1"]
14
- ZONES_WITH_LAN = ["spb-1", "spb-4", "msk-1", "ams-1", "gdn-1"]
14
+ ZONES_WITH_LAN = ["spb-1", "spb-3", "spb-4", "msk-1", "ams-1", "gdn-1"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: twc-cli
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Timeweb Cloud Command Line Interface.
5
5
  Home-page: https://github.com/timeweb-cloud/twc
6
6
  License: MIT
@@ -1,36 +1,36 @@
1
- CHANGELOG.md,sha256=4VPeBGreHTzwFwgjCyNh1132Sx3glB2yBHBypJnl_fU,24717
1
+ CHANGELOG.md,sha256=AnIkFEP7S8yrjLrgp1Vbsp47c79n6RQxz6-6OEwwEmk,26436
2
2
  COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
3
3
  twc/__init__.py,sha256=NwPAMNw3NuHdWGQvWS9_lromVF6VM194oVOipojfJns,113
4
- twc/__main__.py,sha256=YBqi1OMSDaZnu2q8OyNa6N-dkVI90n6DZrJbUvYbHJA,2407
5
- twc/__version__.py,sha256=Pqtq12hKiQFbdtW9mzEN58Z8FqFqqIp-iWJljwSSYUU,442
4
+ twc/__main__.py,sha256=ADHceaQUzgLmvhYHvb5O8urdJWj5IcEHLpTQkSExiD8,2468
5
+ twc/__version__.py,sha256=cJqV4mvQsKCOeicZYGqEH-nKLLWLVP_E-ByegiDQ9js,442
6
6
  twc/api/__init__.py,sha256=SXew0Fe51M7nRBNQaaLRH4NjnRHkQUn7J26OCkQsftA,128
7
7
  twc/api/base.py,sha256=QRefnIgmlbz8n37GLBKeAK1AtzkcNo1IFjZgHDDECJ4,7912
8
- twc/api/client.py,sha256=LSe8pH684fh1u2jxj-IY8O40NvFsqwVctHrshfctRIE,58482
8
+ twc/api/client.py,sha256=8y3Q8zoZeNoKm8ywOXXbSXvMIvCXyAsU43HLy_nb4hQ,58514
9
9
  twc/api/exceptions.py,sha256=UzK3pKRffcXlhnkPy6MDjP_DygVoV17DuZ_mdNbOzts,2369
10
10
  twc/api/types.py,sha256=HCxdTi-o8nVq4ShPthd2fUvlYufEoXafx_6qrNHFH04,5406
11
- twc/apiwrap.py,sha256=0SmFZUH013jKHlpAQmsIySMtFuNtGvkMLQv0zTVJijg,2780
12
- twc/commands/__init__.py,sha256=hvTpQByt7wfl9hVo7xRPCdwAQpctoFiBVdaMLHDqKbs,422
13
- twc/commands/account.py,sha256=6T7J3McTXJKzT7Gi_AgRcKpWdeXcmBTcpwFF0GjzADo,4998
11
+ twc/apiwrap.py,sha256=hKrg_o6rLfY32SEnWMc1BSXHnSAh7TGar1JQ90YnG5M,2970
12
+ twc/commands/__init__.py,sha256=a-6fHQQwOj--Z7uBZGZL3z1rvJiOGUMQMRET1UknIYo,430
13
+ twc/commands/account.py,sha256=6q9ri02oFbUUZuqNVXO-uHOX45B4ELJlPjyfVaEL5Qw,5960
14
14
  twc/commands/balancer.py,sha256=QAouc74ZT5go11gB1vjjfYtd1luTmWrfpACPwokZ5sU,20278
15
15
  twc/commands/common.py,sha256=Wph8cVogUNNvc456SQrASb7mv7G88I8ETwHgISVjLQQ,8282
16
16
  twc/commands/config.py,sha256=hoRtxn2VRxIsuy9vgO6yd0Cu15Rbl-uYMZeU0Ix7dG0,8797
17
17
  twc/commands/database.py,sha256=2NZ-TyRBkFgfYJyUdZUcfdqSaX7QVdWDU4k_yQNtUvo,16052
18
- twc/commands/domain.py,sha256=SHOERsEvmlkdKQV5kaiEgMOLZme5ykW8LCUd1IF1Zzo,15624
18
+ twc/commands/domain.py,sha256=uKr2rP9ajLiZrxA_Xl47Jn7Fh9IRODCtjwXwTfbo-EU,15943
19
19
  twc/commands/firewall.py,sha256=KNolqbi2rsppOZwbs_j3yoZQt-0wKbj1JPGiZdfGxDE,27439
20
20
  twc/commands/floating_ip.py,sha256=G9nD5BbHCZcuytbzeneDJWQDhd8c8WRtq9pAfwI9m7E,8747
21
21
  twc/commands/image.py,sha256=OviQwegXK55H3TBlroCASVcgj2QUVCTo0ZhF5ug9eT8,8165
22
22
  twc/commands/kubernetes.py,sha256=-Cgas1vFVMcrWGinjstuUz3sqX0ZNXv_4mwPwuwKeLE,20870
23
- twc/commands/project.py,sha256=0z0MmkW8CVFb8emftE5OshoAwdLBZq_otQjNpeuuklU,10750
24
- twc/commands/server.py,sha256=nySxygbA1ZwLCE53JjhabLAI7uZjnFE_RdJ_8JzZvOU,70341
23
+ twc/commands/project.py,sha256=xnL3kLIumKzrI9EZ6r6m-PGOl3mZ9IhLQua7WZ3Rghg,10499
24
+ twc/commands/server.py,sha256=Cw8VxOcWEVPtcKZ53h9erhV3VOj7io9E8xQwVN0S53Y,70294
25
25
  twc/commands/ssh_key.py,sha256=NHgTPhAQpDzt-iPHHVo4XqUJvujNqf019N6N9qYZ9Us,7941
26
- twc/commands/storage.py,sha256=qt7_4rvnM2gnN4a0K4KEDoNZ_-2buvMNnCSJIXxQszs,19424
26
+ twc/commands/storage.py,sha256=Pztk5iUBp9RtkdOwsfHaZFCnD8GuH6zOPtluawkRmiI,19404
27
27
  twc/commands/vpc.py,sha256=SAht6UD17mU0d_AZY6W34VEYs7CqUsS2iDakPFxAFQU,8876
28
- twc/fmt.py,sha256=f8submLhHnm8OXBl_4Oe4tuhhMKvgTK1PP1y_25TmyU,6904
28
+ twc/fmt.py,sha256=nbuYZ8nVabYDwCmZqnL3-c6Tmri4B-R_sTCkG6sdfeI,7171
29
29
  twc/typerx.py,sha256=AZ6BgTQvlrZYfKVYd9YqRNQnAR2XuyqImz4rf6di6f4,6737
30
30
  twc/utils.py,sha256=uWizyUC4dHLwtk50q4Sub3zOvnVESfHKBbXYwk5t71w,651
31
- twc/vars.py,sha256=n6RzpTJAu44pp4f8KgXcLnorsQBhIcVIMs6DA8-4MCM,534
32
- twc_cli-2.6.0.dist-info/COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
33
- twc_cli-2.6.0.dist-info/METADATA,sha256=gkmWcgAscC5aSj83A1_RgHYfH9MW_5sd_0_7vK-cxkw,2601
34
- twc_cli-2.6.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
35
- twc_cli-2.6.0.dist-info/entry_points.txt,sha256=tmTaVRhm8BkNrXC_9XJMum7O9wFVOvkXcBetxmahWvE,40
36
- twc_cli-2.6.0.dist-info/RECORD,,
31
+ twc/vars.py,sha256=fva3O2leMGtExb1aWiAk6sOV0O8et9_kEyRpYYIZ7CM,543
32
+ twc_cli-2.7.0.dist-info/COPYING,sha256=fpJLxZS_kHr_659xkpmqEtqHeZp6lQR9CmKCwnYbsmE,1089
33
+ twc_cli-2.7.0.dist-info/METADATA,sha256=w4WaXHt_EXUtrWRlRhntcYi0p5poZ7PM-ZfJGK4BZTY,2601
34
+ twc_cli-2.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
35
+ twc_cli-2.7.0.dist-info/entry_points.txt,sha256=tmTaVRhm8BkNrXC_9XJMum7O9wFVOvkXcBetxmahWvE,40
36
+ twc_cli-2.7.0.dist-info/RECORD,,