librelane 2.4.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of librelane might be problematic. Click here for more details.

Files changed (166) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +470 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +61 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +245 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +402 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +117 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +158 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +722 -0
  27. librelane/container.py +264 -0
  28. librelane/env_info.py +306 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +330 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +985 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/logging/__init__.py +40 -0
  52. librelane/logging/logger.py +323 -0
  53. librelane/open_pdks_rev +1 -0
  54. librelane/plugins.py +21 -0
  55. librelane/py.typed +0 -0
  56. librelane/scripts/base.sdc +80 -0
  57. librelane/scripts/klayout/Readme.md +2 -0
  58. librelane/scripts/klayout/open_design.py +63 -0
  59. librelane/scripts/klayout/render.py +121 -0
  60. librelane/scripts/klayout/stream_out.py +176 -0
  61. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  62. librelane/scripts/klayout/xor.drc +120 -0
  63. librelane/scripts/magic/Readme.md +1 -0
  64. librelane/scripts/magic/common/read.tcl +114 -0
  65. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  66. librelane/scripts/magic/def/mag.tcl +19 -0
  67. librelane/scripts/magic/def/mag_gds.tcl +81 -0
  68. librelane/scripts/magic/drc.tcl +79 -0
  69. librelane/scripts/magic/extract_spice.tcl +98 -0
  70. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  71. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  72. librelane/scripts/magic/gds/extras_mag.tcl +47 -0
  73. librelane/scripts/magic/gds/mag_with_pointers.tcl +32 -0
  74. librelane/scripts/magic/get_bbox.tcl +11 -0
  75. librelane/scripts/magic/lef/extras_maglef.tcl +63 -0
  76. librelane/scripts/magic/lef/maglef.tcl +27 -0
  77. librelane/scripts/magic/lef.tcl +57 -0
  78. librelane/scripts/magic/open.tcl +28 -0
  79. librelane/scripts/magic/wrapper.tcl +19 -0
  80. librelane/scripts/netgen/setup.tcl +28 -0
  81. librelane/scripts/odbpy/apply_def_template.py +49 -0
  82. librelane/scripts/odbpy/cell_frequency.py +107 -0
  83. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  84. librelane/scripts/odbpy/contextualize.py +109 -0
  85. librelane/scripts/odbpy/defutil.py +574 -0
  86. librelane/scripts/odbpy/diodes.py +373 -0
  87. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  88. librelane/scripts/odbpy/exception_codes.py +17 -0
  89. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  90. librelane/scripts/odbpy/io_place.py +482 -0
  91. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  92. librelane/scripts/odbpy/lefutil.py +97 -0
  93. librelane/scripts/odbpy/placers.py +162 -0
  94. librelane/scripts/odbpy/power_utils.py +395 -0
  95. librelane/scripts/odbpy/random_place.py +57 -0
  96. librelane/scripts/odbpy/reader.py +246 -0
  97. librelane/scripts/odbpy/remove_buffers.py +173 -0
  98. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  99. librelane/scripts/odbpy/wire_lengths.py +93 -0
  100. librelane/scripts/openroad/antenna_check.tcl +20 -0
  101. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  102. librelane/scripts/openroad/basic_mp.tcl +24 -0
  103. librelane/scripts/openroad/buffer_list.tcl +10 -0
  104. librelane/scripts/openroad/common/dpl.tcl +24 -0
  105. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  106. librelane/scripts/openroad/common/grt.tcl +32 -0
  107. librelane/scripts/openroad/common/io.tcl +476 -0
  108. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  109. librelane/scripts/openroad/common/resizer.tcl +103 -0
  110. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  111. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  112. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  113. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  114. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  115. librelane/scripts/openroad/cts.tcl +80 -0
  116. librelane/scripts/openroad/cut_rows.tcl +24 -0
  117. librelane/scripts/openroad/dpl.tcl +24 -0
  118. librelane/scripts/openroad/drt.tcl +37 -0
  119. librelane/scripts/openroad/fill.tcl +30 -0
  120. librelane/scripts/openroad/floorplan.tcl +145 -0
  121. librelane/scripts/openroad/gpl.tcl +88 -0
  122. librelane/scripts/openroad/grt.tcl +30 -0
  123. librelane/scripts/openroad/gui.tcl +15 -0
  124. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  125. librelane/scripts/openroad/ioplacer.tcl +67 -0
  126. librelane/scripts/openroad/irdrop.tcl +51 -0
  127. librelane/scripts/openroad/pdn.tcl +52 -0
  128. librelane/scripts/openroad/rcx.tcl +32 -0
  129. librelane/scripts/openroad/repair_design.tcl +70 -0
  130. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  131. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  132. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  133. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  134. librelane/scripts/openroad/sta/corner.tcl +393 -0
  135. librelane/scripts/openroad/tapcell.tcl +25 -0
  136. librelane/scripts/openroad/write_views.tcl +27 -0
  137. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  138. librelane/scripts/pyosys/json_header.py +84 -0
  139. librelane/scripts/pyosys/synthesize.py +493 -0
  140. librelane/scripts/pyosys/ys_common.py +153 -0
  141. librelane/scripts/tclsh/hello.tcl +1 -0
  142. librelane/state/__init__.py +24 -0
  143. librelane/state/__main__.py +61 -0
  144. librelane/state/design_format.py +180 -0
  145. librelane/state/state.py +351 -0
  146. librelane/steps/__init__.py +61 -0
  147. librelane/steps/__main__.py +511 -0
  148. librelane/steps/checker.py +637 -0
  149. librelane/steps/common_variables.py +340 -0
  150. librelane/steps/cvc_rv.py +169 -0
  151. librelane/steps/klayout.py +509 -0
  152. librelane/steps/magic.py +566 -0
  153. librelane/steps/misc.py +160 -0
  154. librelane/steps/netgen.py +253 -0
  155. librelane/steps/odb.py +955 -0
  156. librelane/steps/openroad.py +2433 -0
  157. librelane/steps/openroad_alerts.py +102 -0
  158. librelane/steps/pyosys.py +629 -0
  159. librelane/steps/step.py +1547 -0
  160. librelane/steps/tclstep.py +288 -0
  161. librelane/steps/verilator.py +222 -0
  162. librelane/steps/yosys.py +371 -0
  163. librelane-2.4.0.dev0.dist-info/METADATA +151 -0
  164. librelane-2.4.0.dev0.dist-info/RECORD +166 -0
  165. librelane-2.4.0.dev0.dist-info/WHEEL +4 -0
  166. librelane-2.4.0.dev0.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,413 @@
1
+ # Copyright 2023 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import os
15
+ import sys
16
+ import json
17
+ import gzip
18
+ import click
19
+ import tarfile
20
+ import tempfile
21
+ from io import BytesIO
22
+ from decimal import Decimal
23
+ from typing import Optional, Set, Tuple
24
+
25
+ import cloup
26
+ import httpx
27
+
28
+ from .util import MetricDiff, TableVerbosity
29
+ from ..misc import Filter, get_httpx_session, mkdirp
30
+ from ..cli import formatter_settings, IntEnumChoice
31
+
32
+ default_filter_set = [
33
+ "design__*__area",
34
+ "design__max_*",
35
+ "design__lvs_error__count",
36
+ "antenna__violating*",
37
+ "clock__*",
38
+ "ir__*",
39
+ "power__*",
40
+ "timing__*_vio__*",
41
+ "timing*wns*",
42
+ "timing*tns*",
43
+ "*error*",
44
+ "!*__iter:*",
45
+ ]
46
+
47
+ # passing_filter_set = [
48
+ # "design__*__area",
49
+ # "route__wirelength__max",
50
+ # "design__instance__utilization",
51
+ # "antenna__violating*",
52
+ # "timing__*__ws",
53
+ # "clock__skew__*",
54
+ # "ir__*",
55
+ # "power__*",
56
+ # "!*__iter:*",
57
+ # ]
58
+
59
+
60
+ @cloup.group(
61
+ no_args_is_help=True,
62
+ formatter_settings=formatter_settings,
63
+ )
64
+ def cli():
65
+ pass
66
+
67
+
68
+ def common_opts(f):
69
+ f = cloup.option(
70
+ "-f",
71
+ "--filter",
72
+ "filter_wildcards",
73
+ multiple=True,
74
+ default=("DEFAULT",),
75
+ help="A list of wildcards to filter by. Wildcards prefixed with ! exclude rather than include and take priority. 'DEFAULT' is replaced by a set of default wildcards.",
76
+ )(f)
77
+ f = cloup.option(
78
+ "--table-verbosity",
79
+ type=IntEnumChoice(TableVerbosity),
80
+ default="ALL",
81
+ help=TableVerbosity.__doc__,
82
+ )(f)
83
+ f = cloup.option(
84
+ "--table-out",
85
+ type=click.Path(file_okay=True, dir_okay=False, writable=True),
86
+ help="The place to write the table to.",
87
+ default=None,
88
+ )(f)
89
+ f = cloup.option(
90
+ "--significant-figures",
91
+ type=int,
92
+ help="Number of significant figures.",
93
+ default=4,
94
+ )(f)
95
+ return f
96
+
97
+
98
+ @cloup.command(no_args_is_help=True)
99
+ @common_opts
100
+ @cloup.argument("metric_files", nargs=2)
101
+ def compare(
102
+ metric_files: Tuple[str, str],
103
+ table_verbosity: TableVerbosity,
104
+ filter_wildcards: Tuple[str, ...],
105
+ table_out: Optional[str],
106
+ significant_figures: int,
107
+ ):
108
+ """
109
+ Creates a small summary of the differences between two ``metrics.json`` files.
110
+ """
111
+ if table_verbosity == "NONE":
112
+ print("Table is empty.", file=sys.stderr)
113
+ exit(0)
114
+
115
+ a_path, b_path = metric_files
116
+ a = json.load(open(a_path, encoding="utf8"), parse_float=Decimal)
117
+ b = json.load(open(b_path, encoding="utf8"), parse_float=Decimal)
118
+
119
+ final_filters = []
120
+ for wildcard in filter_wildcards:
121
+ if wildcard == "DEFAULT":
122
+ final_filters += default_filter_set
123
+ else:
124
+ final_filters.append(wildcard)
125
+
126
+ diff = MetricDiff.from_metrics(
127
+ a, b, significant_figures, filter=Filter(final_filters)
128
+ )
129
+
130
+ md_str = diff.render_md(sort_by=("corner", ""), table_verbosity=table_verbosity)
131
+
132
+ table_file = sys.stdout
133
+ if table_out is not None:
134
+ table_file = open(table_out, "w", encoding="utf8")
135
+ print(md_str, file=table_file)
136
+
137
+ # When we upgrade to rich 13 (when NixOS 23.11 comes out,
138
+ # it has a proper markdown table renderer, but until then, this will have to do)
139
+
140
+
141
+ cli.add_command(compare)
142
+
143
+
144
+ def _compare_metric_folders(
145
+ filter_wildcards: Tuple[str, ...],
146
+ table_verbosity: TableVerbosity,
147
+ path_a: str,
148
+ path_b: str,
149
+ significant_figures: int,
150
+ ) -> Tuple[str, str]: # (summary, table)
151
+ a: Set[Tuple[str, str, str]] = set()
152
+ b: Set[Tuple[str, str, str]] = set()
153
+
154
+ def add_designs(in_dir: str, to_set: Set[Tuple[str, str, str]]):
155
+ for file in os.listdir(in_dir):
156
+ basename = os.path.basename(file)
157
+ if not basename.endswith(".metrics.json"):
158
+ continue
159
+ basename = basename[: -len(".metrics.json")]
160
+
161
+ parts = basename.split("-", maxsplit=2)
162
+ if len(parts) != 3:
163
+ raise ValueError(
164
+ f"Invalid filename {basename}: not in the format {{pdk}}-{{scl}}-{{design_name}}"
165
+ )
166
+ pdk, scl, design = parts
167
+ to_set.add((pdk, scl, design))
168
+
169
+ add_designs(path_a, a)
170
+ add_designs(path_b, b)
171
+
172
+ not_in_a = b - a
173
+ not_in_b = a - b
174
+ common = a.intersection(b)
175
+ difference_report = ""
176
+ for tup in not_in_a:
177
+ pdk, scl, design = tup
178
+ difference_report += f"* Results for a new test, `{'/'.join(tup)}`, detected.\n"
179
+ for tup in not_in_b:
180
+ pdk, scl, design = tup
181
+ difference_report += (
182
+ f"* ‼️ Results for `{'/'.join(tup)}` appear to be missing!\n"
183
+ )
184
+
185
+ final_filters = []
186
+ for wildcard in filter_wildcards:
187
+ if wildcard == "DEFAULT":
188
+ final_filters += default_filter_set
189
+ else:
190
+ final_filters.append(wildcard)
191
+
192
+ filter = Filter(final_filters)
193
+ critical_change_report = ""
194
+ tables = ""
195
+ total_critical = 0
196
+ for pdk, scl, design in sorted(common):
197
+ metrics_a = json.load(
198
+ open(
199
+ os.path.join(path_a, f"{pdk}-{scl}-{design}.metrics.json"),
200
+ encoding="utf8",
201
+ ),
202
+ parse_float=Decimal,
203
+ )
204
+
205
+ metrics_b = json.load(
206
+ open(
207
+ os.path.join(path_b, f"{pdk}-{scl}-{design}.metrics.json"),
208
+ encoding="utf8",
209
+ ),
210
+ parse_float=Decimal,
211
+ )
212
+
213
+ diff = MetricDiff.from_metrics(
214
+ metrics_a,
215
+ metrics_b,
216
+ significant_figures,
217
+ filter=filter,
218
+ )
219
+
220
+ stats = diff.stats()
221
+
222
+ total_critical += stats.critical
223
+ if stats.critical > 0:
224
+ critical_change_report += f" * `{pdk}/{scl}/{design}` \n"
225
+ if table_verbosity != "NONE":
226
+ rendered = diff.render_md(("corner", ""), table_verbosity)
227
+ if rendered.strip() != "":
228
+ tables += f"<details><summary><code>{pdk}/{scl}/{design}</code></summary>\n{rendered}\n</details>\n\n"
229
+
230
+ if total_critical == 0:
231
+ critical_change_report = (
232
+ "* No changes to critical metrics were detected in analyzed designs.\n"
233
+ + critical_change_report
234
+ )
235
+ else:
236
+ critical_change_report = (
237
+ "* **Changes to critical metrics were detected in the following designs:**\n"
238
+ + critical_change_report
239
+ )
240
+
241
+ report = ""
242
+ report += difference_report
243
+ report += critical_change_report
244
+
245
+ return report, tables.strip()
246
+
247
+
248
+ @cloup.command(no_args_is_help=True)
249
+ @common_opts
250
+ @cloup.argument("metric_folders", nargs=2)
251
+ def compare_multiple(
252
+ filter_wildcards: Tuple[str, ...],
253
+ table_verbosity: TableVerbosity,
254
+ metric_folders: Tuple[str, str],
255
+ table_out: Optional[str],
256
+ significant_figures: int,
257
+ ):
258
+ """
259
+ Creates a small summary/report of the differences between two folders with
260
+ metrics files.
261
+
262
+ The metrics files must be named in the format ``{pdk}-{scl}-{design}.metrics.json``.
263
+ All other files are ignored.
264
+ """
265
+ path_a, path_b = metric_folders
266
+ summary, tables = _compare_metric_folders(
267
+ filter_wildcards, table_verbosity, path_a, path_b, significant_figures
268
+ )
269
+ print(summary)
270
+ table_file = sys.stdout
271
+ if table_out is not None:
272
+ table_file = open(table_out, "w", encoding="utf8")
273
+ print(tables, file=table_file)
274
+
275
+
276
+ cli.add_command(compare_multiple)
277
+
278
+
279
+ @cloup.command(hidden=True)
280
+ @cloup.option(
281
+ "-r",
282
+ "--repo",
283
+ default="librelane/librelane",
284
+ help="The GitHub repository for LibreLane",
285
+ )
286
+ @cloup.option(
287
+ "-m",
288
+ "--metric-repo",
289
+ default="efabless/librelane-metrics",
290
+ help="The repository storing metrics for --repo",
291
+ )
292
+ @cloup.option(
293
+ "-b",
294
+ "--branch",
295
+ default="main",
296
+ help="The branch to compare to",
297
+ )
298
+ @cloup.option(
299
+ "-c",
300
+ "--commit",
301
+ default=None,
302
+ help="The commit of --repo to fetch the metrics for. By default, that's the latest commit in the chosen branch.",
303
+ )
304
+ @cloup.option(
305
+ "-t",
306
+ "--token",
307
+ default=None,
308
+ help="A GitHub token to use to query the API and fetch the metrics. Not strictly required, but helps avoid rate-limiting.",
309
+ )
310
+ @common_opts
311
+ @cloup.argument("metric_folder", nargs=1)
312
+ def compare_remote(
313
+ filter_wildcards: Tuple[str, ...],
314
+ table_verbosity: TableVerbosity,
315
+ repo: str,
316
+ metric_repo: str,
317
+ commit: Optional[str],
318
+ token: str,
319
+ metric_folder: str,
320
+ table_out: Optional[str],
321
+ significant_figures: int,
322
+ branch: str,
323
+ ):
324
+ """
325
+ Creates a small summary/report of the differences between a folder and
326
+ a set of metrics stored in --metric-repo. Requires Internet access and
327
+ access to GitHub.
328
+
329
+ The metrics files must be named in the format ``{pdk}-{scl}-{design}.metrics.json``.
330
+ All other files are ignored.
331
+ """
332
+ session = get_httpx_session(token)
333
+
334
+ if commit is None:
335
+ try:
336
+ result = session.get(
337
+ f"https://api.github.com/repos/{repo}/branches/{branch}"
338
+ )
339
+ except httpx.HTTPStatusError as e:
340
+ if e.response is not None and e.response.status_code == 404:
341
+ print(f"'{branch}' branch of repo {repo} not found.", file=sys.stderr)
342
+ else:
343
+ print(
344
+ f"failed to get info from github API: {e.response.status_code}",
345
+ file=sys.stderr,
346
+ )
347
+ sys.exit(-1)
348
+ result.raise_for_status()
349
+ commit = str(result.json()["commit"]["sha"])
350
+ url = f"https://github.com/{metric_repo}/tarball/commit-{commit}"
351
+
352
+ try:
353
+ with tempfile.TemporaryDirectory(prefix="librelane_metrics_tmpdir_") as d:
354
+ bio_gz = BytesIO()
355
+ with session.stream("GET", url) as r:
356
+ r.raise_for_status()
357
+ for chunk in r.iter_bytes(chunk_size=8192):
358
+ bio_gz.write(chunk)
359
+ bio_gz.seek(0)
360
+ with gzip.GzipFile(fileobj=bio_gz) as bio, tarfile.TarFile(
361
+ fileobj=bio, mode="r"
362
+ ) as tf:
363
+ for file in tf:
364
+ if file.isdir():
365
+ continue
366
+ stripped = os.path.sep.join(file.name.split(os.path.sep)[1:])
367
+ final_path = os.path.join(d, stripped)
368
+ final_dir = os.path.dirname(final_path)
369
+ mkdirp(final_dir)
370
+ io = tf.extractfile(file)
371
+ if io is None:
372
+ print(
373
+ f"Failed to unpack file in tarball: {file.name}.",
374
+ file=sys.stderr,
375
+ )
376
+ else:
377
+ with open(final_path, "wb") as f:
378
+ f.write(io.read())
379
+
380
+ summary, tables = _compare_metric_folders(
381
+ filter_wildcards,
382
+ table_verbosity,
383
+ d,
384
+ metric_folder,
385
+ significant_figures,
386
+ )
387
+ print(summary)
388
+ table_file = sys.stdout
389
+ if table_out is not None:
390
+ table_file = open(table_out, "w", encoding="utf8")
391
+ print(tables, file=table_file)
392
+ except httpx.HTTPStatusError as e:
393
+ if e.response is not None and e.response.status_code == 404:
394
+ print(f"Metrics not found for commit: {commit}.", file=sys.stderr)
395
+ else:
396
+ if e.response is not None:
397
+ print(
398
+ f"Failed to obtain metrics for {commit} remotely: {e.response}.",
399
+ file=sys.stderr,
400
+ )
401
+ else:
402
+ print(
403
+ f"Failed to request metrics for {commit} from server: {e}.",
404
+ file=sys.stderr,
405
+ )
406
+ sys.exit(-1)
407
+
408
+
409
+ cli.add_command(compare_remote)
410
+
411
+
412
+ if __name__ == "__main__":
413
+ cli()