reqora 0.1.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.
- auto_req/__init__.py +0 -0
- auto_req/analyzer.py +305 -0
- auto_req/cli.py +165 -0
- auto_req/installer.py +467 -0
- auto_req/mapper.py +1134 -0
- auto_req/page_utils.py +18 -0
- auto_req/pytest_plugin.py +61 -0
- reqora-0.1.0.dist-info/METADATA +552 -0
- reqora-0.1.0.dist-info/RECORD +12 -0
- reqora-0.1.0.dist-info/WHEEL +5 -0
- reqora-0.1.0.dist-info/entry_points.txt +2 -0
- reqora-0.1.0.dist-info/top_level.txt +1 -0
auto_req/__init__.py
ADDED
|
File without changes
|
auto_req/analyzer.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# import ast
|
|
2
|
+
# import sys
|
|
3
|
+
# from pathlib import Path
|
|
4
|
+
# from typing import Set
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# class CodeAnalyzer:
|
|
8
|
+
# """Analyzes Python code to extract top-level third-party imported modules."""
|
|
9
|
+
|
|
10
|
+
# def __init__(self):
|
|
11
|
+
# # Retrieve all standard library modules for the current Python runtime
|
|
12
|
+
# self.stdlib_modules: Set[str] = set(sys.stdlib_module_names)
|
|
13
|
+
|
|
14
|
+
# def extract_imports_from_file(self, file_path: Path) -> Set[str]:
|
|
15
|
+
# """Parses a single .py file and returns non-stdlib top-level import names."""
|
|
16
|
+
# if not file_path.is_file() or file_path.suffix != ".py":
|
|
17
|
+
# return set()
|
|
18
|
+
|
|
19
|
+
# try:
|
|
20
|
+
# with open(file_path, "r", encoding="utf-8") as f:
|
|
21
|
+
# tree = ast.parse(f.read(), filename=str(file_path))
|
|
22
|
+
# except (SyntaxError, UnicodeDecodeError):
|
|
23
|
+
# return set()
|
|
24
|
+
|
|
25
|
+
# modules = set()
|
|
26
|
+
# for node in ast.walk(tree):
|
|
27
|
+
# if isinstance(node, ast.Import):
|
|
28
|
+
# for alias in node.names:
|
|
29
|
+
# # Capture top-level package (e.g., 'os.path' -> 'os')
|
|
30
|
+
# modules.add(alias.name.split(".")[0])
|
|
31
|
+
# elif isinstance(node, ast.ImportFrom):
|
|
32
|
+
# # Ensure it's an absolute import (level == 0)
|
|
33
|
+
# if node.module and node.level == 0:
|
|
34
|
+
# modules.add(node.module.split(".")[0])
|
|
35
|
+
|
|
36
|
+
# # Exclude standard library modules
|
|
37
|
+
# return modules - self.stdlib_modules
|
|
38
|
+
|
|
39
|
+
# def scan_directory(self, target_dir: Path) -> Set[str]:
|
|
40
|
+
# """Recursively scans a directory for Python files and aggregates imports."""
|
|
41
|
+
# all_imports = set()
|
|
42
|
+
# for py_file in target_dir.rglob("*.py"):
|
|
43
|
+
# # Ignore virtual environment directories
|
|
44
|
+
# if any(part.startswith((".", "venv", "env")) for part in py_file.parts):
|
|
45
|
+
# continue
|
|
46
|
+
# all_imports.update(self.extract_imports_from_file(py_file))
|
|
47
|
+
# return all_imports
|
|
48
|
+
|
|
49
|
+
import ast
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
from typing import Iterable, Optional, Set, Tuple
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CodeAnalyzer:
|
|
57
|
+
"""Analyze Python imports, including dependencies hidden behind local modules."""
|
|
58
|
+
|
|
59
|
+
PROJECT_MARKERS = (
|
|
60
|
+
"pyproject.toml",
|
|
61
|
+
"setup.py",
|
|
62
|
+
"setup.cfg",
|
|
63
|
+
"pytest.ini",
|
|
64
|
+
"tox.ini",
|
|
65
|
+
".git",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
SKIP_DIR_NAMES = {
|
|
69
|
+
".git",
|
|
70
|
+
".hg",
|
|
71
|
+
".svn",
|
|
72
|
+
"__pycache__",
|
|
73
|
+
".pytest_cache",
|
|
74
|
+
".mypy_cache",
|
|
75
|
+
".ruff_cache",
|
|
76
|
+
"build",
|
|
77
|
+
"dist",
|
|
78
|
+
"node_modules",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
82
|
+
self.stdlib_modules: Set[str] = set(sys.stdlib_module_names)
|
|
83
|
+
self.project_root = Path(project_root).resolve() if project_root else None
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def discover_project_root(cls, target: Path) -> Path:
|
|
87
|
+
"""Return the nearest ancestor that looks like the project root."""
|
|
88
|
+
target = Path(target).resolve()
|
|
89
|
+
start = target.parent if target.is_file() else target
|
|
90
|
+
|
|
91
|
+
for candidate in (start, *start.parents):
|
|
92
|
+
if any((candidate / marker).exists() for marker in cls.PROJECT_MARKERS):
|
|
93
|
+
return candidate
|
|
94
|
+
return start
|
|
95
|
+
|
|
96
|
+
def _ensure_project_root(self, target: Path) -> Path:
|
|
97
|
+
if self.project_root is None:
|
|
98
|
+
self.project_root = self.discover_project_root(target)
|
|
99
|
+
return self.project_root
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _looks_like_venv(path: Path) -> bool:
|
|
103
|
+
"""Detect a virtual environment by structure, not by directory name."""
|
|
104
|
+
if not path.is_dir():
|
|
105
|
+
return False
|
|
106
|
+
if (path / "pyvenv.cfg").is_file():
|
|
107
|
+
return True
|
|
108
|
+
return (
|
|
109
|
+
(path / "Scripts" / "python.exe").is_file()
|
|
110
|
+
or (path / "bin" / "python").is_file()
|
|
111
|
+
) and (
|
|
112
|
+
(path / "Lib" / "site-packages").is_dir()
|
|
113
|
+
or (path / "lib").is_dir()
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _source_roots(self, current_file: Optional[Path] = None) -> Tuple[Path, ...]:
|
|
117
|
+
root = self.project_root or (current_file.parent if current_file else Path.cwd())
|
|
118
|
+
roots = []
|
|
119
|
+
for candidate in (
|
|
120
|
+
current_file.parent if current_file else None,
|
|
121
|
+
root,
|
|
122
|
+
root / "src",
|
|
123
|
+
):
|
|
124
|
+
if candidate and candidate.is_dir() and candidate not in roots:
|
|
125
|
+
roots.append(candidate)
|
|
126
|
+
return tuple(roots)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _module_candidates(base: Path, module_name: str) -> Iterable[Path]:
|
|
130
|
+
if not module_name:
|
|
131
|
+
return ()
|
|
132
|
+
module_path = base.joinpath(*module_name.split("."))
|
|
133
|
+
return (
|
|
134
|
+
module_path.with_suffix(".py"),
|
|
135
|
+
module_path / "__init__.py",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _resolve_local_module(
|
|
139
|
+
self,
|
|
140
|
+
module_name: str,
|
|
141
|
+
current_file: Path,
|
|
142
|
+
level: int = 0,
|
|
143
|
+
) -> Optional[Path]:
|
|
144
|
+
"""Resolve an import to a local .py file/package if one exists."""
|
|
145
|
+
candidates = []
|
|
146
|
+
|
|
147
|
+
if level > 0:
|
|
148
|
+
# from .x => current package; from ..x => parent package, etc.
|
|
149
|
+
base = current_file.parent
|
|
150
|
+
for _ in range(level - 1):
|
|
151
|
+
base = base.parent
|
|
152
|
+
candidates.extend(self._module_candidates(base, module_name))
|
|
153
|
+
else:
|
|
154
|
+
for root in self._source_roots(current_file):
|
|
155
|
+
candidates.extend(self._module_candidates(root, module_name))
|
|
156
|
+
|
|
157
|
+
for candidate in candidates:
|
|
158
|
+
try:
|
|
159
|
+
resolved = candidate.resolve()
|
|
160
|
+
except OSError:
|
|
161
|
+
continue
|
|
162
|
+
if resolved.is_file() and self._is_inside_project(resolved):
|
|
163
|
+
return resolved
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def _is_inside_project(self, path: Path) -> bool:
|
|
167
|
+
root = self.project_root
|
|
168
|
+
if root is None:
|
|
169
|
+
return True
|
|
170
|
+
try:
|
|
171
|
+
path.resolve().relative_to(root.resolve())
|
|
172
|
+
return True
|
|
173
|
+
except ValueError:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _parse_tree(file_path: Path) -> Optional[ast.AST]:
|
|
178
|
+
try:
|
|
179
|
+
text = file_path.read_text(encoding="utf-8")
|
|
180
|
+
return ast.parse(text, filename=str(file_path))
|
|
181
|
+
except (OSError, SyntaxError, UnicodeDecodeError):
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def _analyze_file_imports(self, file_path: Path) -> Tuple[Set[str], Set[Path]]:
|
|
185
|
+
"""Return (external top-level imports, directly referenced local files)."""
|
|
186
|
+
tree = self._parse_tree(file_path)
|
|
187
|
+
if tree is None:
|
|
188
|
+
return set(), set()
|
|
189
|
+
|
|
190
|
+
external: Set[str] = set()
|
|
191
|
+
local_files: Set[Path] = set()
|
|
192
|
+
|
|
193
|
+
for node in ast.walk(tree):
|
|
194
|
+
if isinstance(node, ast.Import):
|
|
195
|
+
for alias in node.names:
|
|
196
|
+
root_name = alias.name.split(".", 1)[0]
|
|
197
|
+
if root_name in self.stdlib_modules:
|
|
198
|
+
continue
|
|
199
|
+
local = self._resolve_local_module(alias.name, file_path)
|
|
200
|
+
if local:
|
|
201
|
+
local_files.add(local)
|
|
202
|
+
else:
|
|
203
|
+
# Try the root package too, useful for package imports.
|
|
204
|
+
local_root = self._resolve_local_module(root_name, file_path)
|
|
205
|
+
if local_root:
|
|
206
|
+
local_files.add(local_root)
|
|
207
|
+
else:
|
|
208
|
+
external.add(root_name)
|
|
209
|
+
|
|
210
|
+
elif isinstance(node, ast.ImportFrom):
|
|
211
|
+
module_name = node.module or ""
|
|
212
|
+
root_name = module_name.split(".", 1)[0] if module_name else ""
|
|
213
|
+
|
|
214
|
+
if node.level == 0 and root_name in self.stdlib_modules:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
base_local = self._resolve_local_module(
|
|
218
|
+
module_name,
|
|
219
|
+
file_path,
|
|
220
|
+
level=node.level,
|
|
221
|
+
) if module_name else None
|
|
222
|
+
|
|
223
|
+
found_local = False
|
|
224
|
+
if base_local:
|
|
225
|
+
local_files.add(base_local)
|
|
226
|
+
found_local = True
|
|
227
|
+
|
|
228
|
+
# `from package import submodule` can hide another local file.
|
|
229
|
+
for alias in node.names:
|
|
230
|
+
if alias.name == "*":
|
|
231
|
+
continue
|
|
232
|
+
child_name = f"{module_name}.{alias.name}" if module_name else alias.name
|
|
233
|
+
child_local = self._resolve_local_module(
|
|
234
|
+
child_name,
|
|
235
|
+
file_path,
|
|
236
|
+
level=node.level,
|
|
237
|
+
)
|
|
238
|
+
if child_local:
|
|
239
|
+
local_files.add(child_local)
|
|
240
|
+
found_local = True
|
|
241
|
+
|
|
242
|
+
if not found_local and node.level == 0 and root_name:
|
|
243
|
+
external.add(root_name)
|
|
244
|
+
|
|
245
|
+
return external, local_files
|
|
246
|
+
|
|
247
|
+
def analyze_entrypoint(self, file_path: Path) -> Set[str]:
|
|
248
|
+
"""
|
|
249
|
+
Recursively follow local imports starting at file_path and return only
|
|
250
|
+
third-party imports required by the reachable dependency graph.
|
|
251
|
+
"""
|
|
252
|
+
file_path = Path(file_path).resolve()
|
|
253
|
+
self._ensure_project_root(file_path)
|
|
254
|
+
|
|
255
|
+
required: Set[str] = set()
|
|
256
|
+
visited: Set[Path] = set()
|
|
257
|
+
pending = [file_path]
|
|
258
|
+
|
|
259
|
+
while pending:
|
|
260
|
+
current = pending.pop()
|
|
261
|
+
if current in visited or not current.is_file():
|
|
262
|
+
continue
|
|
263
|
+
visited.add(current)
|
|
264
|
+
|
|
265
|
+
external, local_files = self._analyze_file_imports(current)
|
|
266
|
+
required.update(external)
|
|
267
|
+
|
|
268
|
+
for local_file in local_files:
|
|
269
|
+
if local_file not in visited:
|
|
270
|
+
pending.append(local_file)
|
|
271
|
+
|
|
272
|
+
return required
|
|
273
|
+
|
|
274
|
+
def extract_imports_from_file(self, file_path: Path) -> Set[str]:
|
|
275
|
+
"""
|
|
276
|
+
Backward-compatible API. It now follows local dependencies recursively,
|
|
277
|
+
which is the desired behavior for framework-based projects.
|
|
278
|
+
"""
|
|
279
|
+
return self.analyze_entrypoint(file_path)
|
|
280
|
+
|
|
281
|
+
def scan_directory(self, target_dir: Path) -> Set[str]:
|
|
282
|
+
"""Scan a project while pruning caches, build output, and any venv name."""
|
|
283
|
+
target_dir = Path(target_dir).resolve()
|
|
284
|
+
self._ensure_project_root(target_dir)
|
|
285
|
+
all_imports: Set[str] = set()
|
|
286
|
+
|
|
287
|
+
for current_root, dir_names, file_names in os.walk(target_dir):
|
|
288
|
+
current_path = Path(current_root)
|
|
289
|
+
|
|
290
|
+
kept_dirs = []
|
|
291
|
+
for name in dir_names:
|
|
292
|
+
directory = current_path / name
|
|
293
|
+
if name in self.SKIP_DIR_NAMES:
|
|
294
|
+
continue
|
|
295
|
+
if self._looks_like_venv(directory):
|
|
296
|
+
continue
|
|
297
|
+
kept_dirs.append(name)
|
|
298
|
+
dir_names[:] = kept_dirs
|
|
299
|
+
|
|
300
|
+
for name in file_names:
|
|
301
|
+
if not name.endswith(".py"):
|
|
302
|
+
continue
|
|
303
|
+
all_imports.update(self.analyze_entrypoint(current_path / name))
|
|
304
|
+
|
|
305
|
+
return all_imports
|
auto_req/cli.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# import argparse
|
|
2
|
+
# import subprocess
|
|
3
|
+
# import sys
|
|
4
|
+
# from pathlib import Path
|
|
5
|
+
# from auto_req.analyzer import CodeAnalyzer
|
|
6
|
+
# from auto_req.installer import PackageInstaller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# def main():
|
|
10
|
+
# parser = argparse.ArgumentParser(
|
|
11
|
+
# description="TraceReq: Auto-detect and install missing Python requirements by analyzing code."
|
|
12
|
+
# )
|
|
13
|
+
# parser.add_argument(
|
|
14
|
+
# "target",
|
|
15
|
+
# nargs="?",
|
|
16
|
+
# default=".",
|
|
17
|
+
# help="Target directory or Python file to analyze (default: current directory)",
|
|
18
|
+
# )
|
|
19
|
+
# args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
# target_path = Path(args.target).resolve()
|
|
22
|
+
# analyzer = CodeAnalyzer()
|
|
23
|
+
|
|
24
|
+
# if target_path.is_file():
|
|
25
|
+
# required_imports = analyzer.extract_imports_from_file(target_path)
|
|
26
|
+
# elif target_path.is_dir():
|
|
27
|
+
# required_imports = analyzer.scan_directory(target_path)
|
|
28
|
+
# else:
|
|
29
|
+
# print(f"Error: Target path '{target_path}' does not exist.")
|
|
30
|
+
# sys.exit(1)
|
|
31
|
+
|
|
32
|
+
# installer = PackageInstaller(
|
|
33
|
+
# project_root=target_path.parent if target_path.is_file() else target_path
|
|
34
|
+
# )
|
|
35
|
+
# missing = installer.resolve_missing(required_imports)
|
|
36
|
+
|
|
37
|
+
# if not missing:
|
|
38
|
+
# print("[TraceReq] All required dependencies are already installed.")
|
|
39
|
+
# else:
|
|
40
|
+
# success = installer.install_packages(missing)
|
|
41
|
+
# if not success:
|
|
42
|
+
# sys.exit(1)
|
|
43
|
+
|
|
44
|
+
# # Automatically execute the file if target is a single Python script
|
|
45
|
+
# if target_path.is_file():
|
|
46
|
+
# python_exec = installer.get_target_python()
|
|
47
|
+
# print(f"[TraceReq] Running {target_path.name} using {python_exec.name}...\n")
|
|
48
|
+
|
|
49
|
+
# # Step 1: Run execution and capture output to intercept runtime errors
|
|
50
|
+
# process = subprocess.run(
|
|
51
|
+
# [str(python_exec), str(target_path)],
|
|
52
|
+
# capture_output=True,
|
|
53
|
+
# text=True,
|
|
54
|
+
# )
|
|
55
|
+
|
|
56
|
+
# # Print standard output cleanly
|
|
57
|
+
# if process.stdout:
|
|
58
|
+
# print(process.stdout, end="")
|
|
59
|
+
|
|
60
|
+
# # Step 2: Intercept execution failures & trigger auto-remediation
|
|
61
|
+
# if process.returncode != 0:
|
|
62
|
+
# error_output = (process.stderr or "") + (process.stdout or "")
|
|
63
|
+
# if process.stderr:
|
|
64
|
+
# print(process.stderr, file=sys.stderr, end="")
|
|
65
|
+
|
|
66
|
+
# # Attempt self-healing repair via post-execution error rules
|
|
67
|
+
# remediated = installer.attempt_error_remediation(error_output)
|
|
68
|
+
|
|
69
|
+
# if remediated:
|
|
70
|
+
# print(f"[TraceReq] Retrying execution for {target_path.name}...\n")
|
|
71
|
+
# retry_process = subprocess.run([str(python_exec), str(target_path)])
|
|
72
|
+
# sys.exit(retry_process.returncode)
|
|
73
|
+
# else:
|
|
74
|
+
# print(f"\n[TraceReq] Execution failed with exit code {process.returncode}")
|
|
75
|
+
# sys.exit(process.returncode)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# if __name__ == "__main__":
|
|
79
|
+
# main()
|
|
80
|
+
|
|
81
|
+
import argparse
|
|
82
|
+
import subprocess
|
|
83
|
+
import sys
|
|
84
|
+
from pathlib import Path
|
|
85
|
+
|
|
86
|
+
from auto_req.analyzer import CodeAnalyzer
|
|
87
|
+
from auto_req.installer import PackageInstaller
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main():
|
|
91
|
+
parser = argparse.ArgumentParser(
|
|
92
|
+
description="TraceReq: discover runtime dependencies recursively and install missing packages."
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"target",
|
|
96
|
+
nargs="?",
|
|
97
|
+
default=".",
|
|
98
|
+
help="Python entry file or project directory (default: current directory)",
|
|
99
|
+
)
|
|
100
|
+
args = parser.parse_args()
|
|
101
|
+
|
|
102
|
+
target_path = Path(args.target).resolve()
|
|
103
|
+
if not target_path.exists():
|
|
104
|
+
print(f"Error: Target path '{target_path}' does not exist.")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
project_root = CodeAnalyzer.discover_project_root(target_path)
|
|
108
|
+
analyzer = CodeAnalyzer(project_root=project_root)
|
|
109
|
+
|
|
110
|
+
if target_path.is_file():
|
|
111
|
+
if target_path.suffix != ".py":
|
|
112
|
+
print(f"Error: '{target_path}' is not a Python file.")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
print(f"[TraceReq] Analyzing dependency graph from: {target_path.name}")
|
|
115
|
+
required_imports = analyzer.analyze_entrypoint(target_path)
|
|
116
|
+
else:
|
|
117
|
+
print(f"[TraceReq] Scanning project: {target_path}")
|
|
118
|
+
required_imports = analyzer.scan_directory(target_path)
|
|
119
|
+
|
|
120
|
+
if required_imports:
|
|
121
|
+
print(f"[TraceReq] Third-party imports detected: {', '.join(sorted(required_imports))}")
|
|
122
|
+
else:
|
|
123
|
+
print("[TraceReq] No third-party imports detected.")
|
|
124
|
+
|
|
125
|
+
installer = PackageInstaller(project_root=project_root)
|
|
126
|
+
missing = installer.resolve_missing(required_imports)
|
|
127
|
+
|
|
128
|
+
if not missing:
|
|
129
|
+
print("[TraceReq] All required dependencies are already installed.")
|
|
130
|
+
elif not installer.install_packages(missing):
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
if target_path.is_file():
|
|
134
|
+
python_exec = installer.get_target_python()
|
|
135
|
+
print(f"[TraceReq] Running {target_path.name} using {python_exec}...\n")
|
|
136
|
+
|
|
137
|
+
process = subprocess.run(
|
|
138
|
+
[str(python_exec), str(target_path)],
|
|
139
|
+
cwd=str(target_path.parent),
|
|
140
|
+
capture_output=True,
|
|
141
|
+
text=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if process.stdout:
|
|
145
|
+
print(process.stdout, end="")
|
|
146
|
+
|
|
147
|
+
if process.returncode != 0:
|
|
148
|
+
error_output = (process.stderr or "") + (process.stdout or "")
|
|
149
|
+
if process.stderr:
|
|
150
|
+
print(process.stderr, file=sys.stderr, end="")
|
|
151
|
+
|
|
152
|
+
if installer.attempt_error_remediation(error_output):
|
|
153
|
+
print(f"[TraceReq] Retrying execution for {target_path.name}...\n")
|
|
154
|
+
retry = subprocess.run(
|
|
155
|
+
[str(python_exec), str(target_path)],
|
|
156
|
+
cwd=str(target_path.parent),
|
|
157
|
+
)
|
|
158
|
+
sys.exit(retry.returncode)
|
|
159
|
+
|
|
160
|
+
print(f"\n[TraceReq] Execution failed with exit code {process.returncode}")
|
|
161
|
+
sys.exit(process.returncode)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|