natural-pdf 0.1.28__py3-none-any.whl → 0.1.31__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 (49) hide show
  1. bad_pdf_analysis/analyze_10_more.py +300 -0
  2. bad_pdf_analysis/analyze_final_10.py +552 -0
  3. bad_pdf_analysis/analyze_specific_pages.py +394 -0
  4. bad_pdf_analysis/analyze_specific_pages_direct.py +382 -0
  5. natural_pdf/analyzers/layout/layout_analyzer.py +2 -3
  6. natural_pdf/analyzers/layout/layout_manager.py +44 -0
  7. natural_pdf/analyzers/layout/surya.py +1 -1
  8. natural_pdf/analyzers/shape_detection_mixin.py +228 -0
  9. natural_pdf/classification/manager.py +67 -0
  10. natural_pdf/core/element_manager.py +578 -27
  11. natural_pdf/core/highlighting_service.py +98 -43
  12. natural_pdf/core/page.py +86 -20
  13. natural_pdf/core/pdf.py +0 -2
  14. natural_pdf/describe/base.py +40 -9
  15. natural_pdf/describe/elements.py +11 -6
  16. natural_pdf/elements/base.py +134 -20
  17. natural_pdf/elements/collections.py +43 -11
  18. natural_pdf/elements/image.py +43 -0
  19. natural_pdf/elements/region.py +64 -19
  20. natural_pdf/elements/text.py +118 -11
  21. natural_pdf/flows/collections.py +4 -4
  22. natural_pdf/flows/region.py +17 -2
  23. natural_pdf/ocr/ocr_manager.py +50 -0
  24. natural_pdf/selectors/parser.py +27 -7
  25. natural_pdf/tables/__init__.py +5 -0
  26. natural_pdf/tables/result.py +101 -0
  27. natural_pdf/utils/bidi_mirror.py +36 -0
  28. natural_pdf/utils/visualization.py +15 -1
  29. {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/METADATA +2 -1
  30. {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/RECORD +48 -26
  31. natural_pdf-0.1.31.dist-info/top_level.txt +6 -0
  32. optimization/memory_comparison.py +172 -0
  33. optimization/pdf_analyzer.py +410 -0
  34. optimization/performance_analysis.py +397 -0
  35. optimization/test_cleanup_methods.py +155 -0
  36. optimization/test_memory_fix.py +162 -0
  37. tools/bad_pdf_eval/__init__.py +1 -0
  38. tools/bad_pdf_eval/analyser.py +302 -0
  39. tools/bad_pdf_eval/collate_summaries.py +130 -0
  40. tools/bad_pdf_eval/eval_suite.py +116 -0
  41. tools/bad_pdf_eval/export_enrichment_csv.py +62 -0
  42. tools/bad_pdf_eval/llm_enrich.py +273 -0
  43. tools/bad_pdf_eval/reporter.py +17 -0
  44. tools/bad_pdf_eval/utils.py +127 -0
  45. tools/rtl_smoke_test.py +80 -0
  46. natural_pdf-0.1.28.dist-info/top_level.txt +0 -2
  47. {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/WHEEL +0 -0
  48. {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/entry_points.txt +0 -0
  49. {natural_pdf-0.1.28.dist-info → natural_pdf-0.1.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Natural PDF Performance Analysis Micro-Suite
4
+
5
+ This script analyzes memory usage and performance characteristics of Natural PDF
6
+ operations using real large PDFs to inform memory management decisions.
7
+ """
8
+
9
+ import gc
10
+ import json
11
+ import os
12
+ import psutil
13
+ import sys
14
+ import time
15
+ import tracemalloc
16
+ from dataclasses import dataclass, asdict
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional, Any, Callable
19
+ import pandas as pd
20
+ import matplotlib.pyplot as plt
21
+
22
+ import natural_pdf as npdf
23
+
24
+
25
+ @dataclass
26
+ class MemorySnapshot:
27
+ """Snapshot of memory usage at a point in time"""
28
+ timestamp: float
29
+ rss_mb: float # Resident Set Size
30
+ vms_mb: float # Virtual Memory Size
31
+ python_objects: int
32
+ operation: str
33
+ page_count: int
34
+ pdf_name: str
35
+ additional_info: Dict[str, Any]
36
+
37
+
38
+ class PerformanceProfiler:
39
+ """Profiles memory usage and performance of Natural PDF operations"""
40
+
41
+ def __init__(self, output_dir: str = "performance_results"):
42
+ self.output_dir = Path(output_dir)
43
+ self.output_dir.mkdir(exist_ok=True)
44
+
45
+ self.snapshots: List[MemorySnapshot] = []
46
+ self.process = psutil.Process()
47
+ self.start_time = time.time()
48
+
49
+ # Start tracemalloc for detailed Python memory tracking
50
+ tracemalloc.start()
51
+
52
+ def take_snapshot(self, operation: str, page_count: int = 0,
53
+ pdf_name: str = "", **additional_info):
54
+ """Take a memory usage snapshot"""
55
+ gc.collect() # Force garbage collection for accurate measurement
56
+
57
+ memory_info = self.process.memory_info()
58
+ python_objects = len(gc.get_objects())
59
+
60
+ snapshot = MemorySnapshot(
61
+ timestamp=time.time() - self.start_time,
62
+ rss_mb=memory_info.rss / 1024 / 1024,
63
+ vms_mb=memory_info.vms / 1024 / 1024,
64
+ python_objects=python_objects,
65
+ operation=operation,
66
+ page_count=page_count,
67
+ pdf_name=pdf_name,
68
+ additional_info=additional_info
69
+ )
70
+
71
+ self.snapshots.append(snapshot)
72
+ print(f"[{snapshot.timestamp:.1f}s] {operation}: {snapshot.rss_mb:.1f}MB RSS, {python_objects} objects")
73
+
74
+ def save_results(self, test_name: str):
75
+ """Save results to JSON and CSV"""
76
+ # Convert to list of dicts for JSON serialization
77
+ data = [asdict(s) for s in self.snapshots]
78
+
79
+ # Save JSON
80
+ json_path = self.output_dir / f"{test_name}_snapshots.json"
81
+ with open(json_path, 'w') as f:
82
+ json.dump(data, f, indent=2)
83
+
84
+ # Save CSV for easy analysis
85
+ df = pd.DataFrame(data)
86
+ csv_path = self.output_dir / f"{test_name}_snapshots.csv"
87
+ df.to_csv(csv_path, index=False)
88
+
89
+ print(f"Results saved to {json_path} and {csv_path}")
90
+ return df
91
+
92
+
93
+ class PDFPerformanceTester:
94
+ """Tests specific PDF operations and measures their performance"""
95
+
96
+ def __init__(self, pdf_path: str, profiler: PerformanceProfiler):
97
+ self.pdf_path = Path(pdf_path)
98
+ self.pdf_name = self.pdf_path.stem
99
+ self.profiler = profiler
100
+ self.pdf = None
101
+
102
+ def test_load_pdf(self):
103
+ """Test just loading the PDF"""
104
+ self.profiler.take_snapshot("before_load", pdf_name=self.pdf_name)
105
+
106
+ self.pdf = npdf.PDF(str(self.pdf_path))
107
+
108
+ self.profiler.take_snapshot("after_load", pdf_name=self.pdf_name,
109
+ total_pages=len(self.pdf.pages))
110
+
111
+ def test_page_access(self, max_pages: int = 10):
112
+ """Test accessing pages sequentially"""
113
+ if not self.pdf:
114
+ self.test_load_pdf()
115
+
116
+ pages_to_test = min(max_pages, len(self.pdf.pages))
117
+
118
+ for i in range(pages_to_test):
119
+ page = self.pdf.pages[i]
120
+
121
+ # Just access the page to trigger lazy loading
122
+ _ = page.width, page.height
123
+
124
+ self.profiler.take_snapshot(
125
+ f"page_access_{i+1}",
126
+ page_count=i+1,
127
+ pdf_name=self.pdf_name,
128
+ page_width=page.width,
129
+ page_height=page.height
130
+ )
131
+
132
+ def test_describe_pages(self, max_pages: int = 5):
133
+ """Test using .describe() on pages"""
134
+ if not self.pdf:
135
+ self.test_load_pdf()
136
+
137
+ pages_to_test = min(max_pages, len(self.pdf.pages))
138
+
139
+ for i in range(pages_to_test):
140
+ page = self.pdf.pages[i]
141
+
142
+ # Use describe to understand page content
143
+ try:
144
+ description = page.describe()
145
+
146
+ self.profiler.take_snapshot(
147
+ f"describe_{i+1}",
148
+ page_count=i+1,
149
+ pdf_name=self.pdf_name,
150
+ description_length=len(description) if description else 0
151
+ )
152
+ except Exception as e:
153
+ self.profiler.take_snapshot(
154
+ f"describe_{i+1}_error",
155
+ page_count=i+1,
156
+ pdf_name=self.pdf_name,
157
+ error=str(e)
158
+ )
159
+
160
+ def test_element_collections(self, max_pages: int = 5):
161
+ """Test find_all operations that create element collections"""
162
+ if not self.pdf:
163
+ self.test_load_pdf()
164
+
165
+ pages_to_test = min(max_pages, len(self.pdf.pages))
166
+
167
+ for i in range(pages_to_test):
168
+ page = self.pdf.pages[i]
169
+
170
+ # Test different element collection operations
171
+ operations = [
172
+ ("words", lambda p: p.find_all("words")),
173
+ ("text_elements", lambda p: p.find_all("text")),
174
+ ("rects", lambda p: p.find_all("rect")),
175
+ ("large_text", lambda p: p.find_all("text[size>12]")),
176
+ ]
177
+
178
+ for op_name, operation in operations:
179
+ try:
180
+ elements = operation(page)
181
+ element_count = len(elements) if elements else 0
182
+
183
+ self.profiler.take_snapshot(
184
+ f"{op_name}_{i+1}",
185
+ page_count=i+1,
186
+ pdf_name=self.pdf_name,
187
+ operation_type=op_name,
188
+ element_count=element_count
189
+ )
190
+ except Exception as e:
191
+ self.profiler.take_snapshot(
192
+ f"{op_name}_{i+1}_error",
193
+ page_count=i+1,
194
+ pdf_name=self.pdf_name,
195
+ operation_type=op_name,
196
+ error=str(e)
197
+ )
198
+
199
+ def test_image_generation(self, max_pages: int = 3, resolutions: List[int] = [72, 144, 216]):
200
+ """Test image generation at different resolutions"""
201
+ if not self.pdf:
202
+ self.test_load_pdf()
203
+
204
+ pages_to_test = min(max_pages, len(self.pdf.pages))
205
+
206
+ for i in range(pages_to_test):
207
+ page = self.pdf.pages[i]
208
+
209
+ for resolution in resolutions:
210
+ try:
211
+ img = page.to_image(resolution=resolution)
212
+
213
+ self.profiler.take_snapshot(
214
+ f"image_{resolution}dpi_{i+1}",
215
+ page_count=i+1,
216
+ pdf_name=self.pdf_name,
217
+ resolution=resolution,
218
+ image_size=f"{img.width}x{img.height}" if img else "None"
219
+ )
220
+
221
+ # Clean up image immediately to test memory release
222
+ del img
223
+
224
+ except Exception as e:
225
+ self.profiler.take_snapshot(
226
+ f"image_{resolution}dpi_{i+1}_error",
227
+ page_count=i+1,
228
+ pdf_name=self.pdf_name,
229
+ resolution=resolution,
230
+ error=str(e)
231
+ )
232
+
233
+ def test_ocr(self, max_pages: int = 2):
234
+ """Test OCR operations (expensive!)"""
235
+ if not self.pdf:
236
+ self.test_load_pdf()
237
+
238
+ pages_to_test = min(max_pages, len(self.pdf.pages))
239
+
240
+ for i in range(pages_to_test):
241
+ page = self.pdf.pages[i]
242
+
243
+ try:
244
+ # Run OCR
245
+ page.apply_ocr(engine="easyocr") # Default engine
246
+
247
+ self.profiler.take_snapshot(
248
+ f"ocr_{i+1}",
249
+ page_count=i+1,
250
+ pdf_name=self.pdf_name,
251
+ operation_type="ocr"
252
+ )
253
+
254
+ except Exception as e:
255
+ self.profiler.take_snapshot(
256
+ f"ocr_{i+1}_error",
257
+ page_count=i+1,
258
+ pdf_name=self.pdf_name,
259
+ operation_type="ocr",
260
+ error=str(e)
261
+ )
262
+
263
+ def test_layout_analysis(self, max_pages: int = 3):
264
+ """Test layout analysis operations"""
265
+ if not self.pdf:
266
+ self.test_load_pdf()
267
+
268
+ pages_to_test = min(max_pages, len(self.pdf.pages))
269
+
270
+ for i in range(pages_to_test):
271
+ page = self.pdf.pages[i]
272
+
273
+ try:
274
+ # Run layout analysis
275
+ layout_result = page.analyze_layout()
276
+
277
+ self.profiler.take_snapshot(
278
+ f"layout_{i+1}",
279
+ page_count=i+1,
280
+ pdf_name=self.pdf_name,
281
+ operation_type="layout",
282
+ layout_regions=len(layout_result) if layout_result else 0
283
+ )
284
+
285
+ except Exception as e:
286
+ self.profiler.take_snapshot(
287
+ f"layout_{i+1}_error",
288
+ page_count=i+1,
289
+ pdf_name=self.pdf_name,
290
+ operation_type="layout",
291
+ error=str(e)
292
+ )
293
+
294
+
295
+ def run_comprehensive_test(pdf_path: str, test_name: str):
296
+ """Run a comprehensive test suite on a PDF"""
297
+ print(f"\n{'='*60}")
298
+ print(f"COMPREHENSIVE TEST: {test_name}")
299
+ print(f"PDF: {pdf_path}")
300
+ print(f"{'='*60}")
301
+
302
+ profiler = PerformanceProfiler()
303
+ tester = PDFPerformanceTester(pdf_path, profiler)
304
+
305
+ # Initial baseline
306
+ profiler.take_snapshot("baseline_start", pdf_name=Path(pdf_path).stem)
307
+
308
+ # Test sequence
309
+ print("\n1. Testing PDF Load...")
310
+ tester.test_load_pdf()
311
+
312
+ print("\n2. Testing Page Access...")
313
+ tester.test_page_access(max_pages=10)
314
+
315
+ print("\n3. Testing Describe Operations...")
316
+ tester.test_describe_pages(max_pages=5)
317
+
318
+ print("\n4. Testing Element Collections...")
319
+ tester.test_element_collections(max_pages=5)
320
+
321
+ print("\n5. Testing Image Generation...")
322
+ tester.test_image_generation(max_pages=3)
323
+
324
+ print("\n6. Testing Layout Analysis...")
325
+ tester.test_layout_analysis(max_pages=3)
326
+
327
+ # OCR test (only for image-heavy PDFs)
328
+ if "OCR" in pdf_path or "image" in test_name.lower():
329
+ print("\n7. Testing OCR (Image-heavy PDF)...")
330
+ tester.test_ocr(max_pages=2)
331
+
332
+ # Final snapshot
333
+ profiler.take_snapshot("test_complete", pdf_name=Path(pdf_path).stem)
334
+
335
+ # Save results
336
+ df = profiler.save_results(test_name)
337
+
338
+ # Quick analysis
339
+ print(f"\n{'-'*40}")
340
+ print("QUICK ANALYSIS:")
341
+ print(f"Peak Memory: {df['rss_mb'].max():.1f} MB")
342
+ print(f"Memory Growth: {df['rss_mb'].iloc[-1] - df['rss_mb'].iloc[0]:.1f} MB")
343
+ print(f"Peak Objects: {df['python_objects'].max():,}")
344
+ print(f"Total Time: {df['timestamp'].iloc[-1]:.1f} seconds")
345
+
346
+ return df
347
+
348
+
349
+ def main():
350
+ """Main test runner"""
351
+ print("Natural PDF Performance Analysis Micro-Suite")
352
+ print("=" * 50)
353
+
354
+ # Find test PDFs
355
+ large_pdfs_dir = Path("pdfs/hidden/large")
356
+ if not large_pdfs_dir.exists():
357
+ print(f"Error: {large_pdfs_dir} not found")
358
+ print("Please ensure large test PDFs are available")
359
+ return
360
+
361
+ # Expected test PDFs
362
+ test_pdfs = {
363
+ "text_heavy": large_pdfs_dir / "appendix_fy2026.pdf",
364
+ "image_heavy": large_pdfs_dir / "OCR 0802030-56.2022.8.14.0060_Cópia integral_Fazenda Marrocos.pdf"
365
+ }
366
+
367
+ results = {}
368
+
369
+ for test_name, pdf_path in test_pdfs.items():
370
+ if pdf_path.exists():
371
+ try:
372
+ results[test_name] = run_comprehensive_test(str(pdf_path), test_name)
373
+ except Exception as e:
374
+ print(f"Error testing {test_name}: {e}")
375
+ traceback.print_exc()
376
+ else:
377
+ print(f"Warning: {pdf_path} not found, skipping {test_name} test")
378
+
379
+ # Generate comparison report
380
+ if results:
381
+ print(f"\n{'='*60}")
382
+ print("COMPARISON SUMMARY")
383
+ print(f"{'='*60}")
384
+
385
+ for test_name, df in results.items():
386
+ print(f"\n{test_name.upper()}:")
387
+ print(f" Peak Memory: {df['rss_mb'].max():.1f} MB")
388
+ print(f" Memory Growth: {df['rss_mb'].iloc[-1] - df['rss_mb'].iloc[0]:.1f} MB")
389
+ print(f" Peak Objects: {df['python_objects'].max():,}")
390
+ print(f" Duration: {df['timestamp'].iloc[-1]:.1f}s")
391
+
392
+ print(f"\nResults saved to performance_results/ directory")
393
+ print("Use the CSV files for detailed analysis")
394
+
395
+
396
+ if __name__ == "__main__":
397
+ main()
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify the new cleanup methods work correctly.
4
+
5
+ This test verifies that:
6
+ 1. Cleanup methods exist and are callable
7
+ 2. They handle edge cases gracefully (empty caches, missing engines)
8
+ 3. They actually clean up loaded models/engines
9
+ """
10
+
11
+ import gc
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+ import pytest
16
+
17
+ import natural_pdf as npdf
18
+ from natural_pdf.ocr.ocr_manager import OCRManager
19
+ from natural_pdf.analyzers.layout.layout_manager import LayoutManager
20
+ from natural_pdf.classification.manager import ClassificationManager
21
+
22
+
23
+ class TestCleanupMethods:
24
+ """Test suite for manager cleanup methods"""
25
+
26
+ def test_ocr_manager_cleanup_empty(self):
27
+ """Test OCR manager cleanup when no engines are loaded"""
28
+ manager = OCRManager()
29
+
30
+ # Test cleanup when nothing is loaded
31
+ count = manager.cleanup_engine()
32
+ assert count == 0, "Should return 0 when no engines loaded"
33
+
34
+ # Test cleanup of specific non-existent engine
35
+ count = manager.cleanup_engine("nonexistent")
36
+ assert count == 0, "Should return 0 when engine doesn't exist"
37
+
38
+ def test_layout_manager_cleanup_empty(self):
39
+ """Test Layout manager cleanup when no detectors are loaded"""
40
+ manager = LayoutManager()
41
+
42
+ # Test cleanup when nothing is loaded
43
+ count = manager.cleanup_detector()
44
+ assert count == 0, "Should return 0 when no detectors loaded"
45
+
46
+ # Test cleanup of specific non-existent detector
47
+ count = manager.cleanup_detector("nonexistent")
48
+ assert count == 0, "Should return 0 when detector doesn't exist"
49
+
50
+ def test_classification_manager_cleanup_empty(self):
51
+ """Test Classification manager cleanup when no models are loaded"""
52
+ try:
53
+ manager = ClassificationManager()
54
+
55
+ # Test cleanup when nothing is loaded
56
+ count = manager.cleanup_models()
57
+ assert count == 0, "Should return 0 when no models loaded"
58
+
59
+ # Test cleanup of specific non-existent model
60
+ count = manager.cleanup_models("nonexistent/model")
61
+ assert count == 0, "Should return 0 when model doesn't exist"
62
+
63
+ except ImportError:
64
+ pytest.skip("Classification dependencies not available")
65
+
66
+ def test_ocr_manager_cleanup_with_engine(self):
67
+ """Test OCR manager cleanup after loading an engine"""
68
+ manager = OCRManager()
69
+
70
+ # Check if any OCR engines are available
71
+ available_engines = manager.get_available_engines()
72
+ if not available_engines:
73
+ pytest.skip("No OCR engines available for testing")
74
+
75
+ engine_name = available_engines[0]
76
+ print(f"Testing with OCR engine: {engine_name}")
77
+
78
+ # Load an engine by accessing it
79
+ try:
80
+ engine_instance = manager._get_engine_instance(engine_name)
81
+ assert engine_name in manager._engine_instances, "Engine should be cached"
82
+
83
+ # Test cleanup of specific engine
84
+ count = manager.cleanup_engine(engine_name)
85
+ assert count == 1, f"Should return 1 after cleaning up {engine_name}"
86
+ assert engine_name not in manager._engine_instances, "Engine should be removed from cache"
87
+
88
+ except Exception as e:
89
+ pytest.skip(f"Could not load {engine_name} engine: {e}")
90
+
91
+ def test_layout_manager_cleanup_with_detector(self):
92
+ """Test Layout manager cleanup after loading a detector"""
93
+ manager = LayoutManager()
94
+
95
+ # Check if any layout engines are available
96
+ available_engines = manager.get_available_engines()
97
+ if not available_engines:
98
+ pytest.skip("No layout engines available for testing")
99
+
100
+ engine_name = available_engines[0]
101
+ print(f"Testing with layout engine: {engine_name}")
102
+
103
+ # Load a detector by accessing it
104
+ try:
105
+ detector_instance = manager._get_engine_instance(engine_name)
106
+ assert engine_name in manager._detector_instances, "Detector should be cached"
107
+
108
+ # Test cleanup of specific detector
109
+ count = manager.cleanup_detector(engine_name)
110
+ assert count == 1, f"Should return 1 after cleaning up {engine_name}"
111
+ assert engine_name not in manager._detector_instances, "Detector should be removed from cache"
112
+
113
+ except Exception as e:
114
+ pytest.skip(f"Could not load {engine_name} detector: {e}")
115
+
116
+ def test_methods_exist(self):
117
+ """Test that all cleanup methods exist and are callable"""
118
+ # Test OCRManager
119
+ manager = OCRManager()
120
+ assert hasattr(manager, 'cleanup_engine'), "OCRManager should have cleanup_engine method"
121
+ assert callable(manager.cleanup_engine), "cleanup_engine should be callable"
122
+
123
+ # Test LayoutManager
124
+ layout_manager = LayoutManager()
125
+ assert hasattr(layout_manager, 'cleanup_detector'), "LayoutManager should have cleanup_detector method"
126
+ assert callable(layout_manager.cleanup_detector), "cleanup_detector should be callable"
127
+
128
+ # Test ClassificationManager (if available)
129
+ try:
130
+ classification_manager = ClassificationManager()
131
+ assert hasattr(classification_manager, 'cleanup_models'), "ClassificationManager should have cleanup_models method"
132
+ assert callable(classification_manager.cleanup_models), "cleanup_models should be callable"
133
+ except ImportError:
134
+ print("Classification dependencies not available, skipping ClassificationManager test")
135
+
136
+
137
+ def main():
138
+ """Run the cleanup method tests"""
139
+ print("Testing manager cleanup methods...")
140
+
141
+ # Run pytest on just this file
142
+ exit_code = pytest.main([__file__, "-v", "-s"])
143
+
144
+ if exit_code == 0:
145
+ print("\n✅ All cleanup method tests passed!")
146
+ print("The memory management methods are working correctly.")
147
+ else:
148
+ print("\n❌ Some tests failed!")
149
+ print("The cleanup methods need investigation.")
150
+
151
+ return exit_code
152
+
153
+
154
+ if __name__ == "__main__":
155
+ exit(main())