loci-api 0.2.41__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.
- loci_api/__init__.py +2 -0
- loci_api/__main__.py +614 -0
- loci_api/api_helper.py +1462 -0
- loci_api/chat/README.md +67 -0
- loci_api/chat/__init__.py +0 -0
- loci_api/chat/chatbot.py +265 -0
- loci_api-0.2.41.dist-info/METADATA +100 -0
- loci_api-0.2.41.dist-info/RECORD +11 -0
- loci_api-0.2.41.dist-info/WHEEL +5 -0
- loci_api-0.2.41.dist-info/entry_points.txt +3 -0
- loci_api-0.2.41.dist-info/top_level.txt +1 -0
loci_api/__init__.py
ADDED
loci_api/__main__.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# pylint:disable=line-too-long,too-many-arguments,too-many-positional-arguments
|
|
2
|
+
"""
|
|
3
|
+
LOCI CLI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
from loci_api import api_helper
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
|
|
15
|
+
def parse_args():
|
|
16
|
+
program_name = os.path.basename(os.path.dirname(os.path.abspath(__file__)))
|
|
17
|
+
parser = argparse.ArgumentParser(prog=program_name, description="LOCI CLI")
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
19
|
+
|
|
20
|
+
projects_parser = subparsers.add_parser("list-projects", help="List all projects")
|
|
21
|
+
projects_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
22
|
+
|
|
23
|
+
list_versions_parser = subparsers.add_parser("list-versions", help="List all versions of a project")
|
|
24
|
+
list_versions_parser.add_argument("project-name", help="Project name", type=str)
|
|
25
|
+
list_versions_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
26
|
+
|
|
27
|
+
last_version_parser = subparsers.add_parser("last-version", help="Get last version of a project")
|
|
28
|
+
last_version_parser.add_argument("project-name", help="Project name", type=str)
|
|
29
|
+
last_version_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
30
|
+
|
|
31
|
+
upload_parser = subparsers.add_parser("upload", help="Upload a new version")
|
|
32
|
+
upload_parser.add_argument("path-to-binary", help="Binary file", type=str)
|
|
33
|
+
upload_parser.add_argument("project-name", help="Project name", type=str)
|
|
34
|
+
upload_parser.add_argument("new-version-name", help="Version name", type=str)
|
|
35
|
+
upload_parser.add_argument("--compare-version-name", help="Version to compare", type=str)
|
|
36
|
+
upload_parser.add_argument("--optimize", help="Invoke AI Agent that runs code optimizations, upon upload completion", action=argparse.BooleanOptionalAction, type=bool, default=False)
|
|
37
|
+
upload_parser.add_argument("--wait", help="wait for processing to finish", action=argparse.BooleanOptionalAction, type=bool, default=True)
|
|
38
|
+
upload_parser.add_argument("--scm-meta", help="Source Control Management (SCM) metadata. "
|
|
39
|
+
"Can be provided as either a path to a JSON file or a raw JSON string. "
|
|
40
|
+
"Refer to the SCMMetadata class for the expected JSON structure.",
|
|
41
|
+
type=str, default=None)
|
|
42
|
+
|
|
43
|
+
upload_last_parser = subparsers.add_parser("upload-last", help="Upload a new version using latest")
|
|
44
|
+
upload_last_parser.add_argument("path-to-binary", help="Binary file", type=str)
|
|
45
|
+
upload_last_parser.add_argument("project-name", help="Project name", type=str)
|
|
46
|
+
upload_last_parser.add_argument("new-version-name", help="Version name", type=str)
|
|
47
|
+
upload_last_parser.add_argument("--wait", help="wait for processing to finish", action=argparse.BooleanOptionalAction, type=bool, default=True)
|
|
48
|
+
upload_last_parser.add_argument("--scm-meta", help="Source Control Management (SCM) metadata. "
|
|
49
|
+
"Can be provided as either a path to a JSON file or a raw JSON string. "
|
|
50
|
+
"Refer to the SCMMetadata class for the expected JSON structure.",
|
|
51
|
+
type=str, default=None)
|
|
52
|
+
|
|
53
|
+
status_parser = subparsers.add_parser("status", help="Check status of a version")
|
|
54
|
+
status_parser.add_argument("project-name", help="Project name", type=str)
|
|
55
|
+
status_parser.add_argument("version-name", help="Version name", type=str)
|
|
56
|
+
status_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
57
|
+
|
|
58
|
+
func_insights_parser = subparsers.add_parser("func-insights", help="Get function insights and diff summary")
|
|
59
|
+
func_insights_parser.add_argument("project-name", help="Project name", type=str)
|
|
60
|
+
func_insights_parser.add_argument("version-name", help="Version name", type=str)
|
|
61
|
+
func_insights_parser.add_argument("--version-name-base", help="Base version name", type=str, default=None)
|
|
62
|
+
func_insights_parser.add_argument("--perc-resp-limit", help="Response time limit (percentage)", type=float, default=None)
|
|
63
|
+
func_insights_parser.add_argument("--perc-thro-limit", help="Throughput limit (percentage)", type=float, default=None)
|
|
64
|
+
func_insights_parser.add_argument("--perc-bott-limit", help="Bottleneck limit (percentage)", type=float, default=None)
|
|
65
|
+
func_insights_parser.add_argument("--pairs", nargs='*', default=[],
|
|
66
|
+
help="Pairs of function_name and binary_name, e.g. --pairs func1 bin1 func2 bin2")
|
|
67
|
+
func_insights_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
68
|
+
func_insights_parser.add_argument("--filter", type=str, nargs='*', choices=['new', 'mod'],
|
|
69
|
+
default='mod',
|
|
70
|
+
help="Filter functions by change type relative to the base version. " \
|
|
71
|
+
"Filters are only applied if base version has been provided (--version-name-base) " \
|
|
72
|
+
"and a comparison report exists; otherwise all functions are returned. " \
|
|
73
|
+
"In a case like this, the response will include an empty summary (diff_summary: {}) " \
|
|
74
|
+
"while 'insights' will still list all functions. " \
|
|
75
|
+
"Example: '--filter mod new' returns insights for both modified and new functions. "
|
|
76
|
+
"Default: 'mod'.")
|
|
77
|
+
|
|
78
|
+
flame_graph_parser = subparsers.add_parser("flame-graph", help="Generate flame graph for a function")
|
|
79
|
+
flame_graph_parser.add_argument("project-name", help="Project name", type=str)
|
|
80
|
+
flame_graph_parser.add_argument("version-name", help="Version name", type=str)
|
|
81
|
+
flame_graph_parser.add_argument("--function-name", help="Function long name", type=str)
|
|
82
|
+
flame_graph_parser.add_argument("--binary-name", help="Binary/container name", type=str)
|
|
83
|
+
|
|
84
|
+
summary_parser = subparsers.add_parser("summary", help="Get function insights summary")
|
|
85
|
+
summary_parser.add_argument("project-name", help="Project name", type=str)
|
|
86
|
+
summary_parser.add_argument("version-name", help="Version name", type=str)
|
|
87
|
+
summary_parser.add_argument("version-name-base", help="Base version name", type=str)
|
|
88
|
+
summary_parser.add_argument("--scm-meta", help="Source Control Management (SCM) metadata. "
|
|
89
|
+
"Can be provided as either a path to a JSON file or a raw JSON string. "
|
|
90
|
+
"Refer to the SCMMetadata class for the expected JSON structure.",
|
|
91
|
+
type=str, default=None)
|
|
92
|
+
summary_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
93
|
+
summary_parser.add_argument("--regenerate", help="Enable summary regeneration", action=argparse.BooleanOptionalAction, type=bool, default=False)
|
|
94
|
+
|
|
95
|
+
auth_parser = subparsers.add_parser("whoami", help="Check authentication status")
|
|
96
|
+
auth_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
97
|
+
|
|
98
|
+
token_parser = subparsers.add_parser("token", help="Manage SCM (Azure DevOps PAT) tokens for a project")
|
|
99
|
+
token_subparsers = token_parser.add_subparsers(dest="token_command", required=True)
|
|
100
|
+
|
|
101
|
+
token_status_parser = token_subparsers.add_parser("status", help="Show the project's SCM token status")
|
|
102
|
+
token_status_parser.add_argument("project-name", help="Project name", type=str)
|
|
103
|
+
token_status_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
104
|
+
|
|
105
|
+
token_test_parser = token_subparsers.add_parser("test", help="Validate a PAT against the project's Azure DevOps org without saving it")
|
|
106
|
+
token_test_parser.add_argument("project-name", help="Project name", type=str)
|
|
107
|
+
token_test_parser.add_argument("--pat", help="PAT value. If omitted, falls back to LOCI_SCM_TOKEN env var, then to one line from stdin.", type=str, default=None)
|
|
108
|
+
token_test_parser.add_argument("-o", "--output", help="Output file path. Default: stdout", type=str, default=None)
|
|
109
|
+
|
|
110
|
+
token_update_parser = token_subparsers.add_parser("update", help="Save a PAT for the project (no validation; pair with `token test` for that)")
|
|
111
|
+
token_update_parser.add_argument("project-name", help="Project name", type=str)
|
|
112
|
+
token_update_parser.add_argument("--pat", help="PAT value. If omitted, falls back to LOCI_SCM_TOKEN env var, then to one line from stdin.", type=str, default=None)
|
|
113
|
+
|
|
114
|
+
gh_parser = subparsers.add_parser("gh", help="Run GitHub related commands")
|
|
115
|
+
gh_subparsers = gh_parser.add_subparsers(dest="gh_command", required=True)
|
|
116
|
+
|
|
117
|
+
gh_comment_parser = gh_subparsers.add_parser("comment", help="Post comment with Loci analysis to GitHub PR")
|
|
118
|
+
gh_comment_parser.add_argument("project-name", help="Project name", type=str)
|
|
119
|
+
gh_comment_parser.add_argument("version-name", help="Version name", type=str)
|
|
120
|
+
gh_comment_parser.add_argument("version-name-base", help="Base version name", type=str)
|
|
121
|
+
gh_comment_parser.add_argument("--regenerate", help="Enable GitHub comment regeneration", action=argparse.BooleanOptionalAction, type=bool, default=False)
|
|
122
|
+
gh_comment_parser.add_argument("--scm-meta", help="Source Control Management (SCM) metadata. "
|
|
123
|
+
"Can be provided as either a path to a JSON file or a raw JSON string. "
|
|
124
|
+
"Refer to the SCMMetadata class for the expected JSON structure.",
|
|
125
|
+
type=str, default=None)
|
|
126
|
+
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
if not args.command:
|
|
130
|
+
parser.print_help()
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
return args
|
|
134
|
+
|
|
135
|
+
class STDPrinter:
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _humanize_key(key: str) -> str:
|
|
138
|
+
if " " in key:
|
|
139
|
+
parts = key.split()
|
|
140
|
+
elif "_" in key:
|
|
141
|
+
parts = key.split("_")
|
|
142
|
+
else:
|
|
143
|
+
parts = re.findall(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+", key) or [key]
|
|
144
|
+
out = []
|
|
145
|
+
for i, p in enumerate(parts):
|
|
146
|
+
if not p:
|
|
147
|
+
continue
|
|
148
|
+
# separate trailing digits: last4 -> last 4
|
|
149
|
+
p = re.sub(r"([a-zA-Z])(\d+)$", r"\1 \2", p)
|
|
150
|
+
p = re.sub(r"(\d+)([a-zA-Z])", r"\1 \2", p)
|
|
151
|
+
out.append(p.capitalize() if i == 0 else p.lower())
|
|
152
|
+
return " ".join(out)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _format_scalar(v: any) -> str:
|
|
156
|
+
if isinstance(v, bool):
|
|
157
|
+
return "Yes" if v else "No"
|
|
158
|
+
if v is None:
|
|
159
|
+
return "undefined"
|
|
160
|
+
if isinstance(v, (int, float)):
|
|
161
|
+
return str(int(v)) if isinstance(v, float) and v.is_integer() else str(v)
|
|
162
|
+
if isinstance(v, (datetime, date)):
|
|
163
|
+
return v.strftime("%d-%m-%Y")
|
|
164
|
+
return str(v)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def print(obj: any, indent: int = 0, bullet: bool = False) -> None:
|
|
168
|
+
if indent < 0:
|
|
169
|
+
indent = 0
|
|
170
|
+
|
|
171
|
+
pad = " " * indent
|
|
172
|
+
printed_bullet = not bullet
|
|
173
|
+
def _get_dash_n_pad():
|
|
174
|
+
nonlocal printed_bullet
|
|
175
|
+
if printed_bullet:
|
|
176
|
+
return pad
|
|
177
|
+
printed_bullet = True
|
|
178
|
+
return f"•{pad[1:]}"
|
|
179
|
+
|
|
180
|
+
if isinstance(obj, dict):
|
|
181
|
+
if not len(obj):
|
|
182
|
+
print("{}")
|
|
183
|
+
return
|
|
184
|
+
for k, v in obj.items():
|
|
185
|
+
label = STDPrinter._humanize_key(str(k))
|
|
186
|
+
if isinstance(v, dict) or isinstance(v, list):
|
|
187
|
+
print(f"{_get_dash_n_pad()}{label}:")
|
|
188
|
+
STDPrinter.print(v, indent + 2)
|
|
189
|
+
else:
|
|
190
|
+
print(f"{_get_dash_n_pad()}{label}: {STDPrinter._format_scalar(v)}")
|
|
191
|
+
elif isinstance(obj, list):
|
|
192
|
+
if not len(obj):
|
|
193
|
+
print("[]")
|
|
194
|
+
return
|
|
195
|
+
for item in obj:
|
|
196
|
+
if isinstance(item, (dict, list)):
|
|
197
|
+
STDPrinter.print(item, indent + 2, bullet=True)
|
|
198
|
+
else:
|
|
199
|
+
print(f"{pad}• {STDPrinter._format_scalar(item)}")
|
|
200
|
+
else:
|
|
201
|
+
print(f"{_get_dash_n_pad()}{STDPrinter._format_scalar(obj)}")
|
|
202
|
+
|
|
203
|
+
def output_success_result(res, output = None) -> int:
|
|
204
|
+
data = json.dumps(res, indent=2)
|
|
205
|
+
if output:
|
|
206
|
+
with open(output, 'w') as f:
|
|
207
|
+
f.write(data)
|
|
208
|
+
else:
|
|
209
|
+
STDPrinter.print(res)
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
def cmd_list_projects(output) -> int:
|
|
213
|
+
projects = api_helper.get_projects()
|
|
214
|
+
if projects is None:
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
res = [{'name': project['name'], 'architecture': project['architecture'], 'company_id': project['company_id']} for project in projects]
|
|
218
|
+
return output_success_result(res, output)
|
|
219
|
+
|
|
220
|
+
def cmd_list_versions(project_name, output) -> int:
|
|
221
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
222
|
+
if project_id is None:
|
|
223
|
+
return 1
|
|
224
|
+
versions = api_helper.get_versions(project_id)
|
|
225
|
+
if versions is None:
|
|
226
|
+
return 1
|
|
227
|
+
res = []
|
|
228
|
+
for version in versions:
|
|
229
|
+
res.append({
|
|
230
|
+
"name": version['properties']['version_name'],
|
|
231
|
+
"started_at": version['properties']['start_dt']
|
|
232
|
+
})
|
|
233
|
+
return output_success_result(res, output)
|
|
234
|
+
|
|
235
|
+
def cmd_last_version(project_name, output) -> int:
|
|
236
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
237
|
+
if project_id is None:
|
|
238
|
+
return 1
|
|
239
|
+
_, version_name = api_helper.get_last_version_id(project_id)
|
|
240
|
+
if version_name is None:
|
|
241
|
+
return 1
|
|
242
|
+
return output_success_result([version_name], output)
|
|
243
|
+
|
|
244
|
+
def read_scm_meta(scm_meta, head, base = "") -> str:
|
|
245
|
+
if not scm_meta:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
scm_metadata = None
|
|
249
|
+
try:
|
|
250
|
+
if scm_meta.endswith('.json') and os.path.exists(scm_meta):
|
|
251
|
+
with open(scm_meta, 'r', encoding='utf-8') as file:
|
|
252
|
+
scm_metadata = json.load(file)
|
|
253
|
+
else:
|
|
254
|
+
scm_metadata = json.loads(scm_meta)
|
|
255
|
+
|
|
256
|
+
if not hasattr(scm_metadata, 'base'):
|
|
257
|
+
scm_metadata['base'] = base
|
|
258
|
+
if not hasattr(scm_metadata, 'head'):
|
|
259
|
+
scm_metadata['head'] = head
|
|
260
|
+
|
|
261
|
+
return api_helper.SCMMetadata(**scm_metadata)
|
|
262
|
+
except Exception as ex:
|
|
263
|
+
print(f'Error. Invalid JSON format in SCM metadata. err: {ex}')
|
|
264
|
+
raise ex
|
|
265
|
+
|
|
266
|
+
def cmd_upload(file_path, project_name, version_name, cmp_ver_name, wait, scm_meta, optimize) -> int:
|
|
267
|
+
compare_version_id = ''
|
|
268
|
+
if cmp_ver_name:
|
|
269
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
270
|
+
versions = api_helper.get_versions(project_id)
|
|
271
|
+
# versions returned by API are sorted by date descending
|
|
272
|
+
for version in versions:
|
|
273
|
+
if cmp_ver_name == version['properties']['version_name']:
|
|
274
|
+
compare_version_id = version['properties']['version_id']
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
scm_metadata = read_scm_meta(scm_meta=scm_meta, head=version_name, base=cmp_ver_name)
|
|
279
|
+
except Exception:
|
|
280
|
+
return 1
|
|
281
|
+
|
|
282
|
+
if optimize and scm_metadata is None:
|
|
283
|
+
print('Warning. SCM metadata not provided, code optimization (enabled with --optimize flag) will be skipped.')
|
|
284
|
+
optimize = False
|
|
285
|
+
|
|
286
|
+
return api_helper.full_upload(file_path, version_name, project_name, use_latest=False, compare_version_id=compare_version_id, wait=wait, scm_metadata=scm_metadata, optimize=optimize)
|
|
287
|
+
|
|
288
|
+
def cmd_upload_last(file_path, project_name, version_name, wait, scm_meta) -> int:
|
|
289
|
+
try:
|
|
290
|
+
scm_metadata = read_scm_meta(scm_meta=scm_meta, head=version_name)
|
|
291
|
+
except Exception:
|
|
292
|
+
return 1
|
|
293
|
+
return api_helper.full_upload(file_path, version_name, project_name, use_latest=True, compare_version_id='', wait=wait, scm_metadata=scm_metadata)
|
|
294
|
+
|
|
295
|
+
def cmd_function_insights(project_name, version_name, version_name_base, perc_resp_limit, perc_thro_limit, perc_bott_limit, pairs, output, filters) -> int:
|
|
296
|
+
if pairs:
|
|
297
|
+
if len(pairs) % 2 != 0:
|
|
298
|
+
print(f'uneven number of pairs: {len(pairs)}')
|
|
299
|
+
return 1
|
|
300
|
+
pairs = [{"function_name": pairs[i], "binary_name": pairs[i + 1]} for i in range(0, len(pairs), 2)]
|
|
301
|
+
|
|
302
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
303
|
+
if not project_id:
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
version_id, version_id_base, report_id = api_helper.get_versions_data(project_id, version_name, version_name_base)
|
|
307
|
+
|
|
308
|
+
if not version_id:
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
result = api_helper.get_version_status(project_id, version_id)
|
|
312
|
+
if not result:
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
if result['status'] == -1:
|
|
316
|
+
print(f'Version {version_name} processing status is not complete. Function insights are not available.')
|
|
317
|
+
return 1
|
|
318
|
+
elif result['status'] != 0:
|
|
319
|
+
print(f'Version {version_name} processing has failed. Function insights are not available.')
|
|
320
|
+
return 1
|
|
321
|
+
|
|
322
|
+
def _to_filter(filters):
|
|
323
|
+
if not filters or not version_id_base:
|
|
324
|
+
return None
|
|
325
|
+
s = {f.strip().lower() for f in filters if f and f.strip()}
|
|
326
|
+
if not s:
|
|
327
|
+
return None
|
|
328
|
+
if "new" in s and "mod" in s:
|
|
329
|
+
return api_helper.FilterType.NEW_N_MODIFIED
|
|
330
|
+
if "new" in s:
|
|
331
|
+
return api_helper.FilterType.NEW
|
|
332
|
+
if "mod" in s:
|
|
333
|
+
return api_helper.FilterType.MODIFIED
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
insights = api_helper.get_function_insights(version_id, version_id_base, report_id, perc_resp_limit, perc_thro_limit, perc_bott_limit, pairs, _to_filter(filters))
|
|
337
|
+
if insights is None:
|
|
338
|
+
return 1
|
|
339
|
+
|
|
340
|
+
def _to_insight(insight, has_base_version):
|
|
341
|
+
res = {
|
|
342
|
+
'total_count': insight['total_count'],
|
|
343
|
+
'binary_name': insight['binary_name'],
|
|
344
|
+
'function_long_name': insight['function_long_name'],
|
|
345
|
+
'function_name': insight['function_name'],
|
|
346
|
+
'source_location': insight['src_location'],
|
|
347
|
+
'mean_bottleneck': insight['mean_bottleneck'],
|
|
348
|
+
'std_bottleneck': insight['std_bottleneck'],
|
|
349
|
+
'mean_throughput': insight['mean_throughput'],
|
|
350
|
+
'std_throughput': insight['std_throughput'],
|
|
351
|
+
'mean_response': insight['mean_resp'],
|
|
352
|
+
'std_response': insight['std_resp'],
|
|
353
|
+
}
|
|
354
|
+
if has_base_version:
|
|
355
|
+
res.update({
|
|
356
|
+
'mean_bottleneck_base': insight['mean_bottleneck_base'],
|
|
357
|
+
'std_bottleneck_base': insight['std_bottleneck_base'],
|
|
358
|
+
'mean_throughput_base': insight['mean_throughput_base'],
|
|
359
|
+
'std_throughput_base': insight['std_throughput_base'],
|
|
360
|
+
'mean_response_base': insight['mean_resp_base'],
|
|
361
|
+
'std_response_base': insight['std_resp_base'],
|
|
362
|
+
'perc_throughput': insight['perc_thro'],
|
|
363
|
+
'perc_response': insight['perc_resp'],
|
|
364
|
+
'perc_bottleneck': insight['perc_bott'],
|
|
365
|
+
})
|
|
366
|
+
return res
|
|
367
|
+
|
|
368
|
+
diff_summary = {}
|
|
369
|
+
|
|
370
|
+
if report_id:
|
|
371
|
+
report_data = api_helper.get_report_symbols_data(report_id)
|
|
372
|
+
if report_data:
|
|
373
|
+
diff_summary = report_data
|
|
374
|
+
return output_success_result({'diff_summary': diff_summary, 'insights': [_to_insight(i, version_id_base is not None) for i in insights['message']]}, output)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def cmd_flame_graph(project_name, version_name, function_name, binary_name) -> int:
|
|
378
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
379
|
+
if not project_id:
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
version_id, _ = api_helper.get_version(project_id, version_name)
|
|
383
|
+
|
|
384
|
+
if not version_id:
|
|
385
|
+
print('Version not found')
|
|
386
|
+
return 1
|
|
387
|
+
|
|
388
|
+
res = api_helper.get_flame_graph(project_id, version_id, binary_name, function_name)
|
|
389
|
+
if res is None:
|
|
390
|
+
return 1
|
|
391
|
+
|
|
392
|
+
return output_success_result(res)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def cmd_status(project_name, version_name, output) -> int:
|
|
396
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
397
|
+
if project_id is None:
|
|
398
|
+
return 1
|
|
399
|
+
|
|
400
|
+
version_id, _ = api_helper.get_version(project_id, version_name)
|
|
401
|
+
|
|
402
|
+
if version_id is None:
|
|
403
|
+
print("Version not found")
|
|
404
|
+
return 1
|
|
405
|
+
|
|
406
|
+
result = api_helper.get_version_status(project_id, version_id)
|
|
407
|
+
if not result:
|
|
408
|
+
return 1
|
|
409
|
+
return output_success_result(result, output)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def cmd_summary(project_name, version_name, version_name_base, scm_meta, output, regenerate) -> int:
|
|
413
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
414
|
+
if not project_id:
|
|
415
|
+
return 1
|
|
416
|
+
|
|
417
|
+
versions = api_helper.get_versions(project_id)
|
|
418
|
+
if not versions:
|
|
419
|
+
return 1
|
|
420
|
+
|
|
421
|
+
version_id, version_id_base, report_id = api_helper.get_versions_data(project_id, version_name, version_name_base)
|
|
422
|
+
|
|
423
|
+
if not version_id:
|
|
424
|
+
print('Version not found')
|
|
425
|
+
return 1
|
|
426
|
+
|
|
427
|
+
if not version_id_base:
|
|
428
|
+
print('Base version not found')
|
|
429
|
+
return 1
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
scm_metadata = read_scm_meta(scm_meta=scm_meta, head=version_name, base=version_name_base)
|
|
433
|
+
except Exception:
|
|
434
|
+
return 1
|
|
435
|
+
|
|
436
|
+
res = api_helper.get_performance_review_report(project_id, version_id, version_id_base, scm_metadata, regenerate)
|
|
437
|
+
if res is None:
|
|
438
|
+
return 1
|
|
439
|
+
|
|
440
|
+
return output_success_result(res, output)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def cmd_whoami(output) -> int:
|
|
444
|
+
res = api_helper.get_auth_status()
|
|
445
|
+
if res is None:
|
|
446
|
+
return 1
|
|
447
|
+
return output_success_result(res, output)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _read_pat(pat_arg) -> str:
|
|
451
|
+
if pat_arg and pat_arg.strip():
|
|
452
|
+
return pat_arg.strip()
|
|
453
|
+
if api_helper.SCM_TOKEN:
|
|
454
|
+
return api_helper.SCM_TOKEN
|
|
455
|
+
if sys.stdin.isatty():
|
|
456
|
+
print("Error: PAT not provided. Pass --pat, set LOCI_SCM_TOKEN, or pipe one line on stdin.", file=sys.stderr)
|
|
457
|
+
return ""
|
|
458
|
+
value = sys.stdin.readline().strip()
|
|
459
|
+
if not value:
|
|
460
|
+
print("Error: empty PAT read from stdin.", file=sys.stderr)
|
|
461
|
+
return value
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def cmd_token_status(project_name, output) -> int:
|
|
465
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
466
|
+
if not project_id:
|
|
467
|
+
return 1
|
|
468
|
+
res = api_helper.get_scm_token_status(project_id)
|
|
469
|
+
if res is None:
|
|
470
|
+
return 1
|
|
471
|
+
return output_success_result(res, output)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def cmd_token_test(project_name, pat, output) -> int:
|
|
475
|
+
pat = _read_pat(pat)
|
|
476
|
+
if not pat:
|
|
477
|
+
return 1
|
|
478
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
479
|
+
if not project_id:
|
|
480
|
+
return 1
|
|
481
|
+
res = api_helper.test_scm_token(project_id, pat)
|
|
482
|
+
if res is None:
|
|
483
|
+
return 1
|
|
484
|
+
output_success_result(res, output)
|
|
485
|
+
return 0 if res.get('valid') else 1
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def cmd_token_update(project_name, pat) -> int:
|
|
489
|
+
pat = _read_pat(pat)
|
|
490
|
+
if not pat:
|
|
491
|
+
return 1
|
|
492
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
493
|
+
if not project_id:
|
|
494
|
+
return 1
|
|
495
|
+
ok = api_helper.update_scm_token(project_id, pat)
|
|
496
|
+
if not ok:
|
|
497
|
+
return 1
|
|
498
|
+
print("SCM token updated.")
|
|
499
|
+
return 0
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def cmd_gh_comment(project_name, version_name, version_name_base, scm_meta, regenerate) -> int:
|
|
503
|
+
project_id, _ = api_helper.get_project(project_name)
|
|
504
|
+
if not project_id:
|
|
505
|
+
return 1
|
|
506
|
+
|
|
507
|
+
versions = api_helper.get_versions(project_id)
|
|
508
|
+
if not versions:
|
|
509
|
+
return 1
|
|
510
|
+
|
|
511
|
+
version_id, version_id_base, report_id = api_helper.get_versions_data(project_id, version_name, version_name_base)
|
|
512
|
+
|
|
513
|
+
if not version_id:
|
|
514
|
+
print('Version not found')
|
|
515
|
+
return 1
|
|
516
|
+
|
|
517
|
+
if not version_id_base:
|
|
518
|
+
print('Base version not found')
|
|
519
|
+
return 1
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
scm_metadata = read_scm_meta(scm_meta=scm_meta, head=version_name, base=version_name_base)
|
|
523
|
+
except Exception:
|
|
524
|
+
return 1
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
res = api_helper.post_github_comment(project_id, version_id, version_id_base, report_id, scm_metadata, regenerate)
|
|
528
|
+
if res is None:
|
|
529
|
+
return 1
|
|
530
|
+
|
|
531
|
+
return output_success_result(res)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def main():
|
|
535
|
+
args = parse_args()
|
|
536
|
+
|
|
537
|
+
if args.command == "list-projects":
|
|
538
|
+
sys.exit(cmd_list_projects(output=args.output))
|
|
539
|
+
|
|
540
|
+
if args.command == "list-versions":
|
|
541
|
+
sys.exit(cmd_list_versions(project_name=getattr(args, 'project-name'), output=args.output))
|
|
542
|
+
|
|
543
|
+
if args.command == "last-version":
|
|
544
|
+
sys.exit(cmd_last_version(project_name=getattr(args, 'project-name'), output=args.output))
|
|
545
|
+
|
|
546
|
+
if args.command == "status":
|
|
547
|
+
sys.exit(cmd_status(project_name=getattr(args, 'project-name'), version_name=getattr(args, 'version-name'), output=args.output))
|
|
548
|
+
|
|
549
|
+
if args.command == "upload":
|
|
550
|
+
sys.exit(cmd_upload(file_path=getattr(args, 'path-to-binary'),
|
|
551
|
+
project_name=getattr(args, 'project-name'),
|
|
552
|
+
version_name=getattr(args, 'new-version-name'),
|
|
553
|
+
cmp_ver_name=getattr(args, 'compare_version_name', None),
|
|
554
|
+
wait=getattr(args, 'wait', True),
|
|
555
|
+
scm_meta=getattr(args, 'scm_meta', None),
|
|
556
|
+
optimize=getattr(args, 'optimize', False)))
|
|
557
|
+
|
|
558
|
+
if args.command == "upload-last":
|
|
559
|
+
sys.exit(cmd_upload_last(file_path=getattr(args, 'path-to-binary'),
|
|
560
|
+
project_name=getattr(args, 'project-name'),
|
|
561
|
+
version_name=getattr(args, 'new-version-name'),
|
|
562
|
+
wait=getattr(args, 'wait', True),
|
|
563
|
+
scm_meta=getattr(args, 'scm_meta', None)))
|
|
564
|
+
|
|
565
|
+
if args.command == "func-insights":
|
|
566
|
+
sys.exit(cmd_function_insights(project_name=getattr(args, 'project-name'),
|
|
567
|
+
version_name=getattr(args, 'version-name'),
|
|
568
|
+
version_name_base=getattr(args, 'version_name_base', None),
|
|
569
|
+
perc_resp_limit=getattr(args, 'perc_resp_limit', None),
|
|
570
|
+
perc_thro_limit=getattr(args, 'perc_thro_limit', None),
|
|
571
|
+
perc_bott_limit=getattr(args, 'perc_bott_limit', None),
|
|
572
|
+
pairs=getattr(args, 'pairs', None),
|
|
573
|
+
output=args.output,
|
|
574
|
+
filters=getattr(args, 'filter', None)))
|
|
575
|
+
|
|
576
|
+
if args.command == "flame-graph":
|
|
577
|
+
sys.exit(cmd_flame_graph(project_name=getattr(args, 'project-name'),
|
|
578
|
+
version_name=getattr(args, 'version-name'),
|
|
579
|
+
function_name=getattr(args, 'function_name'),
|
|
580
|
+
binary_name=getattr(args, 'binary_name')))
|
|
581
|
+
|
|
582
|
+
if args.command == "summary":
|
|
583
|
+
sys.exit(cmd_summary(project_name=getattr(args, 'project-name'),
|
|
584
|
+
version_name=getattr(args, 'version-name'),
|
|
585
|
+
version_name_base=getattr(args, 'version-name-base'),
|
|
586
|
+
scm_meta=getattr(args, 'scm_meta'),
|
|
587
|
+
output=args.output,
|
|
588
|
+
regenerate=getattr(args, 'regenerate', False)))
|
|
589
|
+
|
|
590
|
+
if args.command == "whoami":
|
|
591
|
+
sys.exit(cmd_whoami(output=args.output))
|
|
592
|
+
|
|
593
|
+
if args.command == "token":
|
|
594
|
+
if args.token_command == "status":
|
|
595
|
+
sys.exit(cmd_token_status(project_name=getattr(args, 'project-name'),
|
|
596
|
+
output=args.output))
|
|
597
|
+
if args.token_command == "test":
|
|
598
|
+
sys.exit(cmd_token_test(project_name=getattr(args, 'project-name'),
|
|
599
|
+
pat=getattr(args, 'pat', None),
|
|
600
|
+
output=args.output))
|
|
601
|
+
if args.token_command == "update":
|
|
602
|
+
sys.exit(cmd_token_update(project_name=getattr(args, 'project-name'),
|
|
603
|
+
pat=getattr(args, 'pat', None)))
|
|
604
|
+
|
|
605
|
+
if args.command == "gh":
|
|
606
|
+
if args.gh_command == "comment":
|
|
607
|
+
sys.exit(cmd_gh_comment(project_name=getattr(args, 'project-name'),
|
|
608
|
+
version_name=getattr(args, 'version-name'),
|
|
609
|
+
version_name_base=getattr(args, 'version-name-base'),
|
|
610
|
+
scm_meta=getattr(args, 'scm_meta'),
|
|
611
|
+
regenerate=getattr(args, 'regenerate', False)))
|
|
612
|
+
|
|
613
|
+
if __name__ == "__main__":
|
|
614
|
+
main()
|