direct-cli 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. direct_cli/__init__.py +14 -0
  2. direct_cli/api.py +94 -0
  3. direct_cli/auth.py +58 -0
  4. direct_cli/cli.py +85 -0
  5. direct_cli/commands/__init__.py +61 -0
  6. direct_cli/commands/adextensions.py +96 -0
  7. direct_cli/commands/adgroups.py +189 -0
  8. direct_cli/commands/adimages.py +63 -0
  9. direct_cli/commands/ads.py +306 -0
  10. direct_cli/commands/agencyclients.py +64 -0
  11. direct_cli/commands/audiencetargets.py +187 -0
  12. direct_cli/commands/bidmodifiers.py +110 -0
  13. direct_cli/commands/bids.py +108 -0
  14. direct_cli/commands/businesses.py +61 -0
  15. direct_cli/commands/campaigns.py +311 -0
  16. direct_cli/commands/changes.py +97 -0
  17. direct_cli/commands/clients.py +98 -0
  18. direct_cli/commands/creatives.py +68 -0
  19. direct_cli/commands/dictionaries.py +64 -0
  20. direct_cli/commands/dynamicads.py +104 -0
  21. direct_cli/commands/feeds.py +99 -0
  22. direct_cli/commands/keywordbids.py +111 -0
  23. direct_cli/commands/keywords.py +309 -0
  24. direct_cli/commands/keywordsresearch.py +71 -0
  25. direct_cli/commands/leads.py +65 -0
  26. direct_cli/commands/negativekeywordsharedsets.py +97 -0
  27. direct_cli/commands/reports.py +128 -0
  28. direct_cli/commands/retargeting.py +104 -0
  29. direct_cli/commands/sitelinks.py +92 -0
  30. direct_cli/commands/smartadtargets.py +104 -0
  31. direct_cli/commands/turbopages.py +97 -0
  32. direct_cli/commands/vcards.py +93 -0
  33. direct_cli/output.py +143 -0
  34. direct_cli/utils.py +120 -0
  35. direct_cli-0.0.0.dist-info/METADATA +393 -0
  36. direct_cli-0.0.0.dist-info/RECORD +39 -0
  37. direct_cli-0.0.0.dist-info/WHEEL +5 -0
  38. direct_cli-0.0.0.dist-info/entry_points.txt +2 -0
  39. direct_cli-0.0.0.dist-info/top_level.txt +1 -0
direct_cli/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ Direct CLI - Command-line interface for Yandex Direct API
3
+ """
4
+
5
+ __author__ = "Pavel Maksimov"
6
+ __email__ = "vur21@ya.ru"
7
+
8
+ # Version will be set by setuptools_scm
9
+ try:
10
+ from ._version import version as __version__
11
+ except ImportError:
12
+ __version__ = "unknown"
13
+
14
+ __all__ = ["__version__", "__author__", "__email__"]
direct_cli/api.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ API client wrapper for Direct CLI
3
+ """
4
+
5
+ from typing import Optional, Dict, Any, List
6
+
7
+ from tapi_yandex_direct import YandexDirect
8
+ from .auth import get_credentials
9
+
10
+
11
+ def create_client(
12
+ token: Optional[str] = None,
13
+ login: Optional[str] = None,
14
+ sandbox: bool = False,
15
+ ) -> YandexDirect:
16
+ """
17
+ Create YandexDirect client
18
+
19
+ Args:
20
+ token: API access token
21
+ login: Client login (for agency accounts)
22
+ sandbox: Use sandbox API
23
+
24
+ Returns:
25
+ YandexDirect client instance
26
+ """
27
+ final_token, final_login = get_credentials(token, login)
28
+
29
+ return YandexDirect(
30
+ access_token=final_token,
31
+ login=final_login,
32
+ is_sandbox=sandbox,
33
+ retry_if_exceeded_limit=True,
34
+ retries_if_server_error=5,
35
+ # Report settings
36
+ processing_mode="auto",
37
+ wait_report=True,
38
+ return_money_in_micros=False,
39
+ skip_report_header=True,
40
+ skip_column_header=False,
41
+ skip_report_summary=True,
42
+ )
43
+
44
+
45
+ def fetch_all_pages(
46
+ client: YandexDirect,
47
+ resource_name: str,
48
+ method: str = "get",
49
+ params: Dict[str, Any] = None,
50
+ field_names: Optional[List[str]] = None,
51
+ progress: bool = False,
52
+ ) -> List[Dict[str, Any]]:
53
+ """
54
+ Fetch all pages of results
55
+
56
+ Args:
57
+ client: YandexDirect client
58
+ resource_name: Resource name (e.g., 'campaigns', 'adgroups')
59
+ method: API method ('get')
60
+ params: Request params
61
+ field_names: Field names to include
62
+ progress: Show progress bar
63
+
64
+ Returns:
65
+ List of all items from all pages
66
+ """
67
+ if params is None:
68
+ params = {}
69
+
70
+ body = {
71
+ "method": method,
72
+ "params": params,
73
+ }
74
+
75
+ if field_names:
76
+ body["params"]["FieldNames"] = field_names
77
+
78
+ resource = getattr(client, resource_name)()
79
+ result = resource.post(data=body)
80
+
81
+ # Get items from first page
82
+ items = result().extract()
83
+
84
+ # Check if there are more pages
85
+ if hasattr(result, "iter_items") and callable(result().iter_items):
86
+ all_items = []
87
+
88
+ # Use iter_items for pagination
89
+ for item in result().iter_items():
90
+ all_items.append(item)
91
+
92
+ return all_items
93
+
94
+ return items if isinstance(items, list) else [items]
direct_cli/auth.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Authentication module for Direct CLI
3
+ """
4
+
5
+ import os
6
+ from typing import Optional, Tuple
7
+
8
+ try:
9
+ from dotenv import load_dotenv
10
+ except ImportError:
11
+ load_dotenv = lambda: None
12
+
13
+
14
+ def load_env_file(env_path: Optional[str] = None) -> None:
15
+ """Load environment variables from .env file"""
16
+ if load_dotenv:
17
+ if env_path:
18
+ load_dotenv(env_path)
19
+ else:
20
+ load_dotenv()
21
+
22
+
23
+ def get_credentials(
24
+ token: Optional[str] = None,
25
+ login: Optional[str] = None,
26
+ env_path: Optional[str] = None,
27
+ ) -> Tuple[str, Optional[str]]:
28
+ """
29
+ Get credentials with priority:
30
+ 1. Direct arguments
31
+ 2. Environment variables
32
+ 3. .env file
33
+
34
+ Args:
35
+ token: API access token
36
+ login: Client login (for agency accounts)
37
+ env_path: Path to .env file
38
+
39
+ Returns:
40
+ Tuple of (token, login)
41
+
42
+ Raises:
43
+ ValueError: If token is not provided
44
+ """
45
+ # Load .env file first
46
+ load_env_file(env_path)
47
+
48
+ # Priority: arguments > env vars
49
+ final_token = token or os.getenv("YANDEX_DIRECT_TOKEN")
50
+ final_login = login or os.getenv("YANDEX_DIRECT_LOGIN")
51
+
52
+ if not final_token:
53
+ raise ValueError(
54
+ "API token required. Set YANDEX_DIRECT_TOKEN environment variable "
55
+ "or create .env file, or use --token option."
56
+ )
57
+
58
+ return final_token, final_login
direct_cli/cli.py ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Direct CLI - Command-line interface for Yandex Direct API
4
+ """
5
+
6
+ import click
7
+ from dotenv import load_dotenv
8
+
9
+ from .commands.campaigns import campaigns
10
+ from .commands.adgroups import adgroups
11
+ from .commands.ads import ads
12
+ from .commands.keywords import keywords
13
+ from .commands.keywordbids import keywordbids
14
+ from .commands.bids import bids
15
+ from .commands.bidmodifiers import bidmodifiers
16
+ from .commands.audiencetargets import audiencetargets
17
+ from .commands.retargeting import retargeting
18
+ from .commands.creatives import creatives
19
+ from .commands.adimages import adimages
20
+ from .commands.adextensions import adextensions
21
+ from .commands.sitelinks import sitelinks
22
+ from .commands.vcards import vcards
23
+ from .commands.leads import leads
24
+ from .commands.clients import clients
25
+ from .commands.agencyclients import agencyclients
26
+ from .commands.dictionaries import dictionaries
27
+ from .commands.changes import changes
28
+ from .commands.reports import reports
29
+ from .commands.turbopages import turbopages
30
+ from .commands.negativekeywordsharedsets import negativekeywordsharedsets
31
+ from .commands.feeds import feeds
32
+ from .commands.smartadtargets import smartadtargets
33
+ from .commands.businesses import businesses
34
+ from .commands.keywordsresearch import keywordsresearch
35
+ from .commands.dynamicads import dynamicads
36
+
37
+ # Load .env file
38
+ load_dotenv()
39
+
40
+
41
+ @click.group()
42
+ @click.option("--token", envvar="YANDEX_DIRECT_TOKEN", help="API access token")
43
+ @click.option("--login", envvar="YANDEX_DIRECT_LOGIN", help="Client login")
44
+ @click.option("--sandbox", is_flag=True, help="Use sandbox API")
45
+ @click.pass_context
46
+ def cli(ctx, token, login, sandbox):
47
+ """Command-line interface for Yandex Direct API"""
48
+ ctx.ensure_object(dict)
49
+ ctx.obj["token"] = token
50
+ ctx.obj["login"] = login
51
+ ctx.obj["sandbox"] = sandbox
52
+
53
+
54
+ # Register all commands
55
+ cli.add_command(campaigns)
56
+ cli.add_command(adgroups)
57
+ cli.add_command(ads)
58
+ cli.add_command(keywords)
59
+ cli.add_command(keywordbids)
60
+ cli.add_command(bids)
61
+ cli.add_command(bidmodifiers)
62
+ cli.add_command(audiencetargets)
63
+ cli.add_command(retargeting)
64
+ cli.add_command(creatives)
65
+ cli.add_command(adimages)
66
+ cli.add_command(adextensions)
67
+ cli.add_command(sitelinks)
68
+ cli.add_command(vcards)
69
+ cli.add_command(leads)
70
+ cli.add_command(clients)
71
+ cli.add_command(agencyclients)
72
+ cli.add_command(dictionaries)
73
+ cli.add_command(changes)
74
+ cli.add_command(reports)
75
+ cli.add_command(turbopages)
76
+ cli.add_command(negativekeywordsharedsets)
77
+ cli.add_command(feeds)
78
+ cli.add_command(smartadtargets)
79
+ cli.add_command(businesses)
80
+ cli.add_command(keywordsresearch)
81
+ cli.add_command(dynamicads)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ cli()
@@ -0,0 +1,61 @@
1
+ """
2
+ Command handlers for Direct CLI
3
+ """
4
+
5
+ from .campaigns import campaigns
6
+ from .adgroups import adgroups
7
+ from .ads import ads
8
+ from .keywords import keywords
9
+ from .keywordbids import keywordbids
10
+ from .bids import bids
11
+ from .bidmodifiers import bidmodifiers
12
+ from .audiencetargets import audiencetargets
13
+ from .retargeting import retargeting
14
+ from .creatives import creatives
15
+ from .adimages import adimages
16
+ from .adextensions import adextensions
17
+ from .sitelinks import sitelinks
18
+ from .vcards import vcards
19
+ from .leads import leads
20
+ from .clients import clients
21
+ from .agencyclients import agencyclients
22
+ from .dictionaries import dictionaries
23
+ from .changes import changes
24
+ from .reports import reports
25
+ from .turbopages import turbopages
26
+ from .negativekeywordsharedsets import negativekeywordsharedsets
27
+ from .feeds import feeds
28
+ from .smartadtargets import smartadtargets
29
+ from .businesses import businesses
30
+ from .keywordsresearch import keywordsresearch
31
+ from .dynamicads import dynamicads
32
+
33
+ __all__ = [
34
+ "campaigns",
35
+ "adgroups",
36
+ "ads",
37
+ "keywords",
38
+ "keywordbids",
39
+ "bids",
40
+ "bidmodifiers",
41
+ "audiencetargets",
42
+ "retargeting",
43
+ "creatives",
44
+ "adimages",
45
+ "adextensions",
46
+ "sitelinks",
47
+ "vcards",
48
+ "leads",
49
+ "clients",
50
+ "agencyclients",
51
+ "dictionaries",
52
+ "changes",
53
+ "reports",
54
+ "turbopages",
55
+ "negativekeywordsharedsets",
56
+ "feeds",
57
+ "smartadtargets",
58
+ "businesses",
59
+ "keywordsresearch",
60
+ "dynamicads",
61
+ ]
@@ -0,0 +1,96 @@
1
+ """
2
+ AdExtensions commands
3
+ """
4
+
5
+ import json
6
+ import click
7
+
8
+ from ..api import create_client
9
+ from ..output import format_output, print_error
10
+ from ..utils import parse_ids
11
+
12
+
13
+ @click.group()
14
+ def adextensions():
15
+ """Manage ad extensions"""
16
+ pass
17
+
18
+
19
+ @adextensions.command()
20
+ @click.option("--ids", help="Comma-separated extension IDs")
21
+ @click.option("--types", help="Filter by types")
22
+ @click.option("--limit", type=int, help="Limit number of results")
23
+ @click.option("--fetch-all", is_flag=True, help="Fetch all pages")
24
+ @click.option("--format", "output_format", default="json", help="Output format")
25
+ @click.option("--output", help="Output file")
26
+ @click.option("--fields", help="Comma-separated field names")
27
+ @click.pass_context
28
+ def get(ctx, ids, types, limit, fetch_all, output_format, output, fields):
29
+ """Get ad extensions"""
30
+ try:
31
+ client = create_client(
32
+ token=ctx.obj.get("token"),
33
+ login=ctx.obj.get("login"),
34
+ sandbox=ctx.obj.get("sandbox"),
35
+ )
36
+
37
+ field_names = fields.split(",") if fields else ["Id", "Type", "Status"]
38
+
39
+ criteria = {}
40
+ if ids:
41
+ criteria["Ids"] = parse_ids(ids)
42
+ if types:
43
+ criteria["Types"] = types.split(",")
44
+
45
+ params = {"SelectionCriteria": criteria, "FieldNames": field_names}
46
+
47
+ if limit:
48
+ params["Page"] = {"Limit": limit}
49
+
50
+ body = {"method": "get", "params": params}
51
+
52
+ result = client.adextensions().post(data=body)
53
+
54
+ if fetch_all:
55
+ items = []
56
+ for item in result().iter_items():
57
+ items.append(item)
58
+ format_output(items, output_format, output)
59
+ else:
60
+ data = result().extract()
61
+ format_output(data, output_format, output)
62
+
63
+ except Exception as e:
64
+ print_error(str(e))
65
+ raise click.Abort()
66
+
67
+
68
+ @adextensions.command()
69
+ @click.option("--type", "ext_type", required=True, help="Extension type")
70
+ @click.option("--json", "extra_json", required=True, help="Extension data in JSON")
71
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
72
+ @click.pass_context
73
+ def add(ctx, ext_type, extra_json, dry_run):
74
+ """Add ad extension"""
75
+ try:
76
+ ext_data = {"Type": ext_type}
77
+ ext_data.update(json.loads(extra_json))
78
+
79
+ body = {"method": "add", "params": {"AdExtensions": [ext_data]}}
80
+
81
+ if dry_run:
82
+ format_output(body, "json", None)
83
+ return
84
+
85
+ client = create_client(
86
+ token=ctx.obj.get("token"),
87
+ login=ctx.obj.get("login"),
88
+ sandbox=ctx.obj.get("sandbox"),
89
+ )
90
+
91
+ result = client.adextensions().post(data=body)
92
+ format_output(result().extract(), "json", None)
93
+
94
+ except Exception as e:
95
+ print_error(str(e))
96
+ raise click.Abort()
@@ -0,0 +1,189 @@
1
+ """
2
+ Ad Groups commands
3
+ """
4
+
5
+ import json
6
+ import click
7
+
8
+ from ..api import create_client
9
+ from ..output import format_output, print_error
10
+ from ..utils import parse_ids, get_default_fields
11
+
12
+
13
+ @click.group()
14
+ def adgroups():
15
+ """Manage ad groups"""
16
+ pass
17
+
18
+
19
+ @adgroups.command()
20
+ @click.option("--ids", help="Comma-separated ad group IDs")
21
+ @click.option("--campaign-ids", help="Comma-separated campaign IDs")
22
+ @click.option("--status", help="Filter by status")
23
+ @click.option("--types", help="Filter by types")
24
+ @click.option("--limit", type=int, help="Limit number of results")
25
+ @click.option("--fetch-all", is_flag=True, help="Fetch all pages")
26
+ @click.option("--format", "output_format", default="json", help="Output format")
27
+ @click.option("--output", help="Output file")
28
+ @click.option("--fields", help="Comma-separated field names")
29
+ @click.pass_context
30
+ def get(
31
+ ctx,
32
+ ids,
33
+ campaign_ids,
34
+ status,
35
+ types,
36
+ limit,
37
+ fetch_all,
38
+ output_format,
39
+ output,
40
+ fields,
41
+ ):
42
+ """Get ad groups"""
43
+ try:
44
+ client = create_client(
45
+ token=ctx.obj.get("token"),
46
+ login=ctx.obj.get("login"),
47
+ sandbox=ctx.obj.get("sandbox"),
48
+ )
49
+
50
+ field_names = fields.split(",") if fields else get_default_fields("adgroups")
51
+
52
+ criteria = {}
53
+ if ids:
54
+ criteria["Ids"] = parse_ids(ids)
55
+ if campaign_ids:
56
+ criteria["CampaignIds"] = parse_ids(campaign_ids)
57
+ if status:
58
+ criteria["Statuses"] = [status]
59
+ if types:
60
+ criteria["Types"] = types.split(",")
61
+
62
+ params = {"SelectionCriteria": criteria, "FieldNames": field_names}
63
+
64
+ if limit:
65
+ params["Page"] = {"Limit": limit}
66
+
67
+ body = {"method": "get", "params": params}
68
+
69
+ result = client.adgroups().post(data=body)
70
+
71
+ if fetch_all:
72
+ items = []
73
+ for item in result().iter_items():
74
+ items.append(item)
75
+ format_output(items, output_format, output)
76
+ else:
77
+ data = result().extract()
78
+ format_output(data, output_format, output)
79
+
80
+ except Exception as e:
81
+ print_error(str(e))
82
+ raise click.Abort()
83
+
84
+
85
+ @adgroups.command()
86
+ @click.option("--name", required=True, help="Ad group name")
87
+ @click.option("--campaign-id", required=True, type=int, help="Campaign ID")
88
+ @click.option("--type", "group_type", default="TEXT_AD_GROUP", help="Ad group type")
89
+ @click.option("--region-ids", help="Comma-separated region IDs")
90
+ @click.option("--json", "extra_json", help="Additional JSON parameters")
91
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
92
+ @click.pass_context
93
+ def add(ctx, name, campaign_id, group_type, region_ids, extra_json, dry_run):
94
+ """Add new ad group"""
95
+ try:
96
+ adgroup_data = {"Name": name, "CampaignId": campaign_id, "Type": group_type}
97
+
98
+ if region_ids:
99
+ adgroup_data["RegionIds"] = parse_ids(region_ids)
100
+
101
+ if extra_json:
102
+ extra = json.loads(extra_json)
103
+ adgroup_data.update(extra)
104
+
105
+ body = {"method": "add", "params": {"AdGroups": [adgroup_data]}}
106
+
107
+ if dry_run:
108
+ format_output(body, "json", None)
109
+ return
110
+
111
+ client = create_client(
112
+ token=ctx.obj.get("token"),
113
+ login=ctx.obj.get("login"),
114
+ sandbox=ctx.obj.get("sandbox"),
115
+ )
116
+
117
+ result = client.adgroups().post(data=body)
118
+ format_output(result().extract(), "json", None)
119
+
120
+ except Exception as e:
121
+ print_error(str(e))
122
+ raise click.Abort()
123
+
124
+
125
+ @adgroups.command()
126
+ @click.option("--id", "adgroup_id", required=True, type=int, help="Ad group ID")
127
+ @click.option("--name", help="New ad group name")
128
+ @click.option("--status", help="New status")
129
+ @click.option("--json", "extra_json", help="Additional JSON parameters")
130
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
131
+ @click.pass_context
132
+ def update(ctx, adgroup_id, name, status, extra_json, dry_run):
133
+ """Update ad group"""
134
+ try:
135
+ adgroup_data = {"Id": adgroup_id}
136
+
137
+ if name:
138
+ adgroup_data["Name"] = name
139
+
140
+ if status:
141
+ adgroup_data["Status"] = status
142
+
143
+ if extra_json:
144
+ extra = json.loads(extra_json)
145
+ adgroup_data.update(extra)
146
+
147
+ body = {"method": "update", "params": {"AdGroups": [adgroup_data]}}
148
+
149
+ if dry_run:
150
+ format_output(body, "json", None)
151
+ return
152
+
153
+ client = create_client(
154
+ token=ctx.obj.get("token"),
155
+ login=ctx.obj.get("login"),
156
+ sandbox=ctx.obj.get("sandbox"),
157
+ )
158
+
159
+ result = client.adgroups().post(data=body)
160
+ format_output(result().extract(), "json", None)
161
+
162
+ except Exception as e:
163
+ print_error(str(e))
164
+ raise click.Abort()
165
+
166
+
167
+ @adgroups.command()
168
+ @click.option("--id", "adgroup_id", required=True, type=int, help="Ad group ID")
169
+ @click.pass_context
170
+ def delete(ctx, adgroup_id):
171
+ """Delete ad group"""
172
+ try:
173
+ client = create_client(
174
+ token=ctx.obj.get("token"),
175
+ login=ctx.obj.get("login"),
176
+ sandbox=ctx.obj.get("sandbox"),
177
+ )
178
+
179
+ body = {
180
+ "method": "delete",
181
+ "params": {"SelectionCriteria": {"Ids": [adgroup_id]}},
182
+ }
183
+
184
+ result = client.adgroups().post(data=body)
185
+ format_output(result().extract(), "json", None)
186
+
187
+ except Exception as e:
188
+ print_error(str(e))
189
+ raise click.Abort()
@@ -0,0 +1,63 @@
1
+ """
2
+ AdImages commands
3
+ """
4
+
5
+ import click
6
+
7
+ from ..api import create_client
8
+ from ..output import format_output, print_error
9
+ from ..utils import parse_ids
10
+
11
+
12
+ @click.group()
13
+ def adimages():
14
+ """Manage ad images"""
15
+ pass
16
+
17
+
18
+ @adimages.command()
19
+ @click.option("--ids", help="Comma-separated image IDs")
20
+ @click.option("--limit", type=int, help="Limit number of results")
21
+ @click.option("--fetch-all", is_flag=True, help="Fetch all pages")
22
+ @click.option("--format", "output_format", default="json", help="Output format")
23
+ @click.option("--output", help="Output file")
24
+ @click.option("--fields", help="Comma-separated field names")
25
+ @click.pass_context
26
+ def get(ctx, ids, limit, fetch_all, output_format, output, fields):
27
+ """Get ad images"""
28
+ try:
29
+ client = create_client(
30
+ token=ctx.obj.get("token"),
31
+ login=ctx.obj.get("login"),
32
+ sandbox=ctx.obj.get("sandbox"),
33
+ )
34
+
35
+ field_names = (
36
+ fields.split(",") if fields else ["Id", "Name", "Status", "AdImageHash"]
37
+ )
38
+
39
+ criteria = {}
40
+ if ids:
41
+ criteria["Ids"] = parse_ids(ids)
42
+
43
+ params = {"SelectionCriteria": criteria, "FieldNames": field_names}
44
+
45
+ if limit:
46
+ params["Page"] = {"Limit": limit}
47
+
48
+ body = {"method": "get", "params": params}
49
+
50
+ result = client.adimages().post(data=body)
51
+
52
+ if fetch_all:
53
+ items = []
54
+ for item in result().iter_items():
55
+ items.append(item)
56
+ format_output(items, output_format, output)
57
+ else:
58
+ data = result().extract()
59
+ format_output(data, output_format, output)
60
+
61
+ except Exception as e:
62
+ print_error(str(e))
63
+ raise click.Abort()