pathview-plus 2.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.
@@ -0,0 +1,305 @@
1
+ """
2
+ svg_rendering.py
3
+ Generate SVG (Scalable Vector Graphics) output for KEGG pathways.
4
+
5
+ This complements the existing PNG (pixel-based) and PDF (graph-based) modes
6
+ with native vector graphics that are web-friendly, scalable, and editable.
7
+
8
+ Public API
9
+ ----------
10
+ keggview_svg : Render pathway as SVG with data overlay
11
+ render_node_svg : Generate SVG code for a single node
12
+ render_edge_svg : Generate SVG code for a single edge
13
+
14
+ SVG advantages over PNG:
15
+ - Scalable without quality loss
16
+ - Smaller file size for simple diagrams
17
+ - Editable in vector graphics software
18
+ - Web-native format (no conversion needed)
19
+ - Supports CSS styling and JavaScript interaction
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import warnings
25
+ from pathlib import Path
26
+ from typing import Optional
27
+ from xml.etree import ElementTree as ET
28
+
29
+ import polars as pl
30
+
31
+ from .color_mapping import draw_color_key, make_colormap
32
+ from .utils import wordwrap
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # SVG header and footer
37
+ # ---------------------------------------------------------------------------
38
+
39
+ def _svg_header(width: int, height: int, title: str = "") -> str:
40
+ """Generate SVG document header."""
41
+ return f'''<?xml version="1.0" encoding="UTF-8"?>
42
+ <svg width="{width}" height="{height}"
43
+ xmlns="http://www.w3.org/2000/svg"
44
+ xmlns:xlink="http://www.w3.org/1999/xlink"
45
+ viewBox="0 0 {width} {height}">
46
+ <title>{title}</title>
47
+ <defs>
48
+ <style type="text/css">
49
+ .node {{ stroke: #333; stroke-width: 1; }}
50
+ .edge {{ stroke: #666; stroke-width: 1; fill: none; }}
51
+ .label {{ font-family: Arial, sans-serif; font-size: 11px; fill: #000; text-anchor: middle; }}
52
+ </style>
53
+ </defs>
54
+ '''
55
+
56
+ def _svg_footer() -> str:
57
+ """Generate SVG document footer."""
58
+ return "</svg>"
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Node rendering (rectangles and ellipses)
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def render_node_svg(
66
+ node_id: str,
67
+ x: float,
68
+ y: float,
69
+ width: float,
70
+ height: float,
71
+ shape: str,
72
+ label: str,
73
+ fill_colors: list[str],
74
+ opacity: float = 1.0,
75
+ ) -> str:
76
+ """
77
+ Render a single node as SVG.
78
+
79
+ Parameters
80
+ ----------
81
+ node_id: Unique identifier for the node
82
+ x, y: Center coordinates
83
+ width, height: Node dimensions
84
+ shape: "rectangle", "ellipse", "roundedrectangle"
85
+ label: Text to display on node
86
+ fill_colors: List of hex colors (one per data column/state)
87
+ opacity: Fill opacity (0-1)
88
+
89
+ Returns SVG code string for this node.
90
+ """
91
+ svg_parts = []
92
+ n_states = len(fill_colors)
93
+
94
+ # Calculate bounding box
95
+ x1 = x - width / 2
96
+ y1 = y - height / 2
97
+
98
+ if shape == "ellipse":
99
+ # Slice ellipse vertically for multi-state
100
+ rx, ry = width / 2, height / 2
101
+ for i, color in enumerate(fill_colors):
102
+ # Create clipped ellipse slices
103
+ clip_x = x1 + (i * width / n_states)
104
+ clip_width = width / n_states
105
+ svg_parts.append(
106
+ f'<g clip-path="url(#clip_{node_id}_{i})">'
107
+ f' <ellipse cx="{x}" cy="{y}" rx="{rx}" ry="{ry}" '
108
+ f' fill="{color}" opacity="{opacity}" class="node"/>'
109
+ f'</g>'
110
+ )
111
+ # Define clip path
112
+ svg_parts.insert(
113
+ 0,
114
+ f'<clipPath id="clip_{node_id}_{i}">'
115
+ f' <rect x="{clip_x}" y="{y1}" width="{clip_width}" height="{height}"/>'
116
+ f'</clipPath>'
117
+ )
118
+ else:
119
+ # Rectangle or rounded rectangle
120
+ rx_round = 5 if shape == "roundedrectangle" else 0
121
+ for i, color in enumerate(fill_colors):
122
+ slice_x = x1 + (i * width / n_states)
123
+ slice_width = width / n_states
124
+ svg_parts.append(
125
+ f'<rect x="{slice_x}" y="{y1}" width="{slice_width}" height="{height}" '
126
+ f'rx="{rx_round}" fill="{color}" opacity="{opacity}" class="node"/>'
127
+ )
128
+
129
+ # Add label
130
+ wrapped = wordwrap(label, width=max(8, int(width / 8)))
131
+ lines = wrapped.split("\n")
132
+ y_text = y - (len(lines) - 1) * 5
133
+ for line in lines:
134
+ svg_parts.append(
135
+ f'<text x="{x}" y="{y_text}" class="label">{_escape_xml(line)}</text>'
136
+ )
137
+ y_text += 12
138
+
139
+ return "\n".join(svg_parts)
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Edge rendering
144
+ # ---------------------------------------------------------------------------
145
+
146
+ def render_edge_svg(
147
+ source_x: float,
148
+ source_y: float,
149
+ target_x: float,
150
+ target_y: float,
151
+ edge_type: str = "arrow",
152
+ color: str = "#666",
153
+ width: float = 1.5,
154
+ ) -> str:
155
+ """
156
+ Render a single edge as SVG.
157
+
158
+ Parameters
159
+ ----------
160
+ source_x, source_y: Start coordinates
161
+ target_x, target_y: End coordinates
162
+ edge_type: "arrow", "inhibition", "dotted"
163
+ color: Stroke color
164
+ width: Line width
165
+
166
+ Returns SVG code string for this edge.
167
+ """
168
+ marker_id = f"marker_{edge_type}"
169
+ path_style = f'stroke="{color}" stroke-width="{width}" fill="none" class="edge"'
170
+
171
+ if edge_type == "dotted":
172
+ path_style += ' stroke-dasharray="3,3"'
173
+
174
+ # Define arrow markers
175
+ markers = f'''
176
+ <defs>
177
+ <marker id="{marker_id}" viewBox="0 0 10 10" refX="8" refY="5"
178
+ markerWidth="6" markerHeight="6" orient="auto">
179
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="{color}"/>
180
+ </marker>
181
+ </defs>
182
+ '''
183
+
184
+ # Draw line
185
+ line = f'<line x1="{source_x}" y1="{source_y}" x2="{target_x}" y2="{target_y}" '\
186
+ f'{path_style} marker-end="url(#{marker_id})"/>'
187
+
188
+ return markers + line
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Main SVG rendering function
193
+ # ---------------------------------------------------------------------------
194
+
195
+ def keggview_svg(
196
+ plot_data_gene: Optional[pl.DataFrame],
197
+ cols_gene: Optional[pl.DataFrame],
198
+ plot_data_cpd: Optional[pl.DataFrame],
199
+ cols_cpd: Optional[pl.DataFrame],
200
+ node_data: pl.DataFrame,
201
+ pathway_name: str,
202
+ kegg_dir: Path = Path("."),
203
+ out_suffix: str = "pathview",
204
+ new_signature: bool = True,
205
+ **kwargs,
206
+ ) -> None:
207
+ """
208
+ Render pathway as SVG with data overlay.
209
+
210
+ This is an alternative to keggview_native (PNG) and keggview_graph (PDF).
211
+ Generates a standalone SVG file with nodes colored by expression data.
212
+
213
+ Parameters
214
+ ----------
215
+ plot_data_gene: Gene node positions + data
216
+ cols_gene: Gene node color assignments
217
+ plot_data_cpd: Compound node positions + data
218
+ cols_cpd: Compound node color assignments
219
+ node_data: All pathway nodes
220
+ pathway_name: Pathway ID
221
+ kegg_dir: Output directory
222
+ out_suffix: Output filename suffix
223
+ new_signature: Add "Rendered by pathview.py" watermark
224
+ """
225
+ # Determine canvas size from node positions
226
+ max_x = node_data["x"].max() or 1000
227
+ max_y = node_data["y"].max() or 800
228
+ canvas_width = int(max_x + 100)
229
+ canvas_height = int(max_y + 100)
230
+
231
+ svg_code = [_svg_header(canvas_width, canvas_height, pathway_name)]
232
+
233
+ # Render gene nodes
234
+ if plot_data_gene is not None and cols_gene is not None:
235
+ svg_code.append("<!-- Gene nodes -->")
236
+ color_cols = [c for c in cols_gene.columns if c.endswith("_col")]
237
+ for row in plot_data_gene.iter_rows(named=True):
238
+ node_id = row["entry_id"]
239
+ colors = [cols_gene.filter(pl.col("id") == node_id)[c].item()
240
+ for c in color_cols if not cols_gene.filter(pl.col("id") == node_id).is_empty()]
241
+ if not colors or all(c == "transparent" for c in colors):
242
+ colors = ["#CCCCCC"]
243
+ svg_code.append(
244
+ render_node_svg(
245
+ node_id=node_id,
246
+ x=row["x"],
247
+ y=row["y"],
248
+ width=row["width"],
249
+ height=row["height"],
250
+ shape=row.get("shape", "rectangle"),
251
+ label=row.get("label", ""),
252
+ fill_colors=colors,
253
+ )
254
+ )
255
+
256
+ # Render compound nodes
257
+ if plot_data_cpd is not None and cols_cpd is not None:
258
+ svg_code.append("<!-- Compound nodes -->")
259
+ color_cols = [c for c in cols_cpd.columns if c.endswith("_col")]
260
+ for row in plot_data_cpd.iter_rows(named=True):
261
+ node_id = row["entry_id"]
262
+ colors = [cols_cpd.filter(pl.col("id") == node_id)[c].item()
263
+ for c in color_cols if not cols_cpd.filter(pl.col("id") == node_id).is_empty()]
264
+ if not colors or all(c == "transparent" for c in colors):
265
+ colors = ["#DDDDFF"]
266
+ svg_code.append(
267
+ render_node_svg(
268
+ node_id=node_id,
269
+ x=row["x"],
270
+ y=row["y"],
271
+ width=row["width"],
272
+ height=row["height"],
273
+ shape=row.get("shape", "ellipse"),
274
+ label=row.get("label", ""),
275
+ fill_colors=colors,
276
+ )
277
+ )
278
+
279
+ # Add signature
280
+ if new_signature:
281
+ svg_code.append(
282
+ f'<text x="10" y="{canvas_height - 10}" '
283
+ f'style="font-size: 9px; fill: #666;">Rendered by pathview.py</text>'
284
+ )
285
+
286
+ svg_code.append(_svg_footer())
287
+
288
+ # Write to file
289
+ out_path = Path(kegg_dir) / f"{pathway_name}.{out_suffix}.svg"
290
+ out_path.write_text("\n".join(svg_code), encoding="utf-8")
291
+ print(f"Info: Written → {out_path}")
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # Utilities
296
+ # ---------------------------------------------------------------------------
297
+
298
+ def _escape_xml(text: str) -> str:
299
+ """Escape XML special characters."""
300
+ return (text
301
+ .replace("&", "&amp;")
302
+ .replace("<", "&lt;")
303
+ .replace(">", "&gt;")
304
+ .replace('"', "&quot;")
305
+ .replace("'", "&apos;"))
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ test_all_features.py
4
+ ====================
5
+ Comprehensive test suite demonstrating all pathview.py features.
6
+
7
+ Tests:
8
+ 1. KEGG pathway (PNG)
9
+ 2. KEGG pathway (SVG)
10
+ 3. Reactome pathway
11
+ 4. Multi-condition data
12
+ 5. Custom colors
13
+ 6. Gene symbols
14
+ 7. Compound overlay
15
+ 8. Spline curves
16
+ 9. Graph layout
17
+ 10. Highlighting (API demo)
18
+ """
19
+
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ import polars as pl
24
+
25
+ # Add pathview to path
26
+ sys.path.insert(0, str(Path(__file__).parent))
27
+
28
+ from pathview import (
29
+ catmull_rom_spline,
30
+ cubic_bezier,
31
+ pathview,
32
+ sim_mol_data,
33
+ )
34
+
35
+
36
+ def test_1_kegg_png():
37
+ """Test 1: Basic KEGG pathway with PNG output"""
38
+ print("\n" + "="*70)
39
+ print("TEST 1: KEGG Pathway (PNG)")
40
+ print("="*70)
41
+
42
+ gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
43
+
44
+ result = pathview(
45
+ pathway_id="04110",
46
+ gene_data=gene_df,
47
+ species="hsa",
48
+ output_format="png",
49
+ out_suffix="test1_png"
50
+ )
51
+
52
+ print("✓ Generated: hsa04110.test1_png.png")
53
+ return result is not None
54
+
55
+
56
+ def test_2_kegg_svg():
57
+ """Test 2: KEGG pathway with SVG vector output"""
58
+ print("\n" + "="*70)
59
+ print("TEST 2: KEGG Pathway (SVG Vector)")
60
+ print("="*70)
61
+
62
+ gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
63
+
64
+ result = pathview(
65
+ pathway_id="04110",
66
+ gene_data=gene_df,
67
+ species="hsa",
68
+ output_format="svg",
69
+ out_suffix="test2_svg"
70
+ )
71
+
72
+ print("✓ Generated: hsa04110.test2_svg.svg")
73
+ print(" → Scalable vector graphics")
74
+ return result is not None
75
+
76
+
77
+ def test_3_reactome():
78
+ """Test 3: Reactome SBGN pathway"""
79
+ print("\n" + "="*70)
80
+ print("TEST 3: Reactome SBGN Pathway")
81
+ print("="*70)
82
+ #TODO: Create test for this when server is working
83
+ print("⚠ Requires internet connection to download Reactome pathway")
84
+ print("⚠ Skipping for offline testing - see README for full example")
85
+ return True
86
+
87
+
88
+ def test_4_multi_condition():
89
+ """Test 4: Multi-condition visualization"""
90
+ print("\n" + "="*70)
91
+ print("TEST 4: Multi-Condition Visualization")
92
+ print("="*70)
93
+
94
+ gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=120, n_exp=3)
95
+ gene_df = gene_df.rename({
96
+ "exp1": "Control",
97
+ "exp2": "Treatment_A",
98
+ "exp3": "Treatment_B"
99
+ })
100
+
101
+ result = pathview(
102
+ pathway_id="04010",
103
+ gene_data=gene_df,
104
+ species="hsa",
105
+ out_suffix="test4_multi",
106
+ limit={"gene": 2.0, "cpd": 1.0}
107
+ )
108
+
109
+ print("✓ Generated: hsa04010.test4_multi.png")
110
+ print(" → Each node shows 3 colored slices")
111
+ return result is not None
112
+
113
+
114
+ def test_5_custom_colors():
115
+ """Test 5: Custom color scheme (ColorBrewer)"""
116
+ print("\n" + "="*70)
117
+ print("TEST 5: Custom Color Scheme")
118
+ print("="*70)
119
+
120
+ gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
121
+
122
+ result = pathview(
123
+ pathway_id="04151",
124
+ gene_data=gene_df,
125
+ species="hsa",
126
+ out_suffix="test5_colors",
127
+ low={"gene": "#2166AC", "cpd": "#4575B4"},
128
+ mid={"gene": "#F7F7F7", "cpd": "#F7F7F7"},
129
+ high={"gene": "#D6604D", "cpd": "#B2182B"},
130
+ limit={"gene": 2.5, "cpd": 1.5}
131
+ )
132
+
133
+ print("✓ Generated: hsa04151.test5_colors.png")
134
+ print(" → ColorBrewer RdBu diverging palette")
135
+ return result is not None
136
+
137
+
138
+ def test_6_gene_symbols():
139
+ """Test 6: Gene symbol IDs with auto-conversion"""
140
+ print("\n" + "="*70)
141
+ print("TEST 6: Gene Symbol IDs")
142
+ print("="*70)
143
+
144
+ gene_df = pl.DataFrame({
145
+ "symbol": ["TP53", "EGFR", "KRAS", "PIK3CA", "AKT1"],
146
+ "lfc": [-1.8, 2.4, 1.1, 1.5, 0.9]
147
+ })
148
+
149
+ result = pathview(
150
+ pathway_id="04151",
151
+ gene_data=gene_df,
152
+ species="hsa",
153
+ gene_idtype="SYMBOL",
154
+ out_suffix="test6_symbols"
155
+ )
156
+
157
+ print("✓ Generated: hsa04151.test6_symbols.png")
158
+ print(" → Symbols auto-converted via MyGene.info")
159
+ return result is not None
160
+
161
+
162
+ def test_7_compound_overlay():
163
+ """Test 7: Gene + compound combined overlay"""
164
+ print("\n" + "="*70)
165
+ print("TEST 7: Gene + Compound Overlay")
166
+ print("="*70)
167
+
168
+ gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=80, n_exp=1)
169
+ cpd_df = sim_mol_data(mol_type="cpd", n_mol=30, n_exp=1)
170
+
171
+ result = pathview(
172
+ pathway_id="00010",
173
+ gene_data=gene_df,
174
+ cpd_data=cpd_df,
175
+ species="hsa",
176
+ out_suffix="test7_gene_cpd",
177
+ limit={"gene": 2.0, "cpd": 1.5}
178
+ )
179
+
180
+ print("✓ Generated: hsa00010.test7_gene_cpd.png")
181
+ print(" → Glycolysis with proteomics + metabolomics")
182
+ return result is not None
183
+
184
+
185
+ def test_8_splines():
186
+ """Test 8: Spline curve generation"""
187
+ print("\n" + "="*70)
188
+ print("TEST 8: Spline Curves (Bezier)")
189
+ print("="*70)
190
+
191
+ try:
192
+ import matplotlib.pyplot as plt
193
+
194
+ # Generate cubic Bezier
195
+ curve = cubic_bezier(
196
+ p0=(0, 0),
197
+ p1=(1, 2),
198
+ p2=(3, 2),
199
+ p3=(4, 0),
200
+ n_points=100
201
+ )
202
+
203
+ # Generate Catmull-Rom spline
204
+ smooth = catmull_rom_spline(
205
+ [(0, 0), (1, 2), (3, 1), (4, 3)],
206
+ n_points=50
207
+ )
208
+
209
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
210
+
211
+ ax1.plot(curve[:, 0], curve[:, 1], 'b-', linewidth=2, label='Cubic Bezier')
212
+ ax1.plot([0, 1, 3, 4], [0, 2, 2, 0], 'ro--', alpha=0.5, label='Control points')
213
+ ax1.set_title("Cubic Bezier Curve")
214
+ ax1.legend()
215
+ ax1.grid(True, alpha=0.3)
216
+
217
+ ax2.plot(smooth[:, 0], smooth[:, 1], 'g-', linewidth=2, label='Catmull-Rom')
218
+ ax2.plot([0, 1, 3, 4], [0, 2, 1, 3], 'ro-', alpha=0.5, label='Control points')
219
+ ax2.set_title("Catmull-Rom Spline")
220
+ ax2.legend()
221
+ ax2.grid(True, alpha=0.3)
222
+
223
+ plt.tight_layout()
224
+ plt.savefig("test8_splines.png", dpi=150)
225
+ plt.close()
226
+
227
+ print("✓ Generated: test8_splines.png")
228
+ print(" → Smooth curve demonstrations")
229
+ return True
230
+ except Exception as e:
231
+ print(f"⚠ Skipping matplotlib test: {e}")
232
+ return True
233
+
234
+
235
+ def test_9_graph_layout():
236
+ """Test 9: Graph layout mode (PDF)"""
237
+ print("\n" + "="*70)
238
+ print("TEST 9: Graph Layout Mode (PDF)")
239
+ print("="*70)
240
+
241
+ gene_df = sim_mol_data(mol_type="gene", species="hsa", n_mol=100, n_exp=1)
242
+
243
+ result = pathview(
244
+ pathway_id="04010",
245
+ gene_data=gene_df,
246
+ species="hsa",
247
+ kegg_native=False,
248
+ output_format="pdf",
249
+ out_suffix="test9_graph"
250
+ )
251
+
252
+ print("✓ Generated: hsa04010.test9_graph.pdf")
253
+ print(" → NetworkX layout with Seaborn styling")
254
+ return result is not None
255
+
256
+
257
+ def test_10_highlighting():
258
+ """Test 10: Highlighting API demonstration"""
259
+ print("\n" + "="*70)
260
+ print("TEST 10: Highlighting API (Preview)")
261
+ print("="*70)
262
+
263
+ gene_df = pl.DataFrame({
264
+ "entrez": ["1956", "2099", "5594", "207"],
265
+ "lfc": [ 2.3, -1.1, 1.8, -0.5]
266
+ })
267
+
268
+ result = pathview(
269
+ pathway_id="04010",
270
+ gene_data=gene_df,
271
+ species="hsa",
272
+ out_suffix="test10_base"
273
+ )
274
+
275
+ print("✓ Generated: hsa04010.test10_base.png")
276
+ print("⚠ Full highlighting implementation:")
277
+ print(" from pathview import highlight_nodes, highlight_edges")
278
+ print(" highlighted = result + highlight_nodes([...]) + highlight_edges([...])")
279
+ return result is not None
280
+
281
+
282
+ def main():
283
+ """Run all tests"""
284
+ print("\n" + "="*70)
285
+ print("PATHVIEW.PY COMPREHENSIVE FEATURE TESTS")
286
+ print("="*70)
287
+ print("\nTesting all features:")
288
+ print(" • KEGG pathways (PNG, SVG, PDF)")
289
+ print(" • Multi-condition visualization")
290
+ print(" • Custom color schemes")
291
+ print(" • Gene symbol IDs")
292
+ print(" • Compound overlays")
293
+ print(" • Spline curves")
294
+ print(" • Graph layouts")
295
+ print(" • Highlighting API")
296
+
297
+ tests = [
298
+ test_1_kegg_png,
299
+ test_2_kegg_svg,
300
+ test_3_reactome,
301
+ test_4_multi_condition,
302
+ test_5_custom_colors,
303
+ test_6_gene_symbols,
304
+ test_7_compound_overlay,
305
+ test_8_splines,
306
+ test_9_graph_layout,
307
+ test_10_highlighting,
308
+ ]
309
+
310
+ results = []
311
+ for i, test in enumerate(tests, 1):
312
+ try:
313
+ passed = test()
314
+ results.append((i, test.__doc__.split('\n')[0], passed))
315
+ except Exception as e:
316
+ print(f"\n✗ Test {i} failed: {e}")
317
+ import traceback
318
+ traceback.print_exc()
319
+ results.append((i, test.__doc__.split('\n')[0], False))
320
+
321
+ # Summary
322
+ print("\n" + "="*70)
323
+ print("TEST SUMMARY")
324
+ print("="*70)
325
+ for num, name, passed in results:
326
+ status = "✓ PASS" if passed else "✗ FAIL"
327
+ print(f" {status} {name}")
328
+
329
+ passed_count = sum(1 for _, _, p in results if p)
330
+ print("="*70)
331
+ print(f"Results: {passed_count}/{len(results)} tests passed")
332
+ print("="*70)
333
+
334
+ if passed_count == len(results):
335
+ print("\n🎉 All tests passed!")
336
+ return 0
337
+ else:
338
+ print(f"\n⚠ {len(results) - passed_count} test(s) failed")
339
+ return 1
340
+
341
+
342
+ if __name__ == "__main__":
343
+ sys.exit(main())