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
@@ -0,0 +1,309 @@
1
+ """
2
+ Keywords 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 keywords():
15
+ """Manage keywords"""
16
+ pass
17
+
18
+
19
+ @keywords.command()
20
+ @click.option("--ids", help="Comma-separated keyword IDs")
21
+ @click.option("--adgroup-ids", help="Comma-separated ad group IDs")
22
+ @click.option("--campaign-ids", help="Comma-separated campaign IDs")
23
+ @click.option("--status", help="Filter by status")
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
+ adgroup_ids,
34
+ campaign_ids,
35
+ status,
36
+ limit,
37
+ fetch_all,
38
+ output_format,
39
+ output,
40
+ fields,
41
+ ):
42
+ """Get keywords"""
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("keywords")
51
+
52
+ criteria = {}
53
+ if ids:
54
+ criteria["Ids"] = parse_ids(ids)
55
+ if adgroup_ids:
56
+ criteria["AdGroupIds"] = parse_ids(adgroup_ids)
57
+ if campaign_ids:
58
+ criteria["CampaignIds"] = parse_ids(campaign_ids)
59
+ if status:
60
+ criteria["Statuses"] = [status]
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.keywords().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
+ @keywords.command()
86
+ @click.option("--adgroup-id", required=True, type=int, help="Ad group ID")
87
+ @click.option("--keyword", required=True, help="Keyword text")
88
+ @click.option("--bid", type=float, help="Search bid")
89
+ @click.option("--context-bid", type=float, help="Context bid")
90
+ @click.option("--user-param-1", help="User parameter 1")
91
+ @click.option("--user-param-2", help="User parameter 2")
92
+ @click.option("--json", "extra_json", help="Additional JSON parameters")
93
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
94
+ @click.pass_context
95
+ def add(
96
+ ctx,
97
+ adgroup_id,
98
+ keyword,
99
+ bid,
100
+ context_bid,
101
+ user_param_1,
102
+ user_param_2,
103
+ extra_json,
104
+ dry_run,
105
+ ):
106
+ """Add new keyword"""
107
+ try:
108
+ keyword_data = {"AdGroupId": adgroup_id, "Keyword": keyword}
109
+
110
+ if bid:
111
+ keyword_data["Bid"] = int(bid * 1000000)
112
+ if context_bid:
113
+ keyword_data["ContextBid"] = int(context_bid * 1000000)
114
+ if user_param_1:
115
+ keyword_data["UserParam1"] = user_param_1
116
+ if user_param_2:
117
+ keyword_data["UserParam2"] = user_param_2
118
+
119
+ if extra_json:
120
+ extra = json.loads(extra_json)
121
+ keyword_data.update(extra)
122
+
123
+ body = {"method": "add", "params": {"Keywords": [keyword_data]}}
124
+
125
+ if dry_run:
126
+ format_output(body, "json", None)
127
+ return
128
+
129
+ client = create_client(
130
+ token=ctx.obj.get("token"),
131
+ login=ctx.obj.get("login"),
132
+ sandbox=ctx.obj.get("sandbox"),
133
+ )
134
+
135
+ result = client.keywords().post(data=body)
136
+ format_output(result().extract(), "json", None)
137
+
138
+ except Exception as e:
139
+ print_error(str(e))
140
+ raise click.Abort()
141
+
142
+
143
+ @keywords.command()
144
+ @click.option("--id", "keyword_id", required=True, type=int, help="Keyword ID")
145
+ @click.option("--bid", type=float, help="Search bid")
146
+ @click.option("--context-bid", type=float, help="Context bid")
147
+ @click.option("--status", help="New status")
148
+ @click.option("--json", "extra_json", help="Additional JSON parameters")
149
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
150
+ @click.pass_context
151
+ def update(ctx, keyword_id, bid, context_bid, status, extra_json, dry_run):
152
+ """Update keyword"""
153
+ try:
154
+ keyword_data = {"Id": keyword_id}
155
+
156
+ if bid:
157
+ keyword_data["Bid"] = int(bid * 1000000)
158
+ if context_bid:
159
+ keyword_data["ContextBid"] = int(context_bid * 1000000)
160
+ if status:
161
+ keyword_data["Status"] = status
162
+
163
+ if extra_json:
164
+ extra = json.loads(extra_json)
165
+ keyword_data.update(extra)
166
+
167
+ body = {"method": "update", "params": {"Keywords": [keyword_data]}}
168
+
169
+ if dry_run:
170
+ format_output(body, "json", None)
171
+ return
172
+
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
+ result = client.keywords().post(data=body)
180
+ format_output(result().extract(), "json", None)
181
+
182
+ except Exception as e:
183
+ print_error(str(e))
184
+ raise click.Abort()
185
+
186
+
187
+ @keywords.command()
188
+ @click.option("--id", "keyword_id", required=True, type=int, help="Keyword ID")
189
+ @click.pass_context
190
+ def delete(ctx, keyword_id):
191
+ """Delete keyword"""
192
+ try:
193
+ client = create_client(
194
+ token=ctx.obj.get("token"),
195
+ login=ctx.obj.get("login"),
196
+ sandbox=ctx.obj.get("sandbox"),
197
+ )
198
+
199
+ body = {
200
+ "method": "delete",
201
+ "params": {"SelectionCriteria": {"Ids": [keyword_id]}},
202
+ }
203
+
204
+ result = client.keywords().post(data=body)
205
+ format_output(result().extract(), "json", None)
206
+
207
+ except Exception as e:
208
+ print_error(str(e))
209
+ raise click.Abort()
210
+
211
+
212
+ @keywords.command()
213
+ @click.option("--id", "keyword_id", required=True, type=int, help="Keyword ID")
214
+ @click.pass_context
215
+ def archive(ctx, keyword_id):
216
+ """Archive keyword"""
217
+ try:
218
+ client = create_client(
219
+ token=ctx.obj.get("token"),
220
+ login=ctx.obj.get("login"),
221
+ sandbox=ctx.obj.get("sandbox"),
222
+ )
223
+
224
+ body = {
225
+ "method": "archive",
226
+ "params": {"SelectionCriteria": {"Ids": [keyword_id]}},
227
+ }
228
+
229
+ result = client.keywords().post(data=body)
230
+ format_output(result().extract(), "json", None)
231
+
232
+ except Exception as e:
233
+ print_error(str(e))
234
+ raise click.Abort()
235
+
236
+
237
+ @keywords.command()
238
+ @click.option("--id", "keyword_id", required=True, type=int, help="Keyword ID")
239
+ @click.pass_context
240
+ def unarchive(ctx, keyword_id):
241
+ """Unarchive keyword"""
242
+ try:
243
+ client = create_client(
244
+ token=ctx.obj.get("token"),
245
+ login=ctx.obj.get("login"),
246
+ sandbox=ctx.obj.get("sandbox"),
247
+ )
248
+
249
+ body = {
250
+ "method": "unarchive",
251
+ "params": {"SelectionCriteria": {"Ids": [keyword_id]}},
252
+ }
253
+
254
+ result = client.keywords().post(data=body)
255
+ format_output(result().extract(), "json", None)
256
+
257
+ except Exception as e:
258
+ print_error(str(e))
259
+ raise click.Abort()
260
+
261
+
262
+ @keywords.command()
263
+ @click.option("--id", "keyword_id", required=True, type=int, help="Keyword ID")
264
+ @click.pass_context
265
+ def suspend(ctx, keyword_id):
266
+ """Suspend keyword"""
267
+ try:
268
+ client = create_client(
269
+ token=ctx.obj.get("token"),
270
+ login=ctx.obj.get("login"),
271
+ sandbox=ctx.obj.get("sandbox"),
272
+ )
273
+
274
+ body = {
275
+ "method": "suspend",
276
+ "params": {"SelectionCriteria": {"Ids": [keyword_id]}},
277
+ }
278
+
279
+ result = client.keywords().post(data=body)
280
+ format_output(result().extract(), "json", None)
281
+
282
+ except Exception as e:
283
+ print_error(str(e))
284
+ raise click.Abort()
285
+
286
+
287
+ @keywords.command()
288
+ @click.option("--id", "keyword_id", required=True, type=int, help="Keyword ID")
289
+ @click.pass_context
290
+ def resume(ctx, keyword_id):
291
+ """Resume keyword"""
292
+ try:
293
+ client = create_client(
294
+ token=ctx.obj.get("token"),
295
+ login=ctx.obj.get("login"),
296
+ sandbox=ctx.obj.get("sandbox"),
297
+ )
298
+
299
+ body = {
300
+ "method": "resume",
301
+ "params": {"SelectionCriteria": {"Ids": [keyword_id]}},
302
+ }
303
+
304
+ result = client.keywords().post(data=body)
305
+ format_output(result().extract(), "json", None)
306
+
307
+ except Exception as e:
308
+ print_error(str(e))
309
+ raise click.Abort()
@@ -0,0 +1,71 @@
1
+ """
2
+ KeywordsResearch commands
3
+ """
4
+
5
+ import click
6
+
7
+ from ..api import create_client
8
+ from ..output import format_output, print_error
9
+
10
+
11
+ @click.group()
12
+ def keywordsresearch():
13
+ """Keyword research tools"""
14
+ pass
15
+
16
+
17
+ @keywordsresearch.command()
18
+ @click.option("--keywords", required=True, help="Comma-separated keywords")
19
+ @click.option("--limit", type=int, help="Limit number of results")
20
+ @click.option("--format", "output_format", default="json", help="Output format")
21
+ @click.option("--output", help="Output file")
22
+ @click.pass_context
23
+ def get(ctx, keywords, limit, output_format, output):
24
+ """Get keywords research data"""
25
+ try:
26
+ client = create_client(
27
+ token=ctx.obj.get("token"),
28
+ login=ctx.obj.get("login"),
29
+ sandbox=ctx.obj.get("sandbox"),
30
+ )
31
+
32
+ params = {"Keywords": [k.strip() for k in keywords.split(",")]}
33
+
34
+ if limit:
35
+ params["Limit"] = limit
36
+
37
+ body = {"method": "get", "params": params}
38
+
39
+ result = client.keywordsresearch().post(data=body)
40
+ format_output(result.data, output_format, output)
41
+
42
+ except Exception as e:
43
+ print_error(str(e))
44
+ raise click.Abort()
45
+
46
+
47
+ @keywordsresearch.command()
48
+ @click.option("--keywords", required=True, help="Comma-separated keywords")
49
+ @click.option("--format", "output_format", default="json", help="Output format")
50
+ @click.option("--output", help="Output file")
51
+ @click.pass_context
52
+ def has_search_volume(ctx, keywords, output_format, output):
53
+ """Check if keywords have search volume"""
54
+ try:
55
+ client = create_client(
56
+ token=ctx.obj.get("token"),
57
+ login=ctx.obj.get("login"),
58
+ sandbox=ctx.obj.get("sandbox"),
59
+ )
60
+
61
+ body = {
62
+ "method": "HasSearchVolume",
63
+ "params": {"Keywords": [k.strip() for k in keywords.split(",")]},
64
+ }
65
+
66
+ result = client.keywordsresearch().post(data=body)
67
+ format_output(result.data, output_format, output)
68
+
69
+ except Exception as e:
70
+ print_error(str(e))
71
+ raise click.Abort()
@@ -0,0 +1,65 @@
1
+ """
2
+ Leads 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 leads():
14
+ """Manage leads"""
15
+ pass
16
+
17
+
18
+ @leads.command()
19
+ @click.option("--campaign-ids", help="Comma-separated campaign 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, campaign_ids, limit, fetch_all, output_format, output, fields):
27
+ """Get leads"""
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(",")
37
+ if fields
38
+ else ["Date", "LeadId", "CampaignId", "AdGroupId", "AdId"]
39
+ )
40
+
41
+ criteria = {}
42
+ if campaign_ids:
43
+ criteria["CampaignIds"] = parse_ids(campaign_ids)
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.leads().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()
@@ -0,0 +1,97 @@
1
+ """
2
+ NegativeKeywordSharedSets 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 negativekeywordsharedsets():
15
+ """Manage negative keyword shared sets"""
16
+ pass
17
+
18
+
19
+ @negativekeywordsharedsets.command()
20
+ @click.option("--ids", help="Comma-separated set IDs")
21
+ @click.option("--limit", type=int, help="Limit number of results")
22
+ @click.option("--fetch-all", is_flag=True, help="Fetch all pages")
23
+ @click.option("--format", "output_format", default="json", help="Output format")
24
+ @click.option("--output", help="Output file")
25
+ @click.option("--fields", help="Comma-separated field names")
26
+ @click.pass_context
27
+ def get(ctx, ids, limit, fetch_all, output_format, output, fields):
28
+ """Get negative keyword shared sets"""
29
+ try:
30
+ client = create_client(
31
+ token=ctx.obj.get("token"),
32
+ login=ctx.obj.get("login"),
33
+ sandbox=ctx.obj.get("sandbox"),
34
+ )
35
+
36
+ field_names = (
37
+ fields.split(",") if fields else ["Id", "Name", "NegativeKeywords"]
38
+ )
39
+
40
+ criteria = {}
41
+ if ids:
42
+ criteria["Ids"] = parse_ids(ids)
43
+
44
+ params = {"SelectionCriteria": criteria, "FieldNames": field_names}
45
+
46
+ if limit:
47
+ params["Page"] = {"Limit": limit}
48
+
49
+ body = {"method": "get", "params": params}
50
+
51
+ result = client.negativekeywordsharedsets().post(data=body)
52
+
53
+ if fetch_all:
54
+ items = []
55
+ for item in result().iter_items():
56
+ items.append(item)
57
+ format_output(items, output_format, output)
58
+ else:
59
+ data = result().extract()
60
+ format_output(data, output_format, output)
61
+
62
+ except Exception as e:
63
+ print_error(str(e))
64
+ raise click.Abort()
65
+
66
+
67
+ @negativekeywordsharedsets.command()
68
+ @click.option("--name", required=True, help="Set name")
69
+ @click.option("--keywords", required=True, help="Comma-separated negative keywords")
70
+ @click.option("--dry-run", is_flag=True, help="Show request without sending")
71
+ @click.pass_context
72
+ def add(ctx, name, keywords, dry_run):
73
+ """Add negative keyword shared set"""
74
+ try:
75
+ set_data = {
76
+ "Name": name,
77
+ "NegativeKeywords": [{"Keyword": k.strip()} for k in keywords.split(",")],
78
+ }
79
+
80
+ body = {"method": "add", "params": {"NegativeKeywordSharedSets": [set_data]}}
81
+
82
+ if dry_run:
83
+ format_output(body, "json", None)
84
+ return
85
+
86
+ client = create_client(
87
+ token=ctx.obj.get("token"),
88
+ login=ctx.obj.get("login"),
89
+ sandbox=ctx.obj.get("sandbox"),
90
+ )
91
+
92
+ result = client.negativekeywordsharedsets().post(data=body)
93
+ format_output(result().extract(), "json", None)
94
+
95
+ except Exception as e:
96
+ print_error(str(e))
97
+ raise click.Abort()
@@ -0,0 +1,128 @@
1
+ """
2
+ Reports commands
3
+ """
4
+
5
+ import json
6
+ import click
7
+ from datetime import datetime
8
+
9
+ from ..api import create_client
10
+ from ..output import format_output, print_error
11
+
12
+
13
+ REPORT_TYPES = [
14
+ "CAMPAIGN_PERFORMANCE_REPORT",
15
+ "ADGROUP_PERFORMANCE_REPORT",
16
+ "AD_PERFORMANCE_REPORT",
17
+ "CRITERIA_PERFORMANCE_REPORT",
18
+ "CUSTOM_REPORT",
19
+ "REACH_AND_FREQUENCY_CAMPAIGN_REPORT",
20
+ "SEARCH_QUERY_PERFORMANCE_REPORT",
21
+ ]
22
+
23
+
24
+ @click.group()
25
+ def reports():
26
+ """Generate and manage reports"""
27
+ pass
28
+
29
+
30
+ @reports.command()
31
+ @click.option(
32
+ "--type",
33
+ "report_type",
34
+ required=True,
35
+ help="Report type (CAMPAIGN_PERFORMANCE_REPORT, etc.)",
36
+ )
37
+ @click.option("--from", "date_from", required=True, help="Start date (YYYY-MM-DD)")
38
+ @click.option("--to", "date_to", required=True, help="End date (YYYY-MM-DD)")
39
+ @click.option("--name", required=True, help="Report name")
40
+ @click.option("--fields", required=True, help="Comma-separated field names")
41
+ @click.option("--campaign-ids", help="Comma-separated campaign IDs")
42
+ @click.option("--adgroup-ids", help="Comma-separated ad group IDs")
43
+ @click.option(
44
+ "--format",
45
+ "output_format",
46
+ default="json",
47
+ help="Output format (json/table/csv/tsv)",
48
+ )
49
+ @click.option("--output", help="Output file")
50
+ @click.option("--mode", default="auto", help="Processing mode (online/offline/auto)")
51
+ @click.pass_context
52
+ def get(
53
+ ctx,
54
+ report_type,
55
+ date_from,
56
+ date_to,
57
+ name,
58
+ fields,
59
+ campaign_ids,
60
+ adgroup_ids,
61
+ output_format,
62
+ output,
63
+ mode,
64
+ ):
65
+ """Get report"""
66
+ try:
67
+ client = create_client(
68
+ token=ctx.obj.get("token"),
69
+ login=ctx.obj.get("login"),
70
+ sandbox=ctx.obj.get("sandbox"),
71
+ )
72
+
73
+ field_names = [f.strip() for f in fields.split(",")]
74
+
75
+ selection_criteria = {"DateFrom": date_from, "DateTo": date_to}
76
+
77
+ if campaign_ids:
78
+ selection_criteria["Filter"] = [
79
+ {
80
+ "Field": "CampaignId",
81
+ "Operator": "IN",
82
+ "Values": campaign_ids.split(","),
83
+ }
84
+ ]
85
+ elif adgroup_ids:
86
+ selection_criteria["Filter"] = [
87
+ {
88
+ "Field": "AdGroupId",
89
+ "Operator": "IN",
90
+ "Values": adgroup_ids.split(","),
91
+ }
92
+ ]
93
+
94
+ body = {
95
+ "params": {
96
+ "SelectionCriteria": selection_criteria,
97
+ "FieldNames": field_names,
98
+ "ReportName": name,
99
+ "ReportType": report_type,
100
+ "DateRangeType": "CUSTOM_DATE",
101
+ "Format": "TSV" if output_format in ["tsv", "csv"] else "TSV",
102
+ "IncludeVAT": "YES",
103
+ "IncludeDiscount": "YES",
104
+ }
105
+ }
106
+
107
+ result = client.reports().post(data=body)
108
+
109
+ if output_format == "json":
110
+ format_output(result().to_dicts(), "json", output)
111
+ elif output_format == "table":
112
+ format_output(result().to_dicts(), "table", output)
113
+ elif output_format == "csv":
114
+ format_output(result().to_values(), "csv", output, headers=result.columns)
115
+ elif output_format == "tsv":
116
+ format_output(result().to_values(), "tsv", output, headers=result.columns)
117
+ else:
118
+ format_output(result.data, "json", output)
119
+
120
+ except Exception as e:
121
+ print_error(str(e))
122
+ raise click.Abort()
123
+
124
+
125
+ @reports.command()
126
+ def list_types():
127
+ """List available report types"""
128
+ format_output(REPORT_TYPES, "json", None)