dnctl 0.1.2__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.
dnctl.py ADDED
@@ -0,0 +1,3297 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = [
5
+ # "typer>=0.12",
6
+ # "httpx>=0.27",
7
+ # "rich>=13",
8
+ # "cryptography>=42",
9
+ # ]
10
+ # ///
11
+ """dnctl - manage DevNomads services from the command line.
12
+
13
+ Single-file CLI for the DevNomads public API (https://api.devnomads.nl).
14
+ Create an API key in the control panel, run `dnctl configure`, and go.
15
+ See PLAN.md in the repository for the design.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import configparser
22
+ import hashlib
23
+ import hmac
24
+ import io
25
+ import json
26
+ import os
27
+ import secrets
28
+ import stat
29
+ import sys
30
+ import time
31
+ import urllib.parse
32
+ from contextlib import nullcontext
33
+ from dataclasses import dataclass
34
+ from enum import Enum
35
+ from pathlib import Path
36
+ from typing import Annotated, Any, ContextManager, Iterable, Protocol
37
+
38
+ import httpx
39
+ import typer
40
+ from rich.console import Console
41
+ from rich.table import Table
42
+ from typer.core import TyperGroup
43
+
44
+ __version__ = "0.1.2"
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Constants
48
+
49
+ DEFAULT_API_URL = "https://api.devnomads.nl"
50
+ DEFAULT_PROFILE = "default"
51
+ ENV_API_KEY = "DN_API_KEY"
52
+ ENV_PROFILE = "DN_PROFILE"
53
+ ENV_CONFIG_DIR = "DN_CONFIG_DIR"
54
+ PROVIDERS = ("auroradns", "transip")
55
+ REQUEST_TIMEOUT = 30.0
56
+ MAX_ATTEMPTS = 3
57
+ MAX_RETRY_DELAY = 30.0
58
+ RECORD_COLUMNS = ["name", "type", "ttl", "content", "disabled"]
59
+
60
+ # Data on stdout, everything meant for humans on stderr.
61
+ out_console = Console()
62
+ err_console = Console(stderr=True)
63
+
64
+
65
+ class CliError(typer.Exit):
66
+ """Fatal user-facing error: one line on stderr, exit code 1."""
67
+
68
+ def __init__(self, message: str) -> None:
69
+ # soft_wrap keeps the error a single grep-able line
70
+ err_console.print(f"[red]error:[/] {message}", soft_wrap=True)
71
+ self.message = message
72
+ super().__init__(code=1)
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Config layer
77
+
78
+
79
+ def config_dir() -> Path:
80
+ if env_dir := os.environ.get(ENV_CONFIG_DIR):
81
+ return Path(env_dir)
82
+ xdg = os.environ.get("XDG_CONFIG_HOME")
83
+ base = Path(xdg) if xdg else Path.home() / ".config"
84
+ return base / "dnctl"
85
+
86
+
87
+ def credentials_path() -> Path:
88
+ return config_dir() / "credentials"
89
+
90
+
91
+ def load_credentials() -> configparser.ConfigParser:
92
+ parser = configparser.ConfigParser()
93
+ path = credentials_path()
94
+ if path.exists():
95
+ mode = stat.S_IMODE(path.stat().st_mode)
96
+ if mode & 0o077:
97
+ err_console.print(
98
+ f"[yellow]warning:[/] {path} is readable by others "
99
+ f"(mode {mode:03o}); run: chmod 600 {path}"
100
+ )
101
+ parser.read(path)
102
+ return parser
103
+
104
+
105
+ def _write_private(path: Path, content: str) -> None:
106
+ path.parent.mkdir(parents=True, exist_ok=True)
107
+ path.parent.chmod(0o700)
108
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
109
+ with os.fdopen(fd, "w") as fh:
110
+ fh.write(content)
111
+ path.chmod(0o600)
112
+
113
+
114
+ def save_credentials(parser: configparser.ConfigParser) -> None:
115
+ buffer = io.StringIO()
116
+ parser.write(buffer)
117
+ _write_private(credentials_path(), buffer.getvalue())
118
+
119
+
120
+ def devnomads_profiles(parser: configparser.ConfigParser) -> list[str]:
121
+ # Plain sections are DevNomads accounts; "provider:name" sections
122
+ # belong to transfer source drivers.
123
+ return [section for section in parser.sections() if ":" not in section]
124
+
125
+
126
+ def resolve_credentials(state: AppState) -> tuple[str, str]:
127
+ """Resolve (api_key, api_url): flag > env > profile > [default]."""
128
+
129
+ parser = load_credentials()
130
+ profile = state.profile or os.environ.get(ENV_PROFILE) or DEFAULT_PROFILE
131
+ explicit = bool(state.profile or os.environ.get(ENV_PROFILE))
132
+ section = parser[profile] if parser.has_section(profile) else None
133
+ api_url = section.get("api_url", DEFAULT_API_URL) if section else DEFAULT_API_URL
134
+ api_key = (
135
+ state.api_key
136
+ or os.environ.get(ENV_API_KEY)
137
+ or (section.get("api_key") if section else None)
138
+ )
139
+ if not api_key:
140
+ if explicit and section is None:
141
+ available = ", ".join(devnomads_profiles(parser)) or "none"
142
+ raise CliError(
143
+ f"profile '{profile}' not found in {credentials_path()} "
144
+ f"(available: {available})"
145
+ )
146
+ raise CliError(f"no API key found; run `dnctl configure` or set {ENV_API_KEY}")
147
+ return api_key, api_url
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Output
152
+
153
+
154
+ class OutputFormat(str, Enum):
155
+ table = "table"
156
+ json = "json"
157
+
158
+
159
+ @dataclass
160
+ class AppState:
161
+ profile: str | None = None
162
+ api_key: str | None = None
163
+ output: OutputFormat | None = None
164
+ debug: bool = False
165
+ client: DevNomadsClient | None = None
166
+
167
+
168
+ def state_from(ctx: typer.Context, output: OutputFormat | None = None) -> AppState:
169
+ state = ctx.obj
170
+ if not isinstance(state, AppState): # direct invocation in tests
171
+ state = AppState()
172
+ ctx.obj = state
173
+ if output is not None: # per-command --output overrides the global one
174
+ state.output = output
175
+ return state
176
+
177
+
178
+ def resolve_format(state: AppState) -> OutputFormat:
179
+ if state.output:
180
+ return state.output
181
+ return OutputFormat.table if sys.stdout.isatty() else OutputFormat.json
182
+
183
+
184
+ def render(
185
+ state: AppState,
186
+ data: Any,
187
+ *,
188
+ columns: list[str] | None = None,
189
+ title: str | None = None,
190
+ ) -> None:
191
+ """Render data on stdout: JSON for machines, a rich table for humans."""
192
+
193
+ if resolve_format(state) is OutputFormat.json:
194
+ sys.stdout.write(json.dumps(data, indent=2, default=str) + "\n")
195
+ return
196
+ if isinstance(data, dict):
197
+ _render_kv(data, title)
198
+ elif isinstance(data, list):
199
+ _render_rows(data, columns, title)
200
+ else:
201
+ out_console.print(str(data), soft_wrap=True)
202
+
203
+
204
+ def _cell(value: Any) -> str:
205
+ if value is None:
206
+ return ""
207
+ if isinstance(value, bool):
208
+ return "yes" if value else "no"
209
+ if isinstance(value, list) and not any(
210
+ isinstance(item, (dict, list)) for item in value
211
+ ):
212
+ return ", ".join(str(item) for item in value)
213
+ if isinstance(value, (dict, list)):
214
+ return json.dumps(value)
215
+ return str(value)
216
+
217
+
218
+ def _flatten_row(row: dict[str, Any]) -> dict[str, Any]:
219
+ """Merge one level of nested objects into the row, so type-specific
220
+ details (e.g. the "proxy" object on proxy services) become real
221
+ columns. On a name collision the nested key gets a parent_ prefix."""
222
+
223
+ flat: dict[str, Any] = {}
224
+ nested: list[tuple[str, dict[str, Any]]] = []
225
+ for key, value in row.items():
226
+ if isinstance(value, dict):
227
+ nested.append((key, value))
228
+ else:
229
+ flat[key] = value
230
+ for parent, obj in nested:
231
+ for key, value in obj.items():
232
+ column = key if key not in flat else f"{parent}_{key}"
233
+ flat[column] = value
234
+ return flat
235
+
236
+
237
+ def _render_kv(data: dict[str, Any], title: str | None) -> None:
238
+ table = Table(title=title, show_header=False)
239
+ table.add_column(style="bold")
240
+ table.add_column()
241
+ for key, value in _flatten_row(data).items():
242
+ table.add_row(key, _cell(value))
243
+ out_console.print(table)
244
+
245
+
246
+ def _render_rows(
247
+ rows: list[dict[str, Any]], columns: list[str] | None, title: str | None
248
+ ) -> None:
249
+ if not rows:
250
+ err_console.print("[dim]no results[/]")
251
+ return
252
+ rows = [_flatten_row(row) if isinstance(row, dict) else row for row in rows]
253
+ cols = columns or list(rows[0].keys())
254
+ table = Table(title=title)
255
+ for col in cols:
256
+ table.add_column(col)
257
+ for row in rows:
258
+ table.add_row(*(_cell(row.get(col)) for col in cols))
259
+ out_console.print(table)
260
+
261
+
262
+ def sort_rows(rows: list[dict[str, Any]], sort: str | None) -> list[dict[str, Any]]:
263
+ """Sort rows by a field; a leading '-' reverses. None sorts last."""
264
+
265
+ if not sort or not rows:
266
+ return rows
267
+ reverse = sort.startswith("-")
268
+ field = sort.lstrip("-")
269
+ if not any(field in row for row in rows):
270
+ available = ", ".join(rows[0].keys())
271
+ raise CliError(f"unknown sort field '{field}' (available: {available})")
272
+ values = [row.get(field) for row in rows]
273
+ numeric = all(
274
+ isinstance(value, (int, float)) and not isinstance(value, bool)
275
+ for value in values
276
+ if value is not None
277
+ )
278
+
279
+ def key(row: dict[str, Any]) -> tuple[int, Any]:
280
+ value = row.get(field)
281
+ if value is None:
282
+ return (1, 0 if numeric else "")
283
+ return (0, value if numeric else str(value).lower())
284
+
285
+ return sorted(rows, key=key, reverse=reverse)
286
+
287
+
288
+ def _confirm(question: str, yes: bool) -> None:
289
+ if yes:
290
+ return
291
+ if not sys.stdin.isatty():
292
+ raise CliError("confirmation required; pass --yes to confirm non-interactively")
293
+ if not typer.confirm(question, err=True):
294
+ raise typer.Abort()
295
+
296
+
297
+ def _mask(secret: str) -> str:
298
+ if not secret:
299
+ return ""
300
+ if len(secret) <= 8:
301
+ return "****"
302
+ return f"{secret[:4]}...{secret[-4:]}"
303
+
304
+
305
+ def working(message: str) -> ContextManager[Any]:
306
+ """Spinner on stderr while a request is in flight. Inert when stderr
307
+ is not a terminal (pipelines, CI, redirects), so output stays clean.
308
+ Never nest two of these: rich allows one live display at a time."""
309
+
310
+ if err_console.is_terminal:
311
+ return err_console.status(message, spinner="dots")
312
+ return nullcontext()
313
+
314
+
315
+ # ---------------------------------------------------------------------------
316
+ # API client
317
+
318
+
319
+ class DevNomadsClient:
320
+ """Thin wrapper owning auth, errors and retries; commands never use
321
+ httpx directly."""
322
+
323
+ def __init__(self, api_url: str, api_key: str, *, debug: bool = False) -> None:
324
+ self.debug = debug
325
+ self._http = httpx.Client(
326
+ base_url=api_url,
327
+ headers={
328
+ "Authorization": f"Bearer {api_key}",
329
+ "Accept": "application/json",
330
+ "User-Agent": f"dnctl/{__version__}",
331
+ },
332
+ timeout=REQUEST_TIMEOUT,
333
+ )
334
+
335
+ def request(
336
+ self,
337
+ method: str,
338
+ path: str,
339
+ *,
340
+ params: dict[str, Any] | None = None,
341
+ json_body: Any = None,
342
+ ) -> Any:
343
+ with working(f"{method} {path}"):
344
+ response = self._send(method, path, params, json_body)
345
+ for attempt in range(1, MAX_ATTEMPTS):
346
+ if response.status_code != 429 and response.status_code < 500:
347
+ break
348
+ delay = _retry_delay(response, attempt)
349
+ err_console.print(
350
+ f"[yellow]got {response.status_code}, retrying in {delay:.0f}s[/]"
351
+ )
352
+ time.sleep(delay)
353
+ response = self._send(method, path, params, json_body)
354
+ if response.status_code >= 400:
355
+ raise CliError(_error_message(response))
356
+ if response.status_code == 204 or not response.content:
357
+ return None
358
+ try:
359
+ return _unwrap(response.json())
360
+ except ValueError as exc:
361
+ raise CliError(f"API returned invalid JSON: {exc}") from exc
362
+
363
+ def _send(
364
+ self,
365
+ method: str,
366
+ path: str,
367
+ params: dict[str, Any] | None,
368
+ json_body: Any,
369
+ ) -> httpx.Response:
370
+ if self.debug:
371
+ suffix = f" {json.dumps(json_body)}" if json_body is not None else ""
372
+ err_console.print(f"[dim]> {method} {path}{suffix}[/]")
373
+ try:
374
+ response = self._http.request(method, path, params=params, json=json_body)
375
+ except httpx.HTTPError as exc:
376
+ raise CliError(f"request failed: {exc}") from exc
377
+ if self.debug:
378
+ err_console.print(f"[dim]< {response.status_code} {response.text[:500]}[/]")
379
+ return response
380
+
381
+
382
+ def _unwrap(body: Any) -> Any:
383
+ """Strip the Laravel API resource envelope ({"data": ...}, optionally
384
+ with links/meta) so commands and json output see the actual data."""
385
+
386
+ if (
387
+ isinstance(body, dict)
388
+ and "data" in body
389
+ and set(body)
390
+ <= {
391
+ "data",
392
+ "links",
393
+ "meta",
394
+ }
395
+ ):
396
+ return body["data"]
397
+ return body
398
+
399
+
400
+ def _retry_delay(response: httpx.Response, attempt: int) -> float:
401
+ try:
402
+ delay = float(response.headers.get("Retry-After", ""))
403
+ except ValueError:
404
+ delay = float(2 ** (attempt - 1))
405
+ return min(delay, MAX_RETRY_DELAY)
406
+
407
+
408
+ def _error_message(response: httpx.Response) -> str:
409
+ try:
410
+ body = response.json()
411
+ except ValueError:
412
+ body = None
413
+ detail = ""
414
+ if isinstance(body, dict):
415
+ detail = str(body.get("message") or body.get("error") or "")
416
+ errors = body.get("errors")
417
+ if isinstance(errors, dict):
418
+ flat = "; ".join(
419
+ (
420
+ f"{field}: {' '.join(map(str, messages))}"
421
+ if isinstance(messages, list)
422
+ else f"{field}: {messages}"
423
+ )
424
+ for field, messages in errors.items()
425
+ )
426
+ detail = f"{detail} ({flat})" if detail else flat
427
+ message = f"API error {response.status_code}"
428
+ if detail:
429
+ message += f": {detail}"
430
+ if response.status_code == 401:
431
+ message += " - check your API key (run `dnctl configure`)"
432
+ return message
433
+
434
+
435
+ def get_client(state: AppState) -> DevNomadsClient:
436
+ if state.client is None:
437
+ api_key, api_url = resolve_credentials(state)
438
+ state.client = DevNomadsClient(api_url, api_key, debug=state.debug)
439
+ return state.client
440
+
441
+
442
+ # ---------------------------------------------------------------------------
443
+ # DNS record helpers (PowerDNS rrsets)
444
+
445
+
446
+ def zone_id(zone: str) -> str:
447
+ """PowerDNS zone ids carry a trailing dot; accept names without it."""
448
+
449
+ return zone if zone.endswith(".") else f"{zone}."
450
+
451
+
452
+ def fqdn(name: str, zone: str) -> str:
453
+ """Absolute record name with trailing dot, PowerDNS style."""
454
+
455
+ zone_root = zone.rstrip(".")
456
+ relative = name.rstrip(".")
457
+ if relative in ("@", ""):
458
+ return f"{zone_root}."
459
+ if relative == zone_root or relative.endswith(f".{zone_root}"):
460
+ return f"{relative}."
461
+ return f"{relative}.{zone_root}."
462
+
463
+
464
+ def flatten_rrsets(rrsets: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
465
+ """One row per record value, for display and `records list`."""
466
+
467
+ rows = []
468
+ for rrset in rrsets or []:
469
+ for record in rrset.get("records", []):
470
+ rows.append(
471
+ {
472
+ "name": rrset.get("name"),
473
+ "type": rrset.get("type"),
474
+ "ttl": rrset.get("ttl"),
475
+ "content": record.get("content"),
476
+ "disabled": record.get("disabled", False),
477
+ }
478
+ )
479
+ return sorted(rows, key=lambda row: (row["name"] or "", row["type"] or ""))
480
+
481
+
482
+ def build_rrset(
483
+ zone: str,
484
+ name: str,
485
+ rtype: str,
486
+ *,
487
+ changetype: str,
488
+ ttl: int | None = None,
489
+ contents: Iterable[str] = (),
490
+ ) -> dict[str, Any]:
491
+ rrset: dict[str, Any] = {
492
+ "name": fqdn(name, zone),
493
+ "type": rtype.upper(),
494
+ "changetype": changetype,
495
+ }
496
+ if changetype == "REPLACE":
497
+ rrset["ttl"] = ttl
498
+ rrset["records"] = [
499
+ {"content": content, "disabled": False} for content in contents
500
+ ]
501
+ return rrset
502
+
503
+
504
+ # ---------------------------------------------------------------------------
505
+ # DNS transfer: pull a zone from another provider into DevNomads.
506
+ # Drivers are read-only by design; writing happens exclusively on the
507
+ # DevNomads side through the PowerDNS rrsets PATCH.
508
+
509
+
510
+ @dataclass(frozen=True)
511
+ class TransferRecord:
512
+ """Provider-neutral record: relative name ("@" for the apex), TTL in
513
+ seconds, and content in PowerDNS style (MX/SRV priority embedded)."""
514
+
515
+ name: str
516
+ type: str
517
+ content: str
518
+ ttl: int
519
+
520
+
521
+ class DnsSource(Protocol):
522
+ name: str
523
+
524
+ def get_records(self, zone: str) -> list[TransferRecord]: ...
525
+
526
+
527
+ HOST_CONTENT_TYPES = {"CNAME", "NS", "PTR", "ALIAS", "MX", "SRV"}
528
+
529
+
530
+ def normalize_content(rtype: str, content: str) -> str:
531
+ """Canonicalize record content the PowerDNS way, so source and target
532
+ records compare equal: hostname targets get a trailing dot, TXT values
533
+ get quoted. Idempotent on already-canonical content."""
534
+
535
+ content = content.strip()
536
+ if rtype == "TXT":
537
+ return content if content.startswith('"') else f'"{content}"'
538
+ if rtype in HOST_CONTENT_TYPES:
539
+ parts = content.split()
540
+ if parts and not parts[-1].endswith("."):
541
+ parts[-1] += "."
542
+ return " ".join(parts)
543
+ return content
544
+
545
+
546
+ def _transfer_skip(name: str, rtype: str, zone: str) -> bool:
547
+ # the target zone owns its SOA and apex NS records
548
+ if rtype == "SOA":
549
+ return True
550
+ return rtype == "NS" and name == f"{zone.rstrip('.')}."
551
+
552
+
553
+ def desired_rrsets(
554
+ records: Iterable[TransferRecord], zone: str
555
+ ) -> dict[tuple[str, str], dict[str, Any]]:
556
+ """Group normalized source records into PowerDNS-comparable rrsets."""
557
+
558
+ out: dict[tuple[str, str], dict[str, Any]] = {}
559
+ for record in records:
560
+ rtype = record.type.upper()
561
+ name = fqdn(record.name, zone)
562
+ if _transfer_skip(name, rtype, zone):
563
+ continue
564
+ entry = out.setdefault((name, rtype), {"ttl": record.ttl, "contents": set()})
565
+ entry["ttl"] = min(entry["ttl"], record.ttl)
566
+ entry["contents"].add(normalize_content(rtype, record.content))
567
+ return out
568
+
569
+
570
+ def current_rrsets(
571
+ rrsets: Iterable[dict[str, Any]], zone: str
572
+ ) -> dict[tuple[str, str], dict[str, Any]]:
573
+ """The same comparable shape, from a DevNomads (PowerDNS) zone."""
574
+
575
+ out: dict[tuple[str, str], dict[str, Any]] = {}
576
+ for rrset in rrsets or []:
577
+ rtype = str(rrset.get("type", "")).upper()
578
+ name = str(rrset.get("name", ""))
579
+ if _transfer_skip(name, rtype, zone):
580
+ continue
581
+ out[(name, rtype)] = {
582
+ "ttl": rrset.get("ttl"),
583
+ "contents": {
584
+ normalize_content(rtype, record["content"])
585
+ for record in rrset.get("records", [])
586
+ },
587
+ }
588
+ return out
589
+
590
+
591
+ def diff_rrsets(
592
+ current: dict[tuple[str, str], dict[str, Any]],
593
+ desired: dict[tuple[str, str], dict[str, Any]],
594
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
595
+ """Compute (display rows, PATCH rrsets) to turn current into desired."""
596
+
597
+ changes: list[dict[str, Any]] = []
598
+ patch: list[dict[str, Any]] = []
599
+ for key in sorted(set(current) | set(desired)):
600
+ old, new = current.get(key), desired.get(key)
601
+ if old == new:
602
+ continue
603
+ name, rtype = key
604
+ if new is None:
605
+ if old is None:
606
+ continue
607
+ changes.append(
608
+ {
609
+ "action": "delete",
610
+ "name": name,
611
+ "type": rtype,
612
+ "ttl": old["ttl"],
613
+ "content": "; ".join(sorted(old["contents"])),
614
+ }
615
+ )
616
+ patch.append({"name": name, "type": rtype, "changetype": "DELETE"})
617
+ continue
618
+ changes.append(
619
+ {
620
+ "action": "create" if old is None else "update",
621
+ "name": name,
622
+ "type": rtype,
623
+ "ttl": new["ttl"],
624
+ "content": "; ".join(sorted(new["contents"])),
625
+ }
626
+ )
627
+ patch.append(
628
+ {
629
+ "name": name,
630
+ "type": rtype,
631
+ "ttl": new["ttl"],
632
+ "changetype": "REPLACE",
633
+ "records": [
634
+ {"content": content, "disabled": False}
635
+ for content in sorted(new["contents"])
636
+ ],
637
+ }
638
+ )
639
+ return changes, patch
640
+
641
+
642
+ class TransipSource:
643
+ """TransIP REST API v6: RSA-SHA512 signed auth request for a read-only
644
+ JWT, then plain bearer requests."""
645
+
646
+ name = "transip"
647
+
648
+ def __init__(
649
+ self, login: str, private_key_pem: str, api_url: str | None = None
650
+ ) -> None:
651
+ self.login = login
652
+ self.private_key_pem = private_key_pem
653
+ self._http = httpx.Client(
654
+ base_url=api_url or "https://api.transip.nl/v6",
655
+ headers={"User-Agent": f"dnctl/{__version__}"},
656
+ timeout=REQUEST_TIMEOUT,
657
+ )
658
+ self._authenticated = False
659
+
660
+ def _authenticate(self) -> None:
661
+ from cryptography.hazmat.primitives import hashes, serialization
662
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
663
+
664
+ try:
665
+ key = serialization.load_pem_private_key(
666
+ self.private_key_pem.encode(), password=None
667
+ )
668
+ except ValueError as exc:
669
+ raise CliError(f"cannot load TransIP private key: {exc}") from exc
670
+ if not isinstance(key, rsa.RSAPrivateKey):
671
+ raise CliError("the TransIP private key must be an RSA key")
672
+ nonce = secrets.token_hex(16)
673
+ body = json.dumps(
674
+ {
675
+ "login": self.login,
676
+ "nonce": nonce,
677
+ "read_only": True,
678
+ "expiration_time": "30 minutes",
679
+ "label": f"dnctl-{nonce[:10]}",
680
+ "global_key": True,
681
+ }
682
+ )
683
+ signature = base64.b64encode(
684
+ key.sign(body.encode(), padding.PKCS1v15(), hashes.SHA512())
685
+ ).decode()
686
+ with working("authenticating with TransIP"):
687
+ response = self._http.post(
688
+ "/auth",
689
+ content=body,
690
+ headers={"Signature": signature, "Content-Type": "application/json"},
691
+ )
692
+ if response.status_code >= 400:
693
+ raise CliError(
694
+ f"TransIP authentication failed ({response.status_code}): "
695
+ f"{_provider_error(response)} - check the login name and private "
696
+ "key, and that non-whitelisted API keys are allowed in the "
697
+ "TransIP control panel"
698
+ )
699
+ self._http.headers["Authorization"] = f"Bearer {response.json()['token']}"
700
+ self._authenticated = True
701
+
702
+ def get_records(self, zone: str) -> list[TransferRecord]:
703
+ if not self._authenticated:
704
+ self._authenticate()
705
+ with working(f"TransIP: GET /domains/{zone}/dns"):
706
+ response = self._http.get(f"/domains/{zone}/dns")
707
+ if response.status_code >= 400:
708
+ raise CliError(
709
+ f"TransIP returned {response.status_code} for zone '{zone}': "
710
+ f"{_provider_error(response)}"
711
+ )
712
+ return [
713
+ TransferRecord(
714
+ name=str(entry["name"]),
715
+ type=str(entry["type"]).upper(),
716
+ content=str(entry["content"]),
717
+ ttl=int(entry["expire"]),
718
+ )
719
+ for entry in response.json().get("dnsEntries", [])
720
+ ]
721
+
722
+
723
+ class AuroraDnsSource:
724
+ """AuroraDNS (PCextreme): HMAC-SHA256 signed requests, protocol as
725
+ implemented in Apache Libcloud's auroradns driver."""
726
+
727
+ name = "auroradns"
728
+
729
+ def __init__(
730
+ self, api_key: str, secret_key: str, api_url: str | None = None
731
+ ) -> None:
732
+ self.api_key = api_key
733
+ self.secret_key = secret_key
734
+ self._http = httpx.Client(
735
+ base_url=api_url or "https://api.auroradns.eu",
736
+ headers={"User-Agent": f"dnctl/{__version__}"},
737
+ timeout=REQUEST_TIMEOUT,
738
+ )
739
+
740
+ def _headers(self, method: str, path: str) -> dict[str, str]:
741
+ timestamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
742
+ signature = base64.b64encode(
743
+ hmac.new(
744
+ self.secret_key.encode(),
745
+ f"{method}{path}{timestamp}".encode(),
746
+ hashlib.sha256,
747
+ ).digest()
748
+ ).decode()
749
+ token = base64.b64encode(f"{self.api_key}:{signature}".encode()).decode()
750
+ return {
751
+ "X-AuroraDNS-Date": timestamp,
752
+ "Authorization": f"AuroraDNSv1 {token}",
753
+ }
754
+
755
+ def _get(self, path: str) -> Any:
756
+ with working(f"AuroraDNS: GET {path}"):
757
+ response = self._http.get(path, headers=self._headers("GET", path))
758
+ if response.status_code >= 400:
759
+ raise CliError(
760
+ f"AuroraDNS returned {response.status_code} for {path}: "
761
+ f"{_provider_error(response)}"
762
+ )
763
+ return response.json()
764
+
765
+ def get_records(self, zone: str) -> list[TransferRecord]:
766
+ zones = self._get("/zones")
767
+ zone_id = next(
768
+ (
769
+ item["id"]
770
+ for item in zones
771
+ if str(item.get("name", "")).rstrip(".") == zone.rstrip(".")
772
+ ),
773
+ None,
774
+ )
775
+ if zone_id is None:
776
+ raise CliError(f"zone '{zone}' not found in this AuroraDNS account")
777
+ records = []
778
+ for record in self._get(f"/zones/{zone_id}/records"):
779
+ rtype = str(record["type"]).upper()
780
+ content = str(record["content"])
781
+ prio = record.get("prio")
782
+ if rtype in ("MX", "SRV") and prio is not None:
783
+ content = f"{prio} {content}"
784
+ records.append(
785
+ TransferRecord(
786
+ name=str(record.get("name") or "@"),
787
+ type=rtype,
788
+ content=content,
789
+ ttl=int(record["ttl"]),
790
+ )
791
+ )
792
+ return records
793
+
794
+
795
+ def _provider_error(response: httpx.Response) -> str:
796
+ try:
797
+ body = response.json()
798
+ except ValueError:
799
+ return response.text[:200]
800
+ if isinstance(body, dict):
801
+ return str(body.get("error") or body.get("errormsg") or body)
802
+ return str(body)
803
+
804
+
805
+ def _source_value(
806
+ stored: dict[str, str], key: str, env_var: str, label: str, *, hide: bool = False
807
+ ) -> str:
808
+ value = stored.get(key) or os.environ.get(env_var)
809
+ if value:
810
+ return value
811
+ if not sys.stdin.isatty():
812
+ raise CliError(
813
+ f"missing source credential '{label}'; store it with "
814
+ f"`dnctl configure --provider <driver>` or set {env_var}"
815
+ )
816
+ return typer.prompt(label, hide_input=hide, err=True)
817
+
818
+
819
+ def build_source(driver: str, source_profile: str | None) -> DnsSource:
820
+ if driver not in PROVIDERS:
821
+ raise CliError(f"unknown driver '{driver}' (available: {', '.join(PROVIDERS)})")
822
+ parser = load_credentials()
823
+ section = f"{driver}:{source_profile or DEFAULT_PROFILE}"
824
+ stored = dict(parser[section]) if parser.has_section(section) else {}
825
+ if source_profile and not stored:
826
+ raise CliError(
827
+ f"profile '{section}' not found; run: "
828
+ f"dnctl configure --provider {driver} --profile {source_profile}"
829
+ )
830
+ api_url = stored.get("api_url") # testing escape hatch, like the main client
831
+ if driver == "transip":
832
+ login = _source_value(stored, "login", "DN_TRANSIP_LOGIN", "TransIP login name")
833
+ key_file = _source_value(
834
+ stored,
835
+ "private_key_file",
836
+ "DN_TRANSIP_PRIVATE_KEY_FILE",
837
+ "Path to TransIP private key (PEM)",
838
+ )
839
+ try:
840
+ pem = Path(key_file).expanduser().read_text()
841
+ except OSError as exc:
842
+ raise CliError(f"cannot read TransIP private key: {exc}") from exc
843
+ return TransipSource(login, pem, api_url)
844
+ api_key = _source_value(
845
+ stored, "api_key", "DN_AURORADNS_API_KEY", "AuroraDNS API key", hide=True
846
+ )
847
+ secret_key = _source_value(
848
+ stored,
849
+ "secret_key",
850
+ "DN_AURORADNS_SECRET_KEY",
851
+ "AuroraDNS secret key",
852
+ hide=True,
853
+ )
854
+ return AuroraDnsSource(api_key, secret_key, api_url)
855
+
856
+
857
+ # ---------------------------------------------------------------------------
858
+ # CLI
859
+
860
+
861
+ class PrefixGroup(TyperGroup):
862
+ """Resolve unique command prefixes: `dnctl e l` runs `dnctl emails list`.
863
+
864
+ Exact names always win; an ambiguous prefix fails listing the matches."""
865
+
866
+ # typer vendors click, so the context/command annotations stay loose
867
+ def get_command(self, ctx: Any, cmd_name: str) -> Any:
868
+ command = super().get_command(ctx, cmd_name)
869
+ if command is not None:
870
+ return command
871
+ matches = [
872
+ name for name in self.list_commands(ctx) if name.startswith(cmd_name)
873
+ ]
874
+ if len(matches) == 1:
875
+ return super().get_command(ctx, matches[0])
876
+ if matches:
877
+ ctx.fail(f"'{cmd_name}' is ambiguous: {', '.join(sorted(matches))}")
878
+ return None
879
+
880
+ def resolve_command(self, ctx: Any, args: Any) -> Any:
881
+ # report the resolved full command name, not the typed prefix
882
+ _, command, remaining = super().resolve_command(ctx, args)
883
+ return command.name if command else None, command, remaining
884
+
885
+
886
+ app = typer.Typer(
887
+ name="dnctl",
888
+ cls=PrefixGroup,
889
+ help="Manage your DevNomads services from the command line.",
890
+ no_args_is_help=True,
891
+ )
892
+ configure_app = typer.Typer(cls=PrefixGroup, help="Manage stored credential profiles.")
893
+ services_app = typer.Typer(
894
+ cls=PrefixGroup, help="Your DevNomads services.", no_args_is_help=True
895
+ )
896
+ dns_app = typer.Typer(
897
+ cls=PrefixGroup, help="DNS zones and records.", no_args_is_help=True
898
+ )
899
+ zones_app = typer.Typer(cls=PrefixGroup, help="DNS zones.", no_args_is_help=True)
900
+ records_app = typer.Typer(cls=PrefixGroup, help="DNS records.", no_args_is_help=True)
901
+
902
+ app.add_typer(configure_app, name="configure")
903
+ app.add_typer(services_app, name="services")
904
+ app.add_typer(dns_app, name="dns")
905
+ dns_app.add_typer(zones_app, name="zones")
906
+ dns_app.add_typer(records_app, name="records")
907
+
908
+
909
+ SortOption = Annotated[
910
+ str | None,
911
+ typer.Option(
912
+ "--sort",
913
+ help="Sort by field; prefix with - for descending (e.g. --sort -ttl).",
914
+ ),
915
+ ]
916
+ OutputOption = Annotated[
917
+ OutputFormat | None,
918
+ typer.Option("--output", "-o", help="Output format (table or json)."),
919
+ ]
920
+
921
+
922
+ def _version_callback(value: bool) -> None:
923
+ if value:
924
+ sys.stdout.write(f"dnctl {__version__}\n")
925
+ raise typer.Exit()
926
+
927
+
928
+ @app.callback()
929
+ def main(
930
+ ctx: typer.Context,
931
+ profile: Annotated[
932
+ str | None,
933
+ typer.Option(
934
+ "--profile", "-p", help=f"Credentials profile (env: {ENV_PROFILE})."
935
+ ),
936
+ ] = None,
937
+ api_key: Annotated[
938
+ str | None,
939
+ typer.Option(
940
+ "--api-key", help=f"API key, beats any profile (env: {ENV_API_KEY})."
941
+ ),
942
+ ] = None,
943
+ output: Annotated[
944
+ OutputFormat | None,
945
+ typer.Option(
946
+ "--output",
947
+ "-o",
948
+ help="Output format; default: table on a TTY, json when piped.",
949
+ ),
950
+ ] = None,
951
+ debug: Annotated[
952
+ bool, typer.Option("--debug", help="Log the HTTP exchange to stderr.")
953
+ ] = False,
954
+ version: Annotated[
955
+ bool,
956
+ typer.Option(
957
+ "--version",
958
+ callback=_version_callback,
959
+ is_eager=True,
960
+ help="Print version and exit.",
961
+ ),
962
+ ] = False,
963
+ ) -> None:
964
+ ctx.obj = AppState(profile=profile, api_key=api_key, output=output, debug=debug)
965
+
966
+
967
+ # --- configure -------------------------------------------------------------
968
+
969
+
970
+ @configure_app.callback(invoke_without_command=True)
971
+ def configure(
972
+ ctx: typer.Context,
973
+ profile: Annotated[
974
+ str, typer.Option(help="Profile name to create or update.")
975
+ ] = DEFAULT_PROFILE,
976
+ provider: Annotated[
977
+ str | None,
978
+ typer.Option(
979
+ help="Configure a DNS transfer source profile "
980
+ f"({', '.join(PROVIDERS)}) instead of a DevNomads profile."
981
+ ),
982
+ ] = None,
983
+ ) -> None:
984
+ """Interactively store credentials in your home directory."""
985
+
986
+ if ctx.invoked_subcommand:
987
+ return
988
+ if provider is None:
989
+ section = profile
990
+ values = {
991
+ "api_key": typer.prompt("DevNomads API key", hide_input=True, err=True)
992
+ }
993
+ elif provider == "auroradns":
994
+ section = f"auroradns:{profile}"
995
+ values = {
996
+ "api_key": typer.prompt("AuroraDNS API key", hide_input=True, err=True),
997
+ "secret_key": typer.prompt(
998
+ "AuroraDNS secret key", hide_input=True, err=True
999
+ ),
1000
+ }
1001
+ elif provider == "transip":
1002
+ section = f"transip:{profile}"
1003
+ login = typer.prompt("TransIP login name", err=True)
1004
+ key_source = typer.prompt("Path to TransIP private key (PEM)", err=True)
1005
+ key_path = _import_private_key(Path(key_source).expanduser(), profile)
1006
+ values = {"login": login, "private_key_file": str(key_path)}
1007
+ else:
1008
+ raise CliError(
1009
+ f"unknown provider '{provider}' (available: {', '.join(PROVIDERS)})"
1010
+ )
1011
+ parser = load_credentials()
1012
+ if not parser.has_section(section):
1013
+ parser.add_section(section)
1014
+ for key, value in values.items():
1015
+ parser.set(section, key, value)
1016
+ save_credentials(parser)
1017
+ err_console.print(f"profile '{section}' written to {credentials_path()}")
1018
+
1019
+
1020
+ def _import_private_key(source: Path, profile: str) -> Path:
1021
+ try:
1022
+ pem = source.read_text()
1023
+ except OSError as exc:
1024
+ raise CliError(f"cannot read private key: {exc}") from exc
1025
+ if "PRIVATE KEY" not in pem:
1026
+ raise CliError(f"{source} does not look like a PEM private key")
1027
+ target = config_dir() / f"transip-{profile}.pem"
1028
+ _write_private(target, pem)
1029
+ return target
1030
+
1031
+
1032
+ @configure_app.command("list")
1033
+ def configure_list(
1034
+ ctx: typer.Context, sort: SortOption = None, output: OutputOption = None
1035
+ ) -> None:
1036
+ """List stored profiles (secrets masked)."""
1037
+
1038
+ parser = load_credentials()
1039
+ rows = []
1040
+ for section in parser.sections():
1041
+ provider, _, name = section.partition(":")
1042
+ values = parser[section]
1043
+ secret = values.get("api_key") or values.get("secret_key") or ""
1044
+ rows.append(
1045
+ {
1046
+ "profile": section,
1047
+ "type": provider if name else "devnomads",
1048
+ "api_key": _mask(secret) or values.get("login", ""),
1049
+ }
1050
+ )
1051
+ render(
1052
+ state_from(ctx, output),
1053
+ sort_rows(rows, sort),
1054
+ columns=["profile", "type", "api_key"],
1055
+ title="Profiles",
1056
+ )
1057
+
1058
+
1059
+ # --- services ----------------------------------------------------------------
1060
+
1061
+
1062
+ @services_app.command("list")
1063
+ def services_list(
1064
+ ctx: typer.Context, sort: SortOption = None, output: OutputOption = None
1065
+ ) -> None:
1066
+ """List all your services."""
1067
+
1068
+ state = state_from(ctx, output)
1069
+ data = get_client(state).request("GET", "/services")
1070
+ if isinstance(data, list):
1071
+ data = sort_rows(data, sort)
1072
+ render(
1073
+ state,
1074
+ data,
1075
+ columns=["service_id", "type", "entity", "started_at", "ended_at"],
1076
+ title="Services",
1077
+ )
1078
+
1079
+
1080
+ @services_app.command("show")
1081
+ def services_show(
1082
+ ctx: typer.Context,
1083
+ service_id: Annotated[int, typer.Argument(help="Service ID.")],
1084
+ output: OutputOption = None,
1085
+ ) -> None:
1086
+ """Show one service."""
1087
+
1088
+ state = state_from(ctx, output)
1089
+ data = get_client(state).request("GET", f"/services/{service_id}")
1090
+ render(state, data, title=f"Service {service_id}")
1091
+
1092
+
1093
+ # --- dns zones ---------------------------------------------------------------
1094
+
1095
+
1096
+ @zones_app.command("list")
1097
+ def zones_list(
1098
+ ctx: typer.Context, sort: SortOption = None, output: OutputOption = None
1099
+ ) -> None:
1100
+ """List your DNS zones."""
1101
+
1102
+ state = state_from(ctx, output)
1103
+ data = get_client(state).request("GET", "/services/dns/zones")
1104
+ if isinstance(data, list):
1105
+ data = sort_rows(data, sort)
1106
+ render(
1107
+ state,
1108
+ data,
1109
+ columns=["name", "kind", "serial", "dnssec"],
1110
+ title="DNS zones",
1111
+ )
1112
+
1113
+
1114
+ @zones_app.command("show")
1115
+ def zones_show(
1116
+ ctx: typer.Context,
1117
+ zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
1118
+ output: OutputOption = None,
1119
+ ) -> None:
1120
+ """Show a DNS zone including its records."""
1121
+
1122
+ state = state_from(ctx, output)
1123
+ data = get_client(state).request("GET", f"/services/dns/zones/{zone_id(zone)}")
1124
+ if resolve_format(state) is OutputFormat.json or not isinstance(data, dict):
1125
+ render(state, data)
1126
+ return
1127
+ meta = {key: value for key, value in data.items() if key != "rrsets"}
1128
+ render(state, meta, title=zone)
1129
+ render(
1130
+ state,
1131
+ flatten_rrsets(data.get("rrsets", [])),
1132
+ columns=RECORD_COLUMNS,
1133
+ title="Records",
1134
+ )
1135
+
1136
+
1137
+ # --- dns records -------------------------------------------------------------
1138
+
1139
+
1140
+ @records_app.command("list")
1141
+ def records_list(
1142
+ ctx: typer.Context,
1143
+ zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
1144
+ sort: SortOption = None,
1145
+ output: OutputOption = None,
1146
+ ) -> None:
1147
+ """List the records in a zone, one row per value."""
1148
+
1149
+ state = state_from(ctx, output)
1150
+ data = get_client(state).request("GET", f"/services/dns/zones/{zone_id(zone)}")
1151
+ rrsets = data.get("rrsets", []) if isinstance(data, dict) else []
1152
+ render(
1153
+ state,
1154
+ sort_rows(flatten_rrsets(rrsets), sort),
1155
+ columns=RECORD_COLUMNS,
1156
+ title=f"Records in {zone}",
1157
+ )
1158
+
1159
+
1160
+ @records_app.command("set")
1161
+ def records_set(
1162
+ ctx: typer.Context,
1163
+ zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
1164
+ name: Annotated[
1165
+ str, typer.Argument(help="Record name relative to the zone; @ for the apex.")
1166
+ ],
1167
+ rtype: Annotated[
1168
+ str, typer.Argument(metavar="TYPE", help="Record type, e.g. A, MX, TXT.")
1169
+ ],
1170
+ content: Annotated[
1171
+ list[str],
1172
+ typer.Argument(
1173
+ help="One or more record values; replaces the whole record set."
1174
+ ),
1175
+ ],
1176
+ ttl: Annotated[int, typer.Option(help="TTL in seconds.")] = 3600,
1177
+ ) -> None:
1178
+ """Create or replace a record set (PowerDNS REPLACE semantics)."""
1179
+
1180
+ state = state_from(ctx)
1181
+ rrset = build_rrset(
1182
+ zone, name, rtype, changetype="REPLACE", ttl=ttl, contents=content
1183
+ )
1184
+ payload = {"rrsets": [rrset]}
1185
+ get_client(state).request(
1186
+ "PATCH", f"/services/dns/zones/{zone_id(zone)}", json_body=payload
1187
+ )
1188
+ err_console.print(
1189
+ f"set {rrset['name']} {rrset['type']} "
1190
+ f"({len(rrset['records'])} value(s), ttl {ttl})"
1191
+ )
1192
+
1193
+
1194
+ @records_app.command("delete")
1195
+ def records_delete(
1196
+ ctx: typer.Context,
1197
+ zone: Annotated[str, typer.Argument(help="Zone name, e.g. example.com.")],
1198
+ name: Annotated[
1199
+ str, typer.Argument(help="Record name relative to the zone; @ for the apex.")
1200
+ ],
1201
+ rtype: Annotated[
1202
+ str, typer.Argument(metavar="TYPE", help="Record type, e.g. A, MX, TXT.")
1203
+ ],
1204
+ yes: Annotated[
1205
+ bool, typer.Option("--yes", "-y", help="Do not ask for confirmation.")
1206
+ ] = False,
1207
+ ) -> None:
1208
+ """Delete a whole record set (all values for name + type)."""
1209
+
1210
+ state = state_from(ctx)
1211
+ rrset = build_rrset(zone, name, rtype, changetype="DELETE")
1212
+ _confirm(f"Delete all {rrset['type']} records for {rrset['name']}?", yes)
1213
+ payload = {"rrsets": [rrset]}
1214
+ get_client(state).request(
1215
+ "PATCH", f"/services/dns/zones/{zone_id(zone)}", json_body=payload
1216
+ )
1217
+ err_console.print(f"deleted {rrset['name']} {rrset['type']}")
1218
+
1219
+
1220
+ @dns_app.command("transfer")
1221
+ def dns_transfer(
1222
+ ctx: typer.Context,
1223
+ from_: Annotated[
1224
+ str,
1225
+ typer.Option(
1226
+ "--from",
1227
+ metavar="DRIVER",
1228
+ help=f"Source provider ({', '.join(PROVIDERS)}).",
1229
+ ),
1230
+ ],
1231
+ zone: Annotated[
1232
+ str, typer.Option("--zone", help="Zone name on both sides, e.g. example.com.")
1233
+ ],
1234
+ source_profile: Annotated[
1235
+ str | None,
1236
+ typer.Option(
1237
+ "--source-profile",
1238
+ help="Stored provider profile to read credentials from "
1239
+ "(default: the provider's 'default' profile).",
1240
+ ),
1241
+ ] = None,
1242
+ dry_run: Annotated[
1243
+ bool, typer.Option("--dry-run", help="Show the changes without applying them.")
1244
+ ] = False,
1245
+ yes: Annotated[
1246
+ bool, typer.Option("--yes", "-y", help="Do not ask for confirmation.")
1247
+ ] = False,
1248
+ output: OutputOption = None,
1249
+ ) -> None:
1250
+ """Copy a DNS zone from another provider into DevNomads.
1251
+
1252
+ Fetches the records at the source (read-only), diffs them against the
1253
+ DevNomads zone, shows the changes, and applies them after confirmation.
1254
+ SOA and apex NS records stay untouched - the platform owns those.
1255
+ """
1256
+
1257
+ state = state_from(ctx, output)
1258
+ client = get_client(state) # resolve DevNomads credentials first: fail fast
1259
+ source = build_source(from_, source_profile)
1260
+ zones = client.request("GET", "/services/dns/zones")
1261
+ known = {str(item.get("name", "")).rstrip(".") for item in zones or []}
1262
+ if zone.rstrip(".") not in known:
1263
+ raise CliError(
1264
+ f"zone '{zone}' does not exist at DevNomads; register or transfer "
1265
+ "the domain first (dnctl dns zones list shows your zones)"
1266
+ )
1267
+ err_console.print(f"fetching {zone} from {source.name}")
1268
+ records = source.get_records(zone)
1269
+ data = client.request("GET", f"/services/dns/zones/{zone_id(zone)}")
1270
+ rrsets = data.get("rrsets", []) if isinstance(data, dict) else []
1271
+ changes, patch = diff_rrsets(
1272
+ current_rrsets(rrsets, zone), desired_rrsets(records, zone)
1273
+ )
1274
+ if not changes:
1275
+ err_console.print(f"{zone} is already in sync with {source.name}")
1276
+ return
1277
+ render(
1278
+ state,
1279
+ changes,
1280
+ columns=["action", "name", "type", "ttl", "content"],
1281
+ title=f"Transfer {zone} from {source.name}",
1282
+ )
1283
+ if dry_run:
1284
+ err_console.print(f"dry run: {len(changes)} change(s) not applied")
1285
+ return
1286
+ _confirm(f"Apply {len(changes)} change(s) to {zone}", yes)
1287
+ client.request(
1288
+ "PATCH", f"/services/dns/zones/{zone_id(zone)}", json_body={"rrsets": patch}
1289
+ )
1290
+ err_console.print(f"applied {len(changes)} change(s) to {zone}")
1291
+
1292
+
1293
+ # ---------------------------------------------------------------------------
1294
+ # Generated commands. tools/generate.py renders one thin command per
1295
+ # OpenAPI operation between the markers below, from openapi.json plus
1296
+ # the curation overlay in overlay.json. Do not edit the region by hand.
1297
+
1298
+ _SORT_OPT = typer.Option(
1299
+ None,
1300
+ "--sort",
1301
+ help="Sort by field; prefix with - for descending (e.g. --sort -ttl).",
1302
+ )
1303
+ _YES_OPT = typer.Option(False, "--yes", "-y", help="Do not ask for confirmation.")
1304
+ _OUTPUT_OPT = typer.Option(
1305
+ None, "--output", "-o", help="Output format (table or json)."
1306
+ )
1307
+
1308
+
1309
+ def _generated_call(
1310
+ ctx: typer.Context,
1311
+ method: str,
1312
+ path_template: str,
1313
+ path_params: dict[str, Any],
1314
+ *,
1315
+ body: dict[str, Any] | None = None,
1316
+ sort: str | None = None,
1317
+ columns: list[str] | None = None,
1318
+ confirm: str | None = None,
1319
+ yes: bool = False,
1320
+ output: OutputFormat | None = None,
1321
+ ) -> None:
1322
+ """Shared runtime for generated commands: build the path, confirm if
1323
+ needed, call the API, render the result."""
1324
+
1325
+ state = state_from(ctx, output)
1326
+ if confirm:
1327
+ detail = ", ".join(f"{key}={value}" for key, value in path_params.items())
1328
+ _confirm(f"{confirm} ({detail})?" if detail else f"{confirm}?", yes)
1329
+ quoted = {
1330
+ key: urllib.parse.quote(str(value), safe="")
1331
+ for key, value in path_params.items()
1332
+ }
1333
+ path = path_template.format(**quoted)
1334
+ payload = {key: value for key, value in (body or {}).items() if value is not None}
1335
+ data = get_client(state).request(method, path, json_body=payload or None)
1336
+ if data is None:
1337
+ err_console.print("ok")
1338
+ return
1339
+ if isinstance(data, list):
1340
+ data = sort_rows(data, sort)
1341
+ render(state, data, columns=columns, title=path)
1342
+
1343
+
1344
+ # --- BEGIN GENERATED COMMANDS (tools/generate.py; do not edit by hand) ---
1345
+
1346
+ gen_apps = typer.Typer(cls=PrefixGroup, help="Manage apps.", no_args_is_help=True)
1347
+ app.add_typer(gen_apps, name="apps")
1348
+ gen_buckets = typer.Typer(cls=PrefixGroup, help="Manage buckets.", no_args_is_help=True)
1349
+ app.add_typer(gen_buckets, name="buckets")
1350
+ gen_containers = typer.Typer(
1351
+ cls=PrefixGroup, help="Manage containers.", no_args_is_help=True
1352
+ )
1353
+ app.add_typer(gen_containers, name="containers")
1354
+ gen_databases = typer.Typer(
1355
+ cls=PrefixGroup, help="Manage databases.", no_args_is_help=True
1356
+ )
1357
+ app.add_typer(gen_databases, name="databases")
1358
+ gen_domains = typer.Typer(cls=PrefixGroup, help="Manage domains.", no_args_is_help=True)
1359
+ app.add_typer(gen_domains, name="domains")
1360
+ gen_emails = typer.Typer(cls=PrefixGroup, help="Manage emails.", no_args_is_help=True)
1361
+ app.add_typer(gen_emails, name="emails")
1362
+ gen_forwards = typer.Typer(
1363
+ cls=PrefixGroup, help="Manage forwards.", no_args_is_help=True
1364
+ )
1365
+ app.add_typer(gen_forwards, name="forwards")
1366
+ gen_handles = typer.Typer(cls=PrefixGroup, help="Manage handles.", no_args_is_help=True)
1367
+ app.add_typer(gen_handles, name="handles")
1368
+ gen_proxies = typer.Typer(cls=PrefixGroup, help="Manage proxies.", no_args_is_help=True)
1369
+ app.add_typer(gen_proxies, name="proxies")
1370
+ gen_searches = typer.Typer(
1371
+ cls=PrefixGroup, help="Manage searches.", no_args_is_help=True
1372
+ )
1373
+ app.add_typer(gen_searches, name="searches")
1374
+ gen_servers = typer.Typer(cls=PrefixGroup, help="Manage servers.", no_args_is_help=True)
1375
+ app.add_typer(gen_servers, name="servers")
1376
+ gen_sites = typer.Typer(cls=PrefixGroup, help="Manage sites.", no_args_is_help=True)
1377
+ app.add_typer(gen_sites, name="sites")
1378
+ gen_spams = typer.Typer(cls=PrefixGroup, help="Manage spams.", no_args_is_help=True)
1379
+ app.add_typer(gen_spams, name="spams")
1380
+ gen_containers_instances = typer.Typer(
1381
+ cls=PrefixGroup, help="Manage containers instances.", no_args_is_help=True
1382
+ )
1383
+ gen_containers.add_typer(gen_containers_instances, name="instances")
1384
+ gen_databases_clusters = typer.Typer(
1385
+ cls=PrefixGroup, help="Manage databases clusters.", no_args_is_help=True
1386
+ )
1387
+ gen_databases.add_typer(gen_databases_clusters, name="clusters")
1388
+ gen_databases_permissions = typer.Typer(
1389
+ cls=PrefixGroup, help="Manage databases permissions.", no_args_is_help=True
1390
+ )
1391
+ gen_databases.add_typer(gen_databases_permissions, name="permissions")
1392
+ gen_databases_users = typer.Typer(
1393
+ cls=PrefixGroup, help="Manage databases users.", no_args_is_help=True
1394
+ )
1395
+ gen_databases.add_typer(gen_databases_users, name="users")
1396
+ gen_emails_aliases = typer.Typer(
1397
+ cls=PrefixGroup, help="Manage emails aliases.", no_args_is_help=True
1398
+ )
1399
+ gen_emails.add_typer(gen_emails_aliases, name="aliases")
1400
+ gen_emails_dkim = typer.Typer(
1401
+ cls=PrefixGroup, help="Manage emails dkim.", no_args_is_help=True
1402
+ )
1403
+ gen_emails.add_typer(gen_emails_dkim, name="dkim")
1404
+ gen_emails_forwardings = typer.Typer(
1405
+ cls=PrefixGroup, help="Manage emails forwardings.", no_args_is_help=True
1406
+ )
1407
+ gen_emails.add_typer(gen_emails_forwardings, name="forwardings")
1408
+ gen_emails_mailboxes = typer.Typer(
1409
+ cls=PrefixGroup, help="Manage emails mailboxes.", no_args_is_help=True
1410
+ )
1411
+ gen_emails.add_typer(gen_emails_mailboxes, name="mailboxes")
1412
+ gen_emails_records = typer.Typer(
1413
+ cls=PrefixGroup, help="Manage emails records.", no_args_is_help=True
1414
+ )
1415
+ gen_emails.add_typer(gen_emails_records, name="records")
1416
+ gen_emails_transactional = typer.Typer(
1417
+ cls=PrefixGroup, help="Manage emails transactional.", no_args_is_help=True
1418
+ )
1419
+ gen_emails.add_typer(gen_emails_transactional, name="transactional")
1420
+ gen_spams_clusters = typer.Typer(
1421
+ cls=PrefixGroup, help="Manage spams clusters.", no_args_is_help=True
1422
+ )
1423
+ gen_spams.add_typer(gen_spams_clusters, name="clusters")
1424
+ gen_spams_domain = typer.Typer(
1425
+ cls=PrefixGroup, help="Manage spams domain.", no_args_is_help=True
1426
+ )
1427
+ gen_spams.add_typer(gen_spams_domain, name="domain")
1428
+ gen_spams_transports = typer.Typer(
1429
+ cls=PrefixGroup, help="Manage spams transports.", no_args_is_help=True
1430
+ )
1431
+ gen_spams.add_typer(gen_spams_transports, name="transports")
1432
+ gen_containers_instances_volumes = typer.Typer(
1433
+ cls=PrefixGroup, help="Manage containers instances volumes.", no_args_is_help=True
1434
+ )
1435
+ gen_containers_instances.add_typer(gen_containers_instances_volumes, name="volumes")
1436
+ gen_databases_clusters_users = typer.Typer(
1437
+ cls=PrefixGroup, help="Manage databases clusters users.", no_args_is_help=True
1438
+ )
1439
+ gen_databases_clusters.add_typer(gen_databases_clusters_users, name="users")
1440
+ gen_emails_transactional_aliases = typer.Typer(
1441
+ cls=PrefixGroup, help="Manage emails transactional aliases.", no_args_is_help=True
1442
+ )
1443
+ gen_emails_transactional.add_typer(gen_emails_transactional_aliases, name="aliases")
1444
+ gen_emails_transactional_keys = typer.Typer(
1445
+ cls=PrefixGroup, help="Manage emails transactional keys.", no_args_is_help=True
1446
+ )
1447
+ gen_emails_transactional.add_typer(gen_emails_transactional_keys, name="keys")
1448
+ gen_emails_transactional_users = typer.Typer(
1449
+ cls=PrefixGroup, help="Manage emails transactional users.", no_args_is_help=True
1450
+ )
1451
+ gen_emails_transactional.add_typer(gen_emails_transactional_users, name="users")
1452
+ gen_spams_domain_dkim = typer.Typer(
1453
+ cls=PrefixGroup, help="Manage spams domain dkim.", no_args_is_help=True
1454
+ )
1455
+ gen_spams_domain.add_typer(gen_spams_domain_dkim, name="dkim")
1456
+
1457
+
1458
+ @gen_handles.command("create")
1459
+ def gen_handles_create(
1460
+ ctx: typer.Context,
1461
+ client_id: int | None = typer.Option(None),
1462
+ firstname: str | None = typer.Option(None),
1463
+ prefix: str | None = typer.Option(None),
1464
+ lastname: str | None = typer.Option(None),
1465
+ company: str | None = typer.Option(None),
1466
+ vat_id: str | None = typer.Option(None),
1467
+ email: str | None = typer.Option(None),
1468
+ phone_country_code: str | None = typer.Option(None),
1469
+ phone_area_code: str | None = typer.Option(None),
1470
+ phone_number: str | None = typer.Option(None),
1471
+ street: str | None = typer.Option(None),
1472
+ number: str | None = typer.Option(None),
1473
+ zipcode: str | None = typer.Option(None),
1474
+ city: str | None = typer.Option(None),
1475
+ region: str | None = typer.Option(None),
1476
+ country: str | None = typer.Option(None),
1477
+ output: OutputFormat | None = _OUTPUT_OPT,
1478
+ ) -> None:
1479
+ """Create a contact handle."""
1480
+
1481
+ _generated_call(
1482
+ ctx,
1483
+ "POST",
1484
+ "/handles",
1485
+ {},
1486
+ body={
1487
+ "client_id": client_id,
1488
+ "firstname": firstname,
1489
+ "prefix": prefix,
1490
+ "lastname": lastname,
1491
+ "company": company,
1492
+ "vat_id": vat_id,
1493
+ "email": email,
1494
+ "phone_country_code": phone_country_code,
1495
+ "phone_area_code": phone_area_code,
1496
+ "phone_number": phone_number,
1497
+ "street": street,
1498
+ "number": number,
1499
+ "zipcode": zipcode,
1500
+ "city": city,
1501
+ "region": region,
1502
+ "country": country,
1503
+ },
1504
+ output=output,
1505
+ )
1506
+
1507
+
1508
+ @gen_handles.command("list")
1509
+ def gen_handles_index(
1510
+ ctx: typer.Context,
1511
+ sort: str | None = _SORT_OPT,
1512
+ output: OutputFormat | None = _OUTPUT_OPT,
1513
+ ) -> None:
1514
+ """List your contact handles."""
1515
+
1516
+ _generated_call(
1517
+ ctx,
1518
+ "GET",
1519
+ "/handles",
1520
+ {},
1521
+ sort=sort,
1522
+ output=output,
1523
+ )
1524
+
1525
+
1526
+ @gen_handles.command("show")
1527
+ def gen_handles_show(
1528
+ ctx: typer.Context,
1529
+ handle_id: int = typer.Argument(..., metavar="HANDLE_ID"),
1530
+ output: OutputFormat | None = _OUTPUT_OPT,
1531
+ ) -> None:
1532
+ """Show one contact handle."""
1533
+
1534
+ _generated_call(
1535
+ ctx,
1536
+ "GET",
1537
+ "/handles/{handleId}",
1538
+ {"handleId": handle_id},
1539
+ output=output,
1540
+ )
1541
+
1542
+
1543
+ @gen_apps.command("list")
1544
+ def gen_services_apps_index(
1545
+ ctx: typer.Context,
1546
+ sort: str | None = _SORT_OPT,
1547
+ output: OutputFormat | None = _OUTPUT_OPT,
1548
+ ) -> None:
1549
+ """List apps."""
1550
+
1551
+ _generated_call(
1552
+ ctx,
1553
+ "GET",
1554
+ "/services/apps",
1555
+ {},
1556
+ sort=sort,
1557
+ output=output,
1558
+ )
1559
+
1560
+
1561
+ @gen_apps.command("show")
1562
+ def gen_services_apps_show(
1563
+ ctx: typer.Context,
1564
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1565
+ output: OutputFormat | None = _OUTPUT_OPT,
1566
+ ) -> None:
1567
+ """Show apps details."""
1568
+
1569
+ _generated_call(
1570
+ ctx,
1571
+ "GET",
1572
+ "/services/apps/{serviceId}",
1573
+ {"serviceId": service_id},
1574
+ output=output,
1575
+ )
1576
+
1577
+
1578
+ @gen_apps.command("state")
1579
+ def gen_services_apps_state(
1580
+ ctx: typer.Context,
1581
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1582
+ state: str = typer.Argument(..., metavar="STATE"),
1583
+ yes: bool = _YES_OPT,
1584
+ output: OutputFormat | None = _OUTPUT_OPT,
1585
+ ) -> None:
1586
+ """Change the state of an app service."""
1587
+
1588
+ _generated_call(
1589
+ ctx,
1590
+ "GET",
1591
+ "/services/apps/{serviceId}/state/{state}",
1592
+ {"serviceId": service_id, "state": state},
1593
+ confirm="State apps",
1594
+ yes=yes,
1595
+ output=output,
1596
+ )
1597
+
1598
+
1599
+ @gen_buckets.command("list")
1600
+ def gen_services_buckets_index(
1601
+ ctx: typer.Context,
1602
+ sort: str | None = _SORT_OPT,
1603
+ output: OutputFormat | None = _OUTPUT_OPT,
1604
+ ) -> None:
1605
+ """List buckets."""
1606
+
1607
+ _generated_call(
1608
+ ctx,
1609
+ "GET",
1610
+ "/services/buckets",
1611
+ {},
1612
+ sort=sort,
1613
+ output=output,
1614
+ )
1615
+
1616
+
1617
+ @gen_buckets.command("show")
1618
+ def gen_services_buckets_show(
1619
+ ctx: typer.Context,
1620
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1621
+ output: OutputFormat | None = _OUTPUT_OPT,
1622
+ ) -> None:
1623
+ """Show buckets details."""
1624
+
1625
+ _generated_call(
1626
+ ctx,
1627
+ "GET",
1628
+ "/services/buckets/{serviceId}",
1629
+ {"serviceId": service_id},
1630
+ output=output,
1631
+ )
1632
+
1633
+
1634
+ @gen_containers.command("deploy")
1635
+ def gen_services_containers_deploy(
1636
+ ctx: typer.Context,
1637
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1638
+ yes: bool = _YES_OPT,
1639
+ output: OutputFormat | None = _OUTPUT_OPT,
1640
+ ) -> None:
1641
+ """Deploy a container service."""
1642
+
1643
+ _generated_call(
1644
+ ctx,
1645
+ "GET",
1646
+ "/services/containers/{serviceId}/deploy",
1647
+ {"serviceId": service_id},
1648
+ confirm="Deploy containers",
1649
+ yes=yes,
1650
+ output=output,
1651
+ )
1652
+
1653
+
1654
+ @gen_containers.command("list")
1655
+ def gen_services_containers_index(
1656
+ ctx: typer.Context,
1657
+ sort: str | None = _SORT_OPT,
1658
+ output: OutputFormat | None = _OUTPUT_OPT,
1659
+ ) -> None:
1660
+ """List containers."""
1661
+
1662
+ _generated_call(
1663
+ ctx,
1664
+ "GET",
1665
+ "/services/containers",
1666
+ {},
1667
+ sort=sort,
1668
+ output=output,
1669
+ )
1670
+
1671
+
1672
+ @gen_containers_instances.command("deploy")
1673
+ def gen_services_containers_instances_deploy(
1674
+ ctx: typer.Context,
1675
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1676
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1677
+ yes: bool = _YES_OPT,
1678
+ output: OutputFormat | None = _OUTPUT_OPT,
1679
+ ) -> None:
1680
+ """Deploy a container instance."""
1681
+
1682
+ _generated_call(
1683
+ ctx,
1684
+ "GET",
1685
+ "/services/containers/{serviceId}/instances/{instanceId}/deploy",
1686
+ {"serviceId": service_id, "instanceId": instance_id},
1687
+ confirm="Deploy containers instances",
1688
+ yes=yes,
1689
+ output=output,
1690
+ )
1691
+
1692
+
1693
+ @gen_containers_instances.command("list")
1694
+ def gen_services_containers_instances_index(
1695
+ ctx: typer.Context,
1696
+ service_id: str = typer.Argument(..., metavar="SERVICE_ID"),
1697
+ sort: str | None = _SORT_OPT,
1698
+ output: OutputFormat | None = _OUTPUT_OPT,
1699
+ ) -> None:
1700
+ """List containers instances."""
1701
+
1702
+ _generated_call(
1703
+ ctx,
1704
+ "GET",
1705
+ "/services/containers/{serviceId}/instances",
1706
+ {"serviceId": service_id},
1707
+ sort=sort,
1708
+ output=output,
1709
+ )
1710
+
1711
+
1712
+ @gen_containers_instances.command("logs")
1713
+ def gen_services_containers_instances_logs(
1714
+ ctx: typer.Context,
1715
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1716
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1717
+ output: OutputFormat | None = _OUTPUT_OPT,
1718
+ ) -> None:
1719
+ """Show the logs of a container instance."""
1720
+
1721
+ _generated_call(
1722
+ ctx,
1723
+ "GET",
1724
+ "/services/containers/{serviceId}/instances/{instanceId}/logs",
1725
+ {"serviceId": service_id, "instanceId": instance_id},
1726
+ output=output,
1727
+ )
1728
+
1729
+
1730
+ @gen_containers_instances.command("show")
1731
+ def gen_services_containers_instances_show(
1732
+ ctx: typer.Context,
1733
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1734
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1735
+ output: OutputFormat | None = _OUTPUT_OPT,
1736
+ ) -> None:
1737
+ """Show containers instances details."""
1738
+
1739
+ _generated_call(
1740
+ ctx,
1741
+ "GET",
1742
+ "/services/containers/{serviceId}/instances/{instanceId}",
1743
+ {"serviceId": service_id, "instanceId": instance_id},
1744
+ output=output,
1745
+ )
1746
+
1747
+
1748
+ @gen_containers_instances.command("state")
1749
+ def gen_services_containers_instances_state(
1750
+ ctx: typer.Context,
1751
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1752
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1753
+ state: str = typer.Argument(..., metavar="STATE"),
1754
+ yes: bool = _YES_OPT,
1755
+ output: OutputFormat | None = _OUTPUT_OPT,
1756
+ ) -> None:
1757
+ """Change the state of a container instance."""
1758
+
1759
+ _generated_call(
1760
+ ctx,
1761
+ "GET",
1762
+ "/services/containers/{serviceId}/instances/{instanceId}/state/{state}",
1763
+ {"serviceId": service_id, "instanceId": instance_id, "state": state},
1764
+ confirm="State containers instances",
1765
+ yes=yes,
1766
+ output=output,
1767
+ )
1768
+
1769
+
1770
+ @gen_containers_instances.command("update")
1771
+ def gen_services_containers_instances_update(
1772
+ ctx: typer.Context,
1773
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1774
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1775
+ image: str | None = typer.Option(None),
1776
+ output: OutputFormat | None = _OUTPUT_OPT,
1777
+ ) -> None:
1778
+ """Update containers instances."""
1779
+
1780
+ _generated_call(
1781
+ ctx,
1782
+ "PUT",
1783
+ "/services/containers/{serviceId}/instances/{instanceId}",
1784
+ {"serviceId": service_id, "instanceId": instance_id},
1785
+ body={"image": image},
1786
+ output=output,
1787
+ )
1788
+
1789
+
1790
+ @gen_containers_instances_volumes.command("list")
1791
+ def gen_services_containers_instances_volumes_index(
1792
+ ctx: typer.Context,
1793
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1794
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1795
+ sort: str | None = _SORT_OPT,
1796
+ output: OutputFormat | None = _OUTPUT_OPT,
1797
+ ) -> None:
1798
+ """List containers instances volumes."""
1799
+
1800
+ _generated_call(
1801
+ ctx,
1802
+ "GET",
1803
+ "/services/containers/{serviceId}/instances/{instanceId}/volumes",
1804
+ {"serviceId": service_id, "instanceId": instance_id},
1805
+ sort=sort,
1806
+ output=output,
1807
+ )
1808
+
1809
+
1810
+ @gen_containers_instances_volumes.command("show")
1811
+ def gen_services_containers_instances_volumes_show(
1812
+ ctx: typer.Context,
1813
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1814
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1815
+ volume_id: int = typer.Argument(..., metavar="VOLUME_ID"),
1816
+ output: OutputFormat | None = _OUTPUT_OPT,
1817
+ ) -> None:
1818
+ """Show containers instances volumes details."""
1819
+
1820
+ _generated_call(
1821
+ ctx,
1822
+ "GET",
1823
+ "/services/containers/{serviceId}/instances/{instanceId}/volumes/{volumeId}",
1824
+ {"serviceId": service_id, "instanceId": instance_id, "volumeId": volume_id},
1825
+ output=output,
1826
+ )
1827
+
1828
+
1829
+ @gen_containers_instances_volumes.command("update")
1830
+ def gen_services_containers_instances_volumes_update(
1831
+ ctx: typer.Context,
1832
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1833
+ instance_id: int = typer.Argument(..., metavar="INSTANCE_ID"),
1834
+ volume_id: int = typer.Argument(..., metavar="VOLUME_ID"),
1835
+ path: str | None = typer.Option(None),
1836
+ uid: str | None = typer.Option(None),
1837
+ output: OutputFormat | None = _OUTPUT_OPT,
1838
+ ) -> None:
1839
+ """Update containers instances volumes."""
1840
+
1841
+ _generated_call(
1842
+ ctx,
1843
+ "PUT",
1844
+ "/services/containers/{serviceId}/instances/{instanceId}/volumes/{volumeId}",
1845
+ {"serviceId": service_id, "instanceId": instance_id, "volumeId": volume_id},
1846
+ body={"path": path, "uid": uid},
1847
+ output=output,
1848
+ )
1849
+
1850
+
1851
+ @gen_containers.command("show")
1852
+ def gen_services_containers_show(
1853
+ ctx: typer.Context,
1854
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1855
+ output: OutputFormat | None = _OUTPUT_OPT,
1856
+ ) -> None:
1857
+ """Show containers details."""
1858
+
1859
+ _generated_call(
1860
+ ctx,
1861
+ "GET",
1862
+ "/services/containers/{serviceId}",
1863
+ {"serviceId": service_id},
1864
+ output=output,
1865
+ )
1866
+
1867
+
1868
+ @gen_containers.command("update")
1869
+ def gen_services_containers_update(
1870
+ ctx: typer.Context,
1871
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
1872
+ registry_url: str | None = typer.Option(None),
1873
+ description: str | None = typer.Option(None),
1874
+ port: int | None = typer.Option(None),
1875
+ output: OutputFormat | None = _OUTPUT_OPT,
1876
+ ) -> None:
1877
+ """Update containers."""
1878
+
1879
+ _generated_call(
1880
+ ctx,
1881
+ "PUT",
1882
+ "/services/container/{serviceId}",
1883
+ {"serviceId": service_id},
1884
+ body={"registry_url": registry_url, "description": description, "port": port},
1885
+ output=output,
1886
+ )
1887
+
1888
+
1889
+ @gen_databases_clusters.command("list")
1890
+ def gen_services_databases_clusters_index(
1891
+ ctx: typer.Context,
1892
+ sort: str | None = _SORT_OPT,
1893
+ output: OutputFormat | None = _OUTPUT_OPT,
1894
+ ) -> None:
1895
+ """List databases clusters."""
1896
+
1897
+ _generated_call(
1898
+ ctx,
1899
+ "GET",
1900
+ "/services/databases/clusters",
1901
+ {},
1902
+ sort=sort,
1903
+ output=output,
1904
+ )
1905
+
1906
+
1907
+ @gen_databases_clusters.command("show")
1908
+ def gen_services_databases_clusters_show(
1909
+ ctx: typer.Context,
1910
+ cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
1911
+ output: OutputFormat | None = _OUTPUT_OPT,
1912
+ ) -> None:
1913
+ """Show databases clusters details."""
1914
+
1915
+ _generated_call(
1916
+ ctx,
1917
+ "GET",
1918
+ "/services/databases/clusters/{clusterId}",
1919
+ {"clusterId": cluster_id},
1920
+ output=output,
1921
+ )
1922
+
1923
+
1924
+ @gen_databases_clusters_users.command("list")
1925
+ def gen_services_databases_clusters_users_index(
1926
+ ctx: typer.Context,
1927
+ cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
1928
+ sort: str | None = _SORT_OPT,
1929
+ output: OutputFormat | None = _OUTPUT_OPT,
1930
+ ) -> None:
1931
+ """List databases clusters users."""
1932
+
1933
+ _generated_call(
1934
+ ctx,
1935
+ "GET",
1936
+ "/services/databases/clusters/{clusterId}/users",
1937
+ {"clusterId": cluster_id},
1938
+ sort=sort,
1939
+ output=output,
1940
+ )
1941
+
1942
+
1943
+ @gen_databases_clusters_users.command("show")
1944
+ def gen_services_databases_clusters_users_show(
1945
+ ctx: typer.Context,
1946
+ cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
1947
+ user_id: int = typer.Argument(..., metavar="USER_ID"),
1948
+ output: OutputFormat | None = _OUTPUT_OPT,
1949
+ ) -> None:
1950
+ """Show databases clusters users details."""
1951
+
1952
+ _generated_call(
1953
+ ctx,
1954
+ "GET",
1955
+ "/services/databases/clusters/{clusterId}/users/{userId}",
1956
+ {"clusterId": cluster_id, "userId": user_id},
1957
+ output=output,
1958
+ )
1959
+
1960
+
1961
+ @gen_databases.command("list")
1962
+ def gen_services_databases_index(
1963
+ ctx: typer.Context,
1964
+ sort: str | None = _SORT_OPT,
1965
+ output: OutputFormat | None = _OUTPUT_OPT,
1966
+ ) -> None:
1967
+ """List databases."""
1968
+
1969
+ _generated_call(
1970
+ ctx,
1971
+ "GET",
1972
+ "/services/databases",
1973
+ {},
1974
+ sort=sort,
1975
+ output=output,
1976
+ )
1977
+
1978
+
1979
+ @gen_databases_permissions.command("create")
1980
+ def gen_services_databases_permissions_create(
1981
+ ctx: typer.Context,
1982
+ service_id: str = typer.Argument(..., metavar="SERVICE_ID"),
1983
+ user_id: str | None = typer.Option(None),
1984
+ permissions: list[str] | None = typer.Option(None),
1985
+ output: OutputFormat | None = _OUTPUT_OPT,
1986
+ ) -> None:
1987
+ """Create databases permissions."""
1988
+
1989
+ _generated_call(
1990
+ ctx,
1991
+ "POST",
1992
+ "/services/databases/{serviceId}/permissions",
1993
+ {"serviceId": service_id},
1994
+ body={"user_id": user_id, "permissions": permissions},
1995
+ output=output,
1996
+ )
1997
+
1998
+
1999
+ @gen_databases.command("show")
2000
+ def gen_services_databases_show(
2001
+ ctx: typer.Context,
2002
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2003
+ output: OutputFormat | None = _OUTPUT_OPT,
2004
+ ) -> None:
2005
+ """Show databases details."""
2006
+
2007
+ _generated_call(
2008
+ ctx,
2009
+ "GET",
2010
+ "/services/databases/{serviceId}",
2011
+ {"serviceId": service_id},
2012
+ output=output,
2013
+ )
2014
+
2015
+
2016
+ @gen_databases.command("create")
2017
+ def gen_services_databases_store(
2018
+ ctx: typer.Context,
2019
+ name: str | None = typer.Option(None),
2020
+ client_id: int | None = typer.Option(None),
2021
+ cluster_id: int | None = typer.Option(None),
2022
+ output: OutputFormat | None = _OUTPUT_OPT,
2023
+ ) -> None:
2024
+ """Create databases."""
2025
+
2026
+ _generated_call(
2027
+ ctx,
2028
+ "POST",
2029
+ "/services/databases",
2030
+ {},
2031
+ body={"name": name, "client_id": client_id, "cluster_id": cluster_id},
2032
+ output=output,
2033
+ )
2034
+
2035
+
2036
+ @gen_databases_users.command("create")
2037
+ def gen_services_databases_users_store(
2038
+ ctx: typer.Context,
2039
+ cluster_id: str = typer.Argument(..., metavar="CLUSTER_ID"),
2040
+ username: str | None = typer.Option(None),
2041
+ password: str | None = typer.Option(None),
2042
+ client_id: int | None = typer.Option(None),
2043
+ output: OutputFormat | None = _OUTPUT_OPT,
2044
+ ) -> None:
2045
+ """Create databases users."""
2046
+
2047
+ _generated_call(
2048
+ ctx,
2049
+ "POST",
2050
+ "/services/databases/clusters/{clusterId}/users",
2051
+ {"clusterId": cluster_id},
2052
+ body={"username": username, "password": password, "client_id": client_id},
2053
+ output=output,
2054
+ )
2055
+
2056
+
2057
+ @gen_databases_users.command("update")
2058
+ def gen_services_databases_users_update(
2059
+ ctx: typer.Context,
2060
+ cluster_id: str = typer.Argument(..., metavar="CLUSTER_ID"),
2061
+ user_id: str = typer.Argument(..., metavar="USER_ID"),
2062
+ password: str | None = typer.Option(None),
2063
+ output: OutputFormat | None = _OUTPUT_OPT,
2064
+ ) -> None:
2065
+ """Update databases users."""
2066
+
2067
+ _generated_call(
2068
+ ctx,
2069
+ "PUT",
2070
+ "/services/databases/clusters/{clusterId}/users/{userId}",
2071
+ {"clusterId": cluster_id, "userId": user_id},
2072
+ body={"password": password},
2073
+ output=output,
2074
+ )
2075
+
2076
+
2077
+ @gen_domains.command("create")
2078
+ def gen_services_domains_create(
2079
+ ctx: typer.Context,
2080
+ nameserver_group: str | None = typer.Option(None),
2081
+ domain: str | None = typer.Option(None),
2082
+ client_id: int | None = typer.Option(None),
2083
+ type: str | None = typer.Option(None),
2084
+ token: str | None = typer.Option(None),
2085
+ handle_id_owner: int | None = typer.Option(None),
2086
+ handle_id_administrative: int | None = typer.Option(None),
2087
+ handle_id_technical: int | None = typer.Option(None),
2088
+ output: OutputFormat | None = _OUTPUT_OPT,
2089
+ ) -> None:
2090
+ """Create domains."""
2091
+
2092
+ _generated_call(
2093
+ ctx,
2094
+ "POST",
2095
+ "/services/domains",
2096
+ {},
2097
+ body={
2098
+ "nameserver_group": nameserver_group,
2099
+ "domain": domain,
2100
+ "client_id": client_id,
2101
+ "type": type,
2102
+ "token": token,
2103
+ "handle_id_owner": handle_id_owner,
2104
+ "handle_id_administrative": handle_id_administrative,
2105
+ "handle_id_technical": handle_id_technical,
2106
+ },
2107
+ output=output,
2108
+ )
2109
+
2110
+
2111
+ @gen_domains.command("list")
2112
+ def gen_services_domains_index(
2113
+ ctx: typer.Context,
2114
+ sort: str | None = _SORT_OPT,
2115
+ output: OutputFormat | None = _OUTPUT_OPT,
2116
+ ) -> None:
2117
+ """List domains."""
2118
+
2119
+ _generated_call(
2120
+ ctx,
2121
+ "GET",
2122
+ "/services/domains",
2123
+ {},
2124
+ sort=sort,
2125
+ output=output,
2126
+ )
2127
+
2128
+
2129
+ @gen_domains.command("show")
2130
+ def gen_services_domains_show(
2131
+ ctx: typer.Context,
2132
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2133
+ output: OutputFormat | None = _OUTPUT_OPT,
2134
+ ) -> None:
2135
+ """Show domains details."""
2136
+
2137
+ _generated_call(
2138
+ ctx,
2139
+ "GET",
2140
+ "/services/domains/{serviceId}",
2141
+ {"serviceId": service_id},
2142
+ output=output,
2143
+ )
2144
+
2145
+
2146
+ @gen_emails_aliases.command("create")
2147
+ def gen_services_emails_aliases_create(
2148
+ ctx: typer.Context,
2149
+ service_id: str = typer.Argument(..., metavar="SERVICE_ID"),
2150
+ emailaddress: str | None = typer.Option(None),
2151
+ alias: str | None = typer.Option(None),
2152
+ output: OutputFormat | None = _OUTPUT_OPT,
2153
+ ) -> None:
2154
+ """Create emails aliases."""
2155
+
2156
+ _generated_call(
2157
+ ctx,
2158
+ "POST",
2159
+ "/services/emails/{serviceId}/aliases",
2160
+ {"serviceId": service_id},
2161
+ body={"emailaddress": emailaddress, "alias": alias},
2162
+ output=output,
2163
+ )
2164
+
2165
+
2166
+ @gen_emails_aliases.command("delete")
2167
+ def gen_services_emails_aliases_delete(
2168
+ ctx: typer.Context,
2169
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2170
+ alias_id: int = typer.Argument(..., metavar="ALIAS_ID"),
2171
+ yes: bool = _YES_OPT,
2172
+ output: OutputFormat | None = _OUTPUT_OPT,
2173
+ ) -> None:
2174
+ """Delete emails aliases."""
2175
+
2176
+ _generated_call(
2177
+ ctx,
2178
+ "DELETE",
2179
+ "/services/emails/{serviceId}/aliases/{aliasId}",
2180
+ {"serviceId": service_id, "aliasId": alias_id},
2181
+ confirm="Delete emails aliases",
2182
+ yes=yes,
2183
+ output=output,
2184
+ )
2185
+
2186
+
2187
+ @gen_emails_aliases.command("list")
2188
+ def gen_services_emails_aliases_index(
2189
+ ctx: typer.Context,
2190
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2191
+ sort: str | None = _SORT_OPT,
2192
+ output: OutputFormat | None = _OUTPUT_OPT,
2193
+ ) -> None:
2194
+ """List emails aliases."""
2195
+
2196
+ _generated_call(
2197
+ ctx,
2198
+ "GET",
2199
+ "/services/emails/{serviceId}/aliases",
2200
+ {"serviceId": service_id},
2201
+ sort=sort,
2202
+ output=output,
2203
+ )
2204
+
2205
+
2206
+ @gen_emails_aliases.command("show")
2207
+ def gen_services_emails_aliases_show(
2208
+ ctx: typer.Context,
2209
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2210
+ alias_id: int = typer.Argument(..., metavar="ALIAS_ID"),
2211
+ output: OutputFormat | None = _OUTPUT_OPT,
2212
+ ) -> None:
2213
+ """Show emails aliases details."""
2214
+
2215
+ _generated_call(
2216
+ ctx,
2217
+ "GET",
2218
+ "/services/emails/{serviceId}/aliases/{aliasId}",
2219
+ {"serviceId": service_id, "aliasId": alias_id},
2220
+ output=output,
2221
+ )
2222
+
2223
+
2224
+ @gen_emails.command("create")
2225
+ def gen_services_emails_create(
2226
+ ctx: typer.Context,
2227
+ domain: str | None = typer.Option(None),
2228
+ client_id: int | None = typer.Option(None),
2229
+ type: str | None = typer.Option(None),
2230
+ return_path_domain: str | None = typer.Option(None),
2231
+ relay_hosts: list[str] | None = typer.Option(None),
2232
+ delivery_rate: int | None = typer.Option(None),
2233
+ output: OutputFormat | None = _OUTPUT_OPT,
2234
+ ) -> None:
2235
+ """Create emails."""
2236
+
2237
+ _generated_call(
2238
+ ctx,
2239
+ "POST",
2240
+ "/services/emails",
2241
+ {},
2242
+ body={
2243
+ "domain": domain,
2244
+ "client_id": client_id,
2245
+ "type": type,
2246
+ "return_path_domain": return_path_domain,
2247
+ "relay_hosts": relay_hosts,
2248
+ "delivery_rate": delivery_rate,
2249
+ },
2250
+ output=output,
2251
+ )
2252
+
2253
+
2254
+ @gen_emails.command("delete")
2255
+ def gen_services_emails_delete(
2256
+ ctx: typer.Context,
2257
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2258
+ yes: bool = _YES_OPT,
2259
+ output: OutputFormat | None = _OUTPUT_OPT,
2260
+ ) -> None:
2261
+ """Delete emails."""
2262
+
2263
+ _generated_call(
2264
+ ctx,
2265
+ "DELETE",
2266
+ "/services/emails/{serviceId}",
2267
+ {"serviceId": service_id},
2268
+ confirm="Delete emails",
2269
+ yes=yes,
2270
+ output=output,
2271
+ )
2272
+
2273
+
2274
+ @gen_emails_dkim.command("show")
2275
+ def gen_services_emails_dkim_show(
2276
+ ctx: typer.Context,
2277
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2278
+ output: OutputFormat | None = _OUTPUT_OPT,
2279
+ ) -> None:
2280
+ """Show the DKIM configuration of an email service."""
2281
+
2282
+ _generated_call(
2283
+ ctx,
2284
+ "GET",
2285
+ "/services/emails/{serviceId}/dkim",
2286
+ {"serviceId": service_id},
2287
+ output=output,
2288
+ )
2289
+
2290
+
2291
+ @gen_emails_forwardings.command("create")
2292
+ def gen_services_emails_forwardings_create(
2293
+ ctx: typer.Context,
2294
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2295
+ emailaddress: str | None = typer.Option(None),
2296
+ target: str | None = typer.Option(None),
2297
+ output: OutputFormat | None = _OUTPUT_OPT,
2298
+ ) -> None:
2299
+ """Create emails forwardings."""
2300
+
2301
+ _generated_call(
2302
+ ctx,
2303
+ "POST",
2304
+ "/services/emails/{serviceId}/forwardings",
2305
+ {"serviceId": service_id},
2306
+ body={"emailaddress": emailaddress, "target": target},
2307
+ output=output,
2308
+ )
2309
+
2310
+
2311
+ @gen_emails_forwardings.command("delete")
2312
+ def gen_services_emails_forwardings_delete(
2313
+ ctx: typer.Context,
2314
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2315
+ forwarding_id: int = typer.Argument(..., metavar="FORWARDING_ID"),
2316
+ yes: bool = _YES_OPT,
2317
+ output: OutputFormat | None = _OUTPUT_OPT,
2318
+ ) -> None:
2319
+ """Delete emails forwardings."""
2320
+
2321
+ _generated_call(
2322
+ ctx,
2323
+ "DELETE",
2324
+ "/services/emails/{serviceId}/forwardings/{forwardingId}",
2325
+ {"serviceId": service_id, "forwardingId": forwarding_id},
2326
+ confirm="Delete emails forwardings",
2327
+ yes=yes,
2328
+ output=output,
2329
+ )
2330
+
2331
+
2332
+ @gen_emails_forwardings.command("list")
2333
+ def gen_services_emails_forwardings_index(
2334
+ ctx: typer.Context,
2335
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2336
+ sort: str | None = _SORT_OPT,
2337
+ output: OutputFormat | None = _OUTPUT_OPT,
2338
+ ) -> None:
2339
+ """List emails forwardings."""
2340
+
2341
+ _generated_call(
2342
+ ctx,
2343
+ "GET",
2344
+ "/services/emails/{serviceId}/forwardings",
2345
+ {"serviceId": service_id},
2346
+ sort=sort,
2347
+ output=output,
2348
+ )
2349
+
2350
+
2351
+ @gen_emails_forwardings.command("show")
2352
+ def gen_services_emails_forwardings_show(
2353
+ ctx: typer.Context,
2354
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2355
+ forwarding_id: int = typer.Argument(..., metavar="FORWARDING_ID"),
2356
+ output: OutputFormat | None = _OUTPUT_OPT,
2357
+ ) -> None:
2358
+ """Show emails forwardings details."""
2359
+
2360
+ _generated_call(
2361
+ ctx,
2362
+ "GET",
2363
+ "/services/emails/{serviceId}/forwardings/{forwardingId}",
2364
+ {"serviceId": service_id, "forwardingId": forwarding_id},
2365
+ output=output,
2366
+ )
2367
+
2368
+
2369
+ @gen_emails.command("list")
2370
+ def gen_services_emails_index(
2371
+ ctx: typer.Context,
2372
+ sort: str | None = _SORT_OPT,
2373
+ output: OutputFormat | None = _OUTPUT_OPT,
2374
+ ) -> None:
2375
+ """List emails."""
2376
+
2377
+ _generated_call(
2378
+ ctx,
2379
+ "GET",
2380
+ "/services/emails",
2381
+ {},
2382
+ sort=sort,
2383
+ output=output,
2384
+ )
2385
+
2386
+
2387
+ @gen_emails_mailboxes.command("create")
2388
+ def gen_services_emails_mailboxes_create(
2389
+ ctx: typer.Context,
2390
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2391
+ emailaddress: str | None = typer.Option(None),
2392
+ password: str | None = typer.Option(None),
2393
+ output: OutputFormat | None = _OUTPUT_OPT,
2394
+ ) -> None:
2395
+ """Create emails mailboxes."""
2396
+
2397
+ _generated_call(
2398
+ ctx,
2399
+ "POST",
2400
+ "/services/emails/{serviceId}/mailboxes",
2401
+ {"serviceId": service_id},
2402
+ body={"emailaddress": emailaddress, "password": password},
2403
+ output=output,
2404
+ )
2405
+
2406
+
2407
+ @gen_emails_mailboxes.command("delete")
2408
+ def gen_services_emails_mailboxes_delete(
2409
+ ctx: typer.Context,
2410
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2411
+ emailaddress: str = typer.Argument(..., metavar="EMAILADDRESS"),
2412
+ yes: bool = _YES_OPT,
2413
+ output: OutputFormat | None = _OUTPUT_OPT,
2414
+ ) -> None:
2415
+ """Delete emails mailboxes."""
2416
+
2417
+ _generated_call(
2418
+ ctx,
2419
+ "DELETE",
2420
+ "/services/emails/{serviceId}/mailboxes/{emailaddress}",
2421
+ {"serviceId": service_id, "emailaddress": emailaddress},
2422
+ confirm="Delete emails mailboxes",
2423
+ yes=yes,
2424
+ output=output,
2425
+ )
2426
+
2427
+
2428
+ @gen_emails_mailboxes.command("list")
2429
+ def gen_services_emails_mailboxes_index(
2430
+ ctx: typer.Context,
2431
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2432
+ sort: str | None = _SORT_OPT,
2433
+ output: OutputFormat | None = _OUTPUT_OPT,
2434
+ ) -> None:
2435
+ """List emails mailboxes."""
2436
+
2437
+ _generated_call(
2438
+ ctx,
2439
+ "GET",
2440
+ "/services/emails/{serviceId}/mailboxes",
2441
+ {"serviceId": service_id},
2442
+ sort=sort,
2443
+ output=output,
2444
+ )
2445
+
2446
+
2447
+ @gen_emails_mailboxes.command("show")
2448
+ def gen_services_emails_mailboxes_show(
2449
+ ctx: typer.Context,
2450
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2451
+ emailaddress: str = typer.Argument(..., metavar="EMAILADDRESS"),
2452
+ output: OutputFormat | None = _OUTPUT_OPT,
2453
+ ) -> None:
2454
+ """Show emails mailboxes details."""
2455
+
2456
+ _generated_call(
2457
+ ctx,
2458
+ "GET",
2459
+ "/services/emails/{serviceId}/mailboxes/{emailaddress}",
2460
+ {"serviceId": service_id, "emailaddress": emailaddress},
2461
+ output=output,
2462
+ )
2463
+
2464
+
2465
+ @gen_emails_mailboxes.command("update")
2466
+ def gen_services_emails_mailboxes_update(
2467
+ ctx: typer.Context,
2468
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2469
+ emailaddress: str = typer.Argument(..., metavar="EMAILADDRESS"),
2470
+ password: str | None = typer.Option(None),
2471
+ output: OutputFormat | None = _OUTPUT_OPT,
2472
+ ) -> None:
2473
+ """Update emails mailboxes."""
2474
+
2475
+ _generated_call(
2476
+ ctx,
2477
+ "PUT",
2478
+ "/services/emails/{serviceId}/mailboxes/{emailaddress}",
2479
+ {"serviceId": service_id, "emailaddress": emailaddress},
2480
+ body={"password": password},
2481
+ output=output,
2482
+ )
2483
+
2484
+
2485
+ @gen_emails_records.command("list")
2486
+ def gen_services_emails_records_index(
2487
+ ctx: typer.Context,
2488
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2489
+ sort: str | None = _SORT_OPT,
2490
+ output: OutputFormat | None = _OUTPUT_OPT,
2491
+ ) -> None:
2492
+ """List the DNS records an email service needs."""
2493
+
2494
+ _generated_call(
2495
+ ctx,
2496
+ "GET",
2497
+ "/services/emails/{serviceId}/records",
2498
+ {"serviceId": service_id},
2499
+ sort=sort,
2500
+ output=output,
2501
+ )
2502
+
2503
+
2504
+ @gen_emails.command("show")
2505
+ def gen_services_emails_show(
2506
+ ctx: typer.Context,
2507
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2508
+ output: OutputFormat | None = _OUTPUT_OPT,
2509
+ ) -> None:
2510
+ """Show emails details."""
2511
+
2512
+ _generated_call(
2513
+ ctx,
2514
+ "GET",
2515
+ "/services/emails/{serviceId}",
2516
+ {"serviceId": service_id},
2517
+ output=output,
2518
+ )
2519
+
2520
+
2521
+ @gen_emails_transactional_aliases.command("create")
2522
+ def gen_services_emails_transactional_aliases_create(
2523
+ ctx: typer.Context,
2524
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2525
+ domain: str | None = typer.Option(None),
2526
+ output: OutputFormat | None = _OUTPUT_OPT,
2527
+ ) -> None:
2528
+ """Create emails transactional aliases."""
2529
+
2530
+ _generated_call(
2531
+ ctx,
2532
+ "POST",
2533
+ "/services/emails/{serviceId}/transactional/aliases",
2534
+ {"serviceId": service_id},
2535
+ body={"domain": domain},
2536
+ output=output,
2537
+ )
2538
+
2539
+
2540
+ @gen_emails_transactional_aliases.command("delete")
2541
+ def gen_services_emails_transactional_aliases_delete(
2542
+ ctx: typer.Context,
2543
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2544
+ alias_id: int = typer.Argument(..., metavar="ALIAS_ID"),
2545
+ yes: bool = _YES_OPT,
2546
+ output: OutputFormat | None = _OUTPUT_OPT,
2547
+ ) -> None:
2548
+ """Delete emails transactional aliases."""
2549
+
2550
+ _generated_call(
2551
+ ctx,
2552
+ "DELETE",
2553
+ "/services/emails/{serviceId}/transactional/aliases/{aliasId}",
2554
+ {"serviceId": service_id, "aliasId": alias_id},
2555
+ confirm="Delete emails transactional aliases",
2556
+ yes=yes,
2557
+ output=output,
2558
+ )
2559
+
2560
+
2561
+ @gen_emails_transactional_aliases.command("list")
2562
+ def gen_services_emails_transactional_aliases_index(
2563
+ ctx: typer.Context,
2564
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2565
+ sort: str | None = _SORT_OPT,
2566
+ output: OutputFormat | None = _OUTPUT_OPT,
2567
+ ) -> None:
2568
+ """List emails transactional aliases."""
2569
+
2570
+ _generated_call(
2571
+ ctx,
2572
+ "GET",
2573
+ "/services/emails/{serviceId}/transactional/aliases",
2574
+ {"serviceId": service_id},
2575
+ sort=sort,
2576
+ output=output,
2577
+ )
2578
+
2579
+
2580
+ @gen_emails_transactional_keys.command("create")
2581
+ def gen_services_emails_transactional_keys_create(
2582
+ ctx: typer.Context,
2583
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2584
+ name: str | None = typer.Option(None),
2585
+ key: str | None = typer.Option(None),
2586
+ active: bool | None = typer.Option(None),
2587
+ output: OutputFormat | None = _OUTPUT_OPT,
2588
+ ) -> None:
2589
+ """Create emails transactional keys."""
2590
+
2591
+ _generated_call(
2592
+ ctx,
2593
+ "POST",
2594
+ "/services/emails/{serviceId}/transactional/keys",
2595
+ {"serviceId": service_id},
2596
+ body={"name": name, "key": key, "active": active},
2597
+ output=output,
2598
+ )
2599
+
2600
+
2601
+ @gen_emails_transactional_keys.command("delete")
2602
+ def gen_services_emails_transactional_keys_delete(
2603
+ ctx: typer.Context,
2604
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2605
+ key_id: int = typer.Argument(..., metavar="KEY_ID"),
2606
+ yes: bool = _YES_OPT,
2607
+ output: OutputFormat | None = _OUTPUT_OPT,
2608
+ ) -> None:
2609
+ """Delete emails transactional keys."""
2610
+
2611
+ _generated_call(
2612
+ ctx,
2613
+ "DELETE",
2614
+ "/services/emails/{serviceId}/transactional/keys/{keyId}",
2615
+ {"serviceId": service_id, "keyId": key_id},
2616
+ confirm="Delete emails transactional keys",
2617
+ yes=yes,
2618
+ output=output,
2619
+ )
2620
+
2621
+
2622
+ @gen_emails_transactional_keys.command("list")
2623
+ def gen_services_emails_transactional_keys_index(
2624
+ ctx: typer.Context,
2625
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2626
+ sort: str | None = _SORT_OPT,
2627
+ output: OutputFormat | None = _OUTPUT_OPT,
2628
+ ) -> None:
2629
+ """List emails transactional keys."""
2630
+
2631
+ _generated_call(
2632
+ ctx,
2633
+ "GET",
2634
+ "/services/emails/{serviceId}/transactional/keys",
2635
+ {"serviceId": service_id},
2636
+ sort=sort,
2637
+ output=output,
2638
+ )
2639
+
2640
+
2641
+ @gen_emails_transactional_keys.command("show")
2642
+ def gen_services_emails_transactional_keys_show(
2643
+ ctx: typer.Context,
2644
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2645
+ key_id: int = typer.Argument(..., metavar="KEY_ID"),
2646
+ output: OutputFormat | None = _OUTPUT_OPT,
2647
+ ) -> None:
2648
+ """Show emails transactional keys details."""
2649
+
2650
+ _generated_call(
2651
+ ctx,
2652
+ "GET",
2653
+ "/services/emails/{serviceId}/transactional/keys/{keyId}",
2654
+ {"serviceId": service_id, "keyId": key_id},
2655
+ output=output,
2656
+ )
2657
+
2658
+
2659
+ @gen_emails_transactional_keys.command("update")
2660
+ def gen_services_emails_transactional_keys_update(
2661
+ ctx: typer.Context,
2662
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2663
+ key_id: int = typer.Argument(..., metavar="KEY_ID"),
2664
+ name: str | None = typer.Option(None),
2665
+ key: str | None = typer.Option(None),
2666
+ active: bool | None = typer.Option(None),
2667
+ output: OutputFormat | None = _OUTPUT_OPT,
2668
+ ) -> None:
2669
+ """Update emails transactional keys."""
2670
+
2671
+ _generated_call(
2672
+ ctx,
2673
+ "PUT",
2674
+ "/services/emails/{serviceId}/transactional/keys/{keyId}",
2675
+ {"serviceId": service_id, "keyId": key_id},
2676
+ body={"name": name, "key": key, "active": active},
2677
+ output=output,
2678
+ )
2679
+
2680
+
2681
+ @gen_emails_transactional_users.command("create")
2682
+ def gen_services_emails_transactional_users_create(
2683
+ ctx: typer.Context,
2684
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2685
+ emailaddress: str | None = typer.Option(None),
2686
+ password: str | None = typer.Option(None),
2687
+ delivery_mode: str | None = typer.Option(None),
2688
+ output: OutputFormat | None = _OUTPUT_OPT,
2689
+ ) -> None:
2690
+ """Create emails transactional users."""
2691
+
2692
+ _generated_call(
2693
+ ctx,
2694
+ "POST",
2695
+ "/services/emails/{serviceId}/transactional/users",
2696
+ {"serviceId": service_id},
2697
+ body={
2698
+ "emailaddress": emailaddress,
2699
+ "password": password,
2700
+ "delivery_mode": delivery_mode,
2701
+ },
2702
+ output=output,
2703
+ )
2704
+
2705
+
2706
+ @gen_emails_transactional_users.command("delete")
2707
+ def gen_services_emails_transactional_users_delete(
2708
+ ctx: typer.Context,
2709
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2710
+ user_id: int = typer.Argument(..., metavar="USER_ID"),
2711
+ yes: bool = _YES_OPT,
2712
+ output: OutputFormat | None = _OUTPUT_OPT,
2713
+ ) -> None:
2714
+ """Delete emails transactional users."""
2715
+
2716
+ _generated_call(
2717
+ ctx,
2718
+ "DELETE",
2719
+ "/services/emails/{serviceId}/transactional/users/{userId}",
2720
+ {"serviceId": service_id, "userId": user_id},
2721
+ confirm="Delete emails transactional users",
2722
+ yes=yes,
2723
+ output=output,
2724
+ )
2725
+
2726
+
2727
+ @gen_emails_transactional_users.command("list")
2728
+ def gen_services_emails_transactional_users_index(
2729
+ ctx: typer.Context,
2730
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2731
+ sort: str | None = _SORT_OPT,
2732
+ output: OutputFormat | None = _OUTPUT_OPT,
2733
+ ) -> None:
2734
+ """List emails transactional users."""
2735
+
2736
+ _generated_call(
2737
+ ctx,
2738
+ "GET",
2739
+ "/services/emails/{serviceId}/transactional/users",
2740
+ {"serviceId": service_id},
2741
+ sort=sort,
2742
+ output=output,
2743
+ )
2744
+
2745
+
2746
+ @gen_emails_transactional_users.command("show")
2747
+ def gen_services_emails_transactional_users_show(
2748
+ ctx: typer.Context,
2749
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2750
+ user_id: int = typer.Argument(..., metavar="USER_ID"),
2751
+ output: OutputFormat | None = _OUTPUT_OPT,
2752
+ ) -> None:
2753
+ """Show emails transactional users details."""
2754
+
2755
+ _generated_call(
2756
+ ctx,
2757
+ "GET",
2758
+ "/services/emails/{serviceId}/transactional/users/{userId}",
2759
+ {"serviceId": service_id, "userId": user_id},
2760
+ output=output,
2761
+ )
2762
+
2763
+
2764
+ @gen_emails_transactional_users.command("update")
2765
+ def gen_services_emails_transactional_users_update(
2766
+ ctx: typer.Context,
2767
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2768
+ user_id: int = typer.Argument(..., metavar="USER_ID"),
2769
+ password: str | None = typer.Option(None),
2770
+ delivery_mode: str | None = typer.Option(None),
2771
+ output: OutputFormat | None = _OUTPUT_OPT,
2772
+ ) -> None:
2773
+ """Update emails transactional users."""
2774
+
2775
+ _generated_call(
2776
+ ctx,
2777
+ "PUT",
2778
+ "/services/emails/{serviceId}/transactional/users/{userId}",
2779
+ {"serviceId": service_id, "userId": user_id},
2780
+ body={"password": password, "delivery_mode": delivery_mode},
2781
+ output=output,
2782
+ )
2783
+
2784
+
2785
+ @gen_forwards.command("list")
2786
+ def gen_services_forwards_index(
2787
+ ctx: typer.Context,
2788
+ sort: str | None = _SORT_OPT,
2789
+ output: OutputFormat | None = _OUTPUT_OPT,
2790
+ ) -> None:
2791
+ """List forwards."""
2792
+
2793
+ _generated_call(
2794
+ ctx,
2795
+ "GET",
2796
+ "/services/forwards",
2797
+ {},
2798
+ sort=sort,
2799
+ output=output,
2800
+ )
2801
+
2802
+
2803
+ @gen_forwards.command("show")
2804
+ def gen_services_forwards_show(
2805
+ ctx: typer.Context,
2806
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2807
+ output: OutputFormat | None = _OUTPUT_OPT,
2808
+ ) -> None:
2809
+ """Show forwards details."""
2810
+
2811
+ _generated_call(
2812
+ ctx,
2813
+ "GET",
2814
+ "/services/forwards/{serviceId}",
2815
+ {"serviceId": service_id},
2816
+ output=output,
2817
+ )
2818
+
2819
+
2820
+ @gen_proxies.command("down")
2821
+ def gen_services_proxies_down(
2822
+ ctx: typer.Context,
2823
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2824
+ yes: bool = _YES_OPT,
2825
+ output: OutputFormat | None = _OUTPUT_OPT,
2826
+ ) -> None:
2827
+ """Bring a proxy down."""
2828
+
2829
+ _generated_call(
2830
+ ctx,
2831
+ "GET",
2832
+ "/services/proxies/{serviceId}/down",
2833
+ {"serviceId": service_id},
2834
+ confirm="Down proxies",
2835
+ yes=yes,
2836
+ output=output,
2837
+ )
2838
+
2839
+
2840
+ @gen_proxies.command("list")
2841
+ def gen_services_proxies_index(
2842
+ ctx: typer.Context,
2843
+ sort: str | None = _SORT_OPT,
2844
+ output: OutputFormat | None = _OUTPUT_OPT,
2845
+ ) -> None:
2846
+ """List proxies."""
2847
+
2848
+ _generated_call(
2849
+ ctx,
2850
+ "GET",
2851
+ "/services/proxies",
2852
+ {},
2853
+ sort=sort,
2854
+ output=output,
2855
+ )
2856
+
2857
+
2858
+ @gen_proxies.command("show")
2859
+ def gen_services_proxies_show(
2860
+ ctx: typer.Context,
2861
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2862
+ output: OutputFormat | None = _OUTPUT_OPT,
2863
+ ) -> None:
2864
+ """Show proxies details."""
2865
+
2866
+ _generated_call(
2867
+ ctx,
2868
+ "GET",
2869
+ "/services/proxies/{serviceId}",
2870
+ {"serviceId": service_id},
2871
+ output=output,
2872
+ )
2873
+
2874
+
2875
+ @gen_proxies.command("up")
2876
+ def gen_services_proxies_up(
2877
+ ctx: typer.Context,
2878
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2879
+ yes: bool = _YES_OPT,
2880
+ output: OutputFormat | None = _OUTPUT_OPT,
2881
+ ) -> None:
2882
+ """Bring a proxy up."""
2883
+
2884
+ _generated_call(
2885
+ ctx,
2886
+ "GET",
2887
+ "/services/proxies/{serviceId}/up",
2888
+ {"serviceId": service_id},
2889
+ confirm="Up proxies",
2890
+ yes=yes,
2891
+ output=output,
2892
+ )
2893
+
2894
+
2895
+ @gen_searches.command("list")
2896
+ def gen_services_searches_index(
2897
+ ctx: typer.Context,
2898
+ sort: str | None = _SORT_OPT,
2899
+ output: OutputFormat | None = _OUTPUT_OPT,
2900
+ ) -> None:
2901
+ """List searches."""
2902
+
2903
+ _generated_call(
2904
+ ctx,
2905
+ "GET",
2906
+ "/services/searches",
2907
+ {},
2908
+ sort=sort,
2909
+ output=output,
2910
+ )
2911
+
2912
+
2913
+ @gen_searches.command("show")
2914
+ def gen_services_searches_show(
2915
+ ctx: typer.Context,
2916
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2917
+ output: OutputFormat | None = _OUTPUT_OPT,
2918
+ ) -> None:
2919
+ """Show searches details."""
2920
+
2921
+ _generated_call(
2922
+ ctx,
2923
+ "GET",
2924
+ "/services/searches/{serviceId}",
2925
+ {"serviceId": service_id},
2926
+ output=output,
2927
+ )
2928
+
2929
+
2930
+ @gen_servers.command("list")
2931
+ def gen_services_servers_index(
2932
+ ctx: typer.Context,
2933
+ sort: str | None = _SORT_OPT,
2934
+ output: OutputFormat | None = _OUTPUT_OPT,
2935
+ ) -> None:
2936
+ """List servers."""
2937
+
2938
+ _generated_call(
2939
+ ctx,
2940
+ "GET",
2941
+ "/services/servers",
2942
+ {},
2943
+ sort=sort,
2944
+ output=output,
2945
+ )
2946
+
2947
+
2948
+ @gen_servers.command("show")
2949
+ def gen_services_servers_show(
2950
+ ctx: typer.Context,
2951
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2952
+ output: OutputFormat | None = _OUTPUT_OPT,
2953
+ ) -> None:
2954
+ """Show servers details."""
2955
+
2956
+ _generated_call(
2957
+ ctx,
2958
+ "GET",
2959
+ "/services/servers/{serviceId}",
2960
+ {"serviceId": service_id},
2961
+ output=output,
2962
+ )
2963
+
2964
+
2965
+ @gen_servers.command("state")
2966
+ def gen_services_servers_state(
2967
+ ctx: typer.Context,
2968
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
2969
+ state: str = typer.Argument(..., metavar="STATE"),
2970
+ yes: bool = _YES_OPT,
2971
+ output: OutputFormat | None = _OUTPUT_OPT,
2972
+ ) -> None:
2973
+ """Change the state of a server."""
2974
+
2975
+ _generated_call(
2976
+ ctx,
2977
+ "GET",
2978
+ "/services/servers/{serviceId}/state/{state}",
2979
+ {"serviceId": service_id, "state": state},
2980
+ confirm="State servers",
2981
+ yes=yes,
2982
+ output=output,
2983
+ )
2984
+
2985
+
2986
+ @gen_sites.command("list")
2987
+ def gen_services_sites_index(
2988
+ ctx: typer.Context,
2989
+ sort: str | None = _SORT_OPT,
2990
+ output: OutputFormat | None = _OUTPUT_OPT,
2991
+ ) -> None:
2992
+ """List sites."""
2993
+
2994
+ _generated_call(
2995
+ ctx,
2996
+ "GET",
2997
+ "/services/sites",
2998
+ {},
2999
+ sort=sort,
3000
+ output=output,
3001
+ )
3002
+
3003
+
3004
+ @gen_sites.command("show")
3005
+ def gen_services_sites_show(
3006
+ ctx: typer.Context,
3007
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3008
+ output: OutputFormat | None = _OUTPUT_OPT,
3009
+ ) -> None:
3010
+ """Show sites details."""
3011
+
3012
+ _generated_call(
3013
+ ctx,
3014
+ "GET",
3015
+ "/services/sites/{serviceId}",
3016
+ {"serviceId": service_id},
3017
+ output=output,
3018
+ )
3019
+
3020
+
3021
+ @gen_spams_clusters.command("list")
3022
+ def gen_services_spams_clusters_index(
3023
+ ctx: typer.Context,
3024
+ sort: str | None = _SORT_OPT,
3025
+ output: OutputFormat | None = _OUTPUT_OPT,
3026
+ ) -> None:
3027
+ """List spams clusters."""
3028
+
3029
+ _generated_call(
3030
+ ctx,
3031
+ "GET",
3032
+ "/services/spams/clusters",
3033
+ {},
3034
+ sort=sort,
3035
+ output=output,
3036
+ )
3037
+
3038
+
3039
+ @gen_spams_clusters.command("show")
3040
+ def gen_services_spams_clusters_show(
3041
+ ctx: typer.Context,
3042
+ cluster_id: int = typer.Argument(..., metavar="CLUSTER_ID"),
3043
+ output: OutputFormat | None = _OUTPUT_OPT,
3044
+ ) -> None:
3045
+ """Show spams clusters details."""
3046
+
3047
+ _generated_call(
3048
+ ctx,
3049
+ "GET",
3050
+ "/services/spams/clusters/{clusterId}",
3051
+ {"clusterId": cluster_id},
3052
+ output=output,
3053
+ )
3054
+
3055
+
3056
+ @gen_spams_domain.command("create")
3057
+ def gen_services_spams_domain_create(
3058
+ ctx: typer.Context,
3059
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3060
+ domain: str | None = typer.Option(None),
3061
+ output: OutputFormat | None = _OUTPUT_OPT,
3062
+ ) -> None:
3063
+ """Create spams domain."""
3064
+
3065
+ _generated_call(
3066
+ ctx,
3067
+ "POST",
3068
+ "/services/spams/{serviceId}/domains",
3069
+ {"serviceId": service_id},
3070
+ body={"domain": domain},
3071
+ output=output,
3072
+ )
3073
+
3074
+
3075
+ @gen_spams_domain.command("delete")
3076
+ def gen_services_spams_domain_delete(
3077
+ ctx: typer.Context,
3078
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3079
+ domain: str = typer.Argument(..., metavar="DOMAIN"),
3080
+ yes: bool = _YES_OPT,
3081
+ output: OutputFormat | None = _OUTPUT_OPT,
3082
+ ) -> None:
3083
+ """Delete spams domain."""
3084
+
3085
+ _generated_call(
3086
+ ctx,
3087
+ "DELETE",
3088
+ "/services/spams/{serviceId}/domains/{domain}",
3089
+ {"serviceId": service_id, "domain": domain},
3090
+ confirm="Delete spams domain",
3091
+ yes=yes,
3092
+ output=output,
3093
+ )
3094
+
3095
+
3096
+ @gen_spams_domain_dkim.command("disable")
3097
+ def gen_services_spams_domain_dkim_disable(
3098
+ ctx: typer.Context,
3099
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3100
+ domain: str = typer.Argument(..., metavar="DOMAIN"),
3101
+ yes: bool = _YES_OPT,
3102
+ output: OutputFormat | None = _OUTPUT_OPT,
3103
+ ) -> None:
3104
+ """Disable DKIM for a spam filter domain."""
3105
+
3106
+ _generated_call(
3107
+ ctx,
3108
+ "GET",
3109
+ "/services/spams/{serviceId}/domains/{domain}/dkim/disable",
3110
+ {"serviceId": service_id, "domain": domain},
3111
+ confirm="Disable spams domain dkim",
3112
+ yes=yes,
3113
+ output=output,
3114
+ )
3115
+
3116
+
3117
+ @gen_spams_domain_dkim.command("enable")
3118
+ def gen_services_spams_domain_dkim_enable(
3119
+ ctx: typer.Context,
3120
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3121
+ domain: str = typer.Argument(..., metavar="DOMAIN"),
3122
+ yes: bool = _YES_OPT,
3123
+ output: OutputFormat | None = _OUTPUT_OPT,
3124
+ ) -> None:
3125
+ """Enable DKIM for a spam filter domain."""
3126
+
3127
+ _generated_call(
3128
+ ctx,
3129
+ "GET",
3130
+ "/services/spams/{serviceId}/domains/{domain}/dkim/enable",
3131
+ {"serviceId": service_id, "domain": domain},
3132
+ confirm="Enable spams domain dkim",
3133
+ yes=yes,
3134
+ output=output,
3135
+ )
3136
+
3137
+
3138
+ @gen_spams_domain.command("list")
3139
+ def gen_services_spams_domain_index(
3140
+ ctx: typer.Context,
3141
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3142
+ sort: str | None = _SORT_OPT,
3143
+ output: OutputFormat | None = _OUTPUT_OPT,
3144
+ ) -> None:
3145
+ """List spams domain."""
3146
+
3147
+ _generated_call(
3148
+ ctx,
3149
+ "GET",
3150
+ "/services/spams/{serviceId}/domains",
3151
+ {"serviceId": service_id},
3152
+ sort=sort,
3153
+ output=output,
3154
+ )
3155
+
3156
+
3157
+ @gen_spams_domain.command("show")
3158
+ def gen_services_spams_domain_show(
3159
+ ctx: typer.Context,
3160
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3161
+ domain: str = typer.Argument(..., metavar="DOMAIN"),
3162
+ output: OutputFormat | None = _OUTPUT_OPT,
3163
+ ) -> None:
3164
+ """Show spams domain details."""
3165
+
3166
+ _generated_call(
3167
+ ctx,
3168
+ "GET",
3169
+ "/services/spams/{serviceId}/domains/{domain}",
3170
+ {"serviceId": service_id, "domain": domain},
3171
+ output=output,
3172
+ )
3173
+
3174
+
3175
+ @gen_spams.command("list")
3176
+ def gen_services_spams_index(
3177
+ ctx: typer.Context,
3178
+ sort: str | None = _SORT_OPT,
3179
+ output: OutputFormat | None = _OUTPUT_OPT,
3180
+ ) -> None:
3181
+ """List spams."""
3182
+
3183
+ _generated_call(
3184
+ ctx,
3185
+ "GET",
3186
+ "/services/spams",
3187
+ {},
3188
+ sort=sort,
3189
+ output=output,
3190
+ )
3191
+
3192
+
3193
+ @gen_spams.command("show")
3194
+ def gen_services_spams_show(
3195
+ ctx: typer.Context,
3196
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3197
+ output: OutputFormat | None = _OUTPUT_OPT,
3198
+ ) -> None:
3199
+ """Show spams details."""
3200
+
3201
+ _generated_call(
3202
+ ctx,
3203
+ "GET",
3204
+ "/services/spams/{serviceId}",
3205
+ {"serviceId": service_id},
3206
+ output=output,
3207
+ )
3208
+
3209
+
3210
+ @gen_spams_transports.command("create")
3211
+ def gen_services_spams_transports_create(
3212
+ ctx: typer.Context,
3213
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3214
+ domain: str | None = typer.Option(None),
3215
+ host: str | None = typer.Option(None),
3216
+ port: int | None = typer.Option(None),
3217
+ output: OutputFormat | None = _OUTPUT_OPT,
3218
+ ) -> None:
3219
+ """Create spams transports."""
3220
+
3221
+ _generated_call(
3222
+ ctx,
3223
+ "POST",
3224
+ "/services/spams/{serviceId}/transports",
3225
+ {"serviceId": service_id},
3226
+ body={"domain": domain, "host": host, "port": port},
3227
+ output=output,
3228
+ )
3229
+
3230
+
3231
+ @gen_spams_transports.command("delete")
3232
+ def gen_services_spams_transports_delete(
3233
+ ctx: typer.Context,
3234
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3235
+ domain: str = typer.Argument(..., metavar="DOMAIN"),
3236
+ yes: bool = _YES_OPT,
3237
+ output: OutputFormat | None = _OUTPUT_OPT,
3238
+ ) -> None:
3239
+ """Delete spams transports."""
3240
+
3241
+ _generated_call(
3242
+ ctx,
3243
+ "DELETE",
3244
+ "/services/spams/{serviceId}/transports/{domain}",
3245
+ {"serviceId": service_id, "domain": domain},
3246
+ confirm="Delete spams transports",
3247
+ yes=yes,
3248
+ output=output,
3249
+ )
3250
+
3251
+
3252
+ @gen_spams_transports.command("list")
3253
+ def gen_services_spams_transports_index(
3254
+ ctx: typer.Context,
3255
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3256
+ sort: str | None = _SORT_OPT,
3257
+ output: OutputFormat | None = _OUTPUT_OPT,
3258
+ ) -> None:
3259
+ """List spams transports."""
3260
+
3261
+ _generated_call(
3262
+ ctx,
3263
+ "GET",
3264
+ "/services/spams/{serviceId}/transports",
3265
+ {"serviceId": service_id},
3266
+ sort=sort,
3267
+ output=output,
3268
+ )
3269
+
3270
+
3271
+ @gen_spams_transports.command("show")
3272
+ def gen_services_spams_transports_show(
3273
+ ctx: typer.Context,
3274
+ service_id: int = typer.Argument(..., metavar="SERVICE_ID"),
3275
+ domain: str = typer.Argument(..., metavar="DOMAIN"),
3276
+ output: OutputFormat | None = _OUTPUT_OPT,
3277
+ ) -> None:
3278
+ """Show spams transports details."""
3279
+
3280
+ _generated_call(
3281
+ ctx,
3282
+ "GET",
3283
+ "/services/spams/{serviceId}/transports/{domain}",
3284
+ {"serviceId": service_id, "domain": domain},
3285
+ output=output,
3286
+ )
3287
+
3288
+
3289
+ # --- END GENERATED COMMANDS ---
3290
+
3291
+
3292
+ def run() -> None:
3293
+ app()
3294
+
3295
+
3296
+ if __name__ == "__main__":
3297
+ run()