cppkh-interface 0.1.3__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cppkh-interface
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: Dependency-free Python interface for cppkh with runtime C++ compilation.
5
5
  License-Expression: MIT
6
6
  Author: GGN_2015
@@ -1,68 +1,68 @@
1
- # cppkh-interface
2
-
3
- `cppkh-interface` is a Python package for computing integer Khovanov homology
4
- with the C++ `cppkh` implementation.
5
-
6
- Version `0.1.3` has no runtime Python-package dependencies. Link crossing signs,
7
- PD validation, R1 removal, and nugatory-crossing removal all use the bundled
8
- canonical `cppkh` C++ source and its SageMath-compatible orientation rules.
9
-
10
- The package is compatible with the main `javakh-interface` function:
11
-
12
- ```python
13
- import cppkh_interface
14
-
15
- pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
16
- print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
17
- print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
18
- ```
19
-
20
- Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
21
- the `cppkh` C++ source file in built distributions and compiles a local
22
- executable on first use using only Python's standard library. The compiled
23
- executable is cached for later calls.
24
-
25
- In the repository checkout, the package does not keep a committed backup copy
26
- of the C++ source. The build backend copies `../../src/main.cpp` into the
27
- package data directory only while the PEP 517 build is running, then removes
28
- that temporary copy.
29
-
30
- ## Install
31
-
32
- ```sh
33
- pip install cppkh-interface
34
- ```
35
-
36
- A C++14 compiler must be available at runtime. The package looks at
37
- `CPPKH_INTERFACE_CXX`, then `CXX`, then searches `PATH` for `g++`, `clang++`, or
38
- `c++`. To select a compiler explicitly:
39
-
40
- ```sh
41
- CPPKH_INTERFACE_CXX=clang++ python your_script.py
42
- ```
43
-
44
- Windows PowerShell:
45
-
46
- ```powershell
47
- $env:CPPKH_INTERFACE_CXX = "C:\path\to\g++.exe"
48
- python your_script.py
49
- ```
50
-
51
- ## Build And Publish
52
-
53
- From this directory:
54
-
55
- ```sh
56
- python -m build
57
- poetry publish
58
- ```
59
-
60
- Do not use `poetry build` or `poetry publish --build`: Poetry's direct builder
61
- bypasses the source-synchronizing PEP 517 backend. Build first with
62
- `python -m build`, inspect/test the wheel, then publish the existing artifacts.
63
-
64
- For local testing:
65
-
66
- ```sh
67
- poetry run python -m cppkh_interface "[[1,5,2,4], [3,1,4,6], [5,3,6,2]]"
68
- ```
1
+ # cppkh-interface
2
+
3
+ `cppkh-interface` is a Python package for computing integer Khovanov homology
4
+ with the C++ `cppkh` implementation.
5
+
6
+ Version `0.1.3` has no runtime Python-package dependencies. Link crossing signs,
7
+ PD validation, R1 removal, and nugatory-crossing removal all use the bundled
8
+ canonical `cppkh` C++ source and its SageMath-compatible orientation rules.
9
+
10
+ The package is compatible with the main `javakh-interface` function:
11
+
12
+ ```python
13
+ import cppkh_interface
14
+
15
+ pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
16
+ print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
17
+ print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
18
+ ```
19
+
20
+ Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
21
+ the `cppkh` C++ source file in built distributions and compiles a local
22
+ executable on first use using only Python's standard library. The compiled
23
+ executable is cached for later calls.
24
+
25
+ In the repository checkout, the package does not keep a committed backup copy
26
+ of the C++ source. The build backend copies `../../src/main.cpp` into the
27
+ package data directory only while the PEP 517 build is running, then removes
28
+ that temporary copy.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ pip install cppkh-interface
34
+ ```
35
+
36
+ A C++14 compiler must be available at runtime. The package looks at
37
+ `CPPKH_INTERFACE_CXX`, then `CXX`, then searches `PATH` for `g++`, `clang++`, or
38
+ `c++`. To select a compiler explicitly:
39
+
40
+ ```sh
41
+ CPPKH_INTERFACE_CXX=clang++ python your_script.py
42
+ ```
43
+
44
+ Windows PowerShell:
45
+
46
+ ```powershell
47
+ $env:CPPKH_INTERFACE_CXX = "C:\path\to\g++.exe"
48
+ python your_script.py
49
+ ```
50
+
51
+ ## Build And Publish
52
+
53
+ From this directory:
54
+
55
+ ```sh
56
+ python -m build
57
+ poetry publish
58
+ ```
59
+
60
+ Do not use `poetry build` or `poetry publish --build`: Poetry's direct builder
61
+ bypasses the source-synchronizing PEP 517 backend. Build first with
62
+ `python -m build`, inspect/test the wheel, then publish the existing artifacts.
63
+
64
+ For local testing:
65
+
66
+ ```sh
67
+ poetry run python -m cppkh_interface "[[1,5,2,4], [3,1,4,6], [5,3,6,2]]"
68
+ ```
@@ -1,207 +1,207 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import hashlib
5
- import io
6
- import shutil
7
- import tarfile
8
- import tempfile
9
- import zipfile
10
- from pathlib import Path
11
-
12
- from poetry.core.masonry import api as poetry_api
13
-
14
-
15
- PROJECT_ROOT = Path(__file__).resolve().parents[1]
16
- REPO_ROOT = PROJECT_ROOT.parents[1]
17
- SOURCE = REPO_ROOT / "src" / "main.cpp"
18
- TARGET = PROJECT_ROOT / "cppkh_interface" / "data" / "src" / "main.cpp"
19
- WHEEL_SOURCE_NAME = "cppkh_interface/data/src/main.cpp"
20
-
21
-
22
- def _sync_cpp_source() -> None:
23
- if SOURCE.exists():
24
- TARGET.parent.mkdir(parents=True, exist_ok=True)
25
- shutil.copyfile(SOURCE, TARGET)
26
- return
27
- if TARGET.exists():
28
- return
29
- raise FileNotFoundError(
30
- "cppkh source file was not found. Expected either "
31
- f"{SOURCE} in the repository checkout or {TARGET} in the source tree."
32
- )
33
-
34
-
35
- def _clean_cpp_source() -> None:
36
- if TARGET.exists() and SOURCE.exists():
37
- TARGET.unlink()
38
-
39
-
40
- def _source_bytes() -> bytes:
41
- if TARGET.exists():
42
- return TARGET.read_bytes()
43
- if SOURCE.exists():
44
- return SOURCE.read_bytes()
45
- raise FileNotFoundError("cppkh source file was not available for packaging")
46
-
47
-
48
- def _wheel_hash_and_size(data: bytes) -> tuple[str, str]:
49
- digest = hashlib.sha256(data).digest()
50
- encoded = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
51
- return f"sha256={encoded}", str(len(data))
52
-
53
-
54
- def _make_universal_wheel_metadata(data: bytes) -> bytes:
55
- text = data.decode("utf-8")
56
- lines = []
57
- tag_written = False
58
- root_written = False
59
- for line in text.splitlines():
60
- if line.startswith("Root-Is-Purelib:"):
61
- lines.append("Root-Is-Purelib: true")
62
- root_written = True
63
- elif line.startswith("Tag:"):
64
- if not tag_written:
65
- lines.append("Tag: py3-none-any")
66
- tag_written = True
67
- else:
68
- lines.append(line)
69
- if not root_written:
70
- lines.append("Root-Is-Purelib: true")
71
- if not tag_written:
72
- lines.append("Tag: py3-none-any")
73
- return ("\n".join(lines) + "\n").encode("utf-8")
74
-
75
-
76
- def _universal_wheel_path(wheel_path: Path, record_name: str) -> Path:
77
- dist_info = record_name.split("/", 1)[0]
78
- base = dist_info.removesuffix(".dist-info")
79
- return wheel_path.with_name(f"{base}-py3-none-any.whl")
80
-
81
-
82
- def _rewrite_wheel_with_source(wheel_path: Path) -> Path:
83
- source_data = _source_bytes()
84
- rows: list[tuple[str, str, str]] = []
85
-
86
- with zipfile.ZipFile(wheel_path, "r") as zin:
87
- record_name = next(
88
- (name for name in zin.namelist() if name.endswith(".dist-info/RECORD")),
89
- None,
90
- )
91
- if record_name is None:
92
- raise RuntimeError(f"wheel RECORD file not found in {wheel_path}")
93
- output_path = _universal_wheel_path(wheel_path, record_name)
94
- temp_path = output_path.with_name(output_path.name + ".tmp")
95
-
96
- with zipfile.ZipFile(temp_path, "w", compression=zipfile.ZIP_DEFLATED) as zout:
97
- wheel_metadata_name = record_name.removesuffix("RECORD") + "WHEEL"
98
-
99
- for item in zin.infolist():
100
- if item.filename in (WHEEL_SOURCE_NAME, record_name):
101
- continue
102
- data = zin.read(item.filename)
103
- if item.filename == wheel_metadata_name:
104
- data = _make_universal_wheel_metadata(data)
105
- zout.writestr(item, data)
106
- if not item.is_dir():
107
- digest, size = _wheel_hash_and_size(data)
108
- rows.append((item.filename, digest, size))
109
-
110
- source_info = zipfile.ZipInfo(WHEEL_SOURCE_NAME, date_time=(2016, 1, 1, 0, 0, 0))
111
- source_info.compress_type = zipfile.ZIP_DEFLATED
112
- zout.writestr(source_info, source_data)
113
- digest, size = _wheel_hash_and_size(source_data)
114
- rows.append((WHEEL_SOURCE_NAME, digest, size))
115
-
116
- rows.append((record_name, "", ""))
117
- record_text = "".join(f"{path},{digest},{size}\n" for path, digest, size in rows)
118
- record_info = zipfile.ZipInfo(record_name, date_time=(2016, 1, 1, 0, 0, 0))
119
- record_info.compress_type = zipfile.ZIP_DEFLATED
120
- zout.writestr(record_info, record_text.encode("utf-8"))
121
-
122
- temp_path.replace(output_path)
123
- if output_path != wheel_path and wheel_path.exists():
124
- wheel_path.unlink()
125
- return output_path
126
-
127
-
128
- def finalize_poetry_wheels() -> None:
129
- dist = PROJECT_ROOT / "dist"
130
- if not dist.exists():
131
- return
132
- for wheel_path in sorted(dist.glob("cppkh_interface-*.whl")):
133
- _rewrite_wheel_with_source(wheel_path)
134
-
135
-
136
- def _rewrite_sdist_with_source(sdist_path: Path) -> None:
137
- source_data = _source_bytes()
138
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz")
139
- temp_path = Path(temp_file.name)
140
- temp_file.close()
141
-
142
- try:
143
- with tarfile.open(sdist_path, "r:gz") as tin, tarfile.open(temp_path, "w:gz") as tout:
144
- names = tin.getnames()
145
- if not names:
146
- raise RuntimeError(f"sdist is empty: {sdist_path}")
147
- root = names[0].split("/", 1)[0]
148
- source_name = f"{root}/{WHEEL_SOURCE_NAME}"
149
-
150
- for member in tin.getmembers():
151
- if member.name == source_name:
152
- continue
153
- if member.isfile():
154
- extracted = tin.extractfile(member)
155
- if extracted is None:
156
- raise RuntimeError(f"could not read {member.name} from {sdist_path}")
157
- with extracted:
158
- tout.addfile(member, extracted)
159
- else:
160
- tout.addfile(member)
161
-
162
- source_info = tarfile.TarInfo(source_name)
163
- source_info.size = len(source_data)
164
- source_info.mtime = 0
165
- source_info.mode = 0o644
166
- tout.addfile(source_info, io.BytesIO(source_data))
167
-
168
- temp_path.replace(sdist_path)
169
- finally:
170
- if temp_path.exists():
171
- temp_path.unlink()
172
-
173
-
174
- def get_requires_for_build_wheel(config_settings=None):
175
- return poetry_api.get_requires_for_build_wheel(config_settings)
176
-
177
-
178
- def get_requires_for_build_sdist(config_settings=None):
179
- return poetry_api.get_requires_for_build_sdist(config_settings)
180
-
181
-
182
- def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
183
- _sync_cpp_source()
184
- try:
185
- return poetry_api.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
186
- finally:
187
- _clean_cpp_source()
188
-
189
-
190
- def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
191
- _sync_cpp_source()
192
- try:
193
- wheel_name = poetry_api.build_wheel(wheel_directory, config_settings, metadata_directory)
194
- final_path = _rewrite_wheel_with_source(Path(wheel_directory) / wheel_name)
195
- return final_path.name
196
- finally:
197
- _clean_cpp_source()
198
-
199
-
200
- def build_sdist(sdist_directory, config_settings=None):
201
- _sync_cpp_source()
202
- try:
203
- sdist_name = poetry_api.build_sdist(sdist_directory, config_settings)
204
- _rewrite_sdist_with_source(Path(sdist_directory) / sdist_name)
205
- return sdist_name
206
- finally:
207
- _clean_cpp_source()
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import io
6
+ import shutil
7
+ import tarfile
8
+ import tempfile
9
+ import zipfile
10
+ from pathlib import Path
11
+
12
+ from poetry.core.masonry import api as poetry_api
13
+
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
16
+ REPO_ROOT = PROJECT_ROOT.parents[1]
17
+ SOURCE = REPO_ROOT / "src" / "main.cpp"
18
+ TARGET = PROJECT_ROOT / "cppkh_interface" / "data" / "src" / "main.cpp"
19
+ WHEEL_SOURCE_NAME = "cppkh_interface/data/src/main.cpp"
20
+
21
+
22
+ def _sync_cpp_source() -> None:
23
+ if SOURCE.exists():
24
+ TARGET.parent.mkdir(parents=True, exist_ok=True)
25
+ shutil.copyfile(SOURCE, TARGET)
26
+ return
27
+ if TARGET.exists():
28
+ return
29
+ raise FileNotFoundError(
30
+ "cppkh source file was not found. Expected either "
31
+ f"{SOURCE} in the repository checkout or {TARGET} in the source tree."
32
+ )
33
+
34
+
35
+ def _clean_cpp_source() -> None:
36
+ if TARGET.exists() and SOURCE.exists():
37
+ TARGET.unlink()
38
+
39
+
40
+ def _source_bytes() -> bytes:
41
+ if TARGET.exists():
42
+ return TARGET.read_bytes()
43
+ if SOURCE.exists():
44
+ return SOURCE.read_bytes()
45
+ raise FileNotFoundError("cppkh source file was not available for packaging")
46
+
47
+
48
+ def _wheel_hash_and_size(data: bytes) -> tuple[str, str]:
49
+ digest = hashlib.sha256(data).digest()
50
+ encoded = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
51
+ return f"sha256={encoded}", str(len(data))
52
+
53
+
54
+ def _make_universal_wheel_metadata(data: bytes) -> bytes:
55
+ text = data.decode("utf-8")
56
+ lines = []
57
+ tag_written = False
58
+ root_written = False
59
+ for line in text.splitlines():
60
+ if line.startswith("Root-Is-Purelib:"):
61
+ lines.append("Root-Is-Purelib: true")
62
+ root_written = True
63
+ elif line.startswith("Tag:"):
64
+ if not tag_written:
65
+ lines.append("Tag: py3-none-any")
66
+ tag_written = True
67
+ else:
68
+ lines.append(line)
69
+ if not root_written:
70
+ lines.append("Root-Is-Purelib: true")
71
+ if not tag_written:
72
+ lines.append("Tag: py3-none-any")
73
+ return ("\n".join(lines) + "\n").encode("utf-8")
74
+
75
+
76
+ def _universal_wheel_path(wheel_path: Path, record_name: str) -> Path:
77
+ dist_info = record_name.split("/", 1)[0]
78
+ base = dist_info.removesuffix(".dist-info")
79
+ return wheel_path.with_name(f"{base}-py3-none-any.whl")
80
+
81
+
82
+ def _rewrite_wheel_with_source(wheel_path: Path) -> Path:
83
+ source_data = _source_bytes()
84
+ rows: list[tuple[str, str, str]] = []
85
+
86
+ with zipfile.ZipFile(wheel_path, "r") as zin:
87
+ record_name = next(
88
+ (name for name in zin.namelist() if name.endswith(".dist-info/RECORD")),
89
+ None,
90
+ )
91
+ if record_name is None:
92
+ raise RuntimeError(f"wheel RECORD file not found in {wheel_path}")
93
+ output_path = _universal_wheel_path(wheel_path, record_name)
94
+ temp_path = output_path.with_name(output_path.name + ".tmp")
95
+
96
+ with zipfile.ZipFile(temp_path, "w", compression=zipfile.ZIP_DEFLATED) as zout:
97
+ wheel_metadata_name = record_name.removesuffix("RECORD") + "WHEEL"
98
+
99
+ for item in zin.infolist():
100
+ if item.filename in (WHEEL_SOURCE_NAME, record_name):
101
+ continue
102
+ data = zin.read(item.filename)
103
+ if item.filename == wheel_metadata_name:
104
+ data = _make_universal_wheel_metadata(data)
105
+ zout.writestr(item, data)
106
+ if not item.is_dir():
107
+ digest, size = _wheel_hash_and_size(data)
108
+ rows.append((item.filename, digest, size))
109
+
110
+ source_info = zipfile.ZipInfo(WHEEL_SOURCE_NAME, date_time=(2016, 1, 1, 0, 0, 0))
111
+ source_info.compress_type = zipfile.ZIP_DEFLATED
112
+ zout.writestr(source_info, source_data)
113
+ digest, size = _wheel_hash_and_size(source_data)
114
+ rows.append((WHEEL_SOURCE_NAME, digest, size))
115
+
116
+ rows.append((record_name, "", ""))
117
+ record_text = "".join(f"{path},{digest},{size}\n" for path, digest, size in rows)
118
+ record_info = zipfile.ZipInfo(record_name, date_time=(2016, 1, 1, 0, 0, 0))
119
+ record_info.compress_type = zipfile.ZIP_DEFLATED
120
+ zout.writestr(record_info, record_text.encode("utf-8"))
121
+
122
+ temp_path.replace(output_path)
123
+ if output_path != wheel_path and wheel_path.exists():
124
+ wheel_path.unlink()
125
+ return output_path
126
+
127
+
128
+ def finalize_poetry_wheels() -> None:
129
+ dist = PROJECT_ROOT / "dist"
130
+ if not dist.exists():
131
+ return
132
+ for wheel_path in sorted(dist.glob("cppkh_interface-*.whl")):
133
+ _rewrite_wheel_with_source(wheel_path)
134
+
135
+
136
+ def _rewrite_sdist_with_source(sdist_path: Path) -> None:
137
+ source_data = _source_bytes()
138
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz")
139
+ temp_path = Path(temp_file.name)
140
+ temp_file.close()
141
+
142
+ try:
143
+ with tarfile.open(sdist_path, "r:gz") as tin, tarfile.open(temp_path, "w:gz") as tout:
144
+ names = tin.getnames()
145
+ if not names:
146
+ raise RuntimeError(f"sdist is empty: {sdist_path}")
147
+ root = names[0].split("/", 1)[0]
148
+ source_name = f"{root}/{WHEEL_SOURCE_NAME}"
149
+
150
+ for member in tin.getmembers():
151
+ if member.name == source_name:
152
+ continue
153
+ if member.isfile():
154
+ extracted = tin.extractfile(member)
155
+ if extracted is None:
156
+ raise RuntimeError(f"could not read {member.name} from {sdist_path}")
157
+ with extracted:
158
+ tout.addfile(member, extracted)
159
+ else:
160
+ tout.addfile(member)
161
+
162
+ source_info = tarfile.TarInfo(source_name)
163
+ source_info.size = len(source_data)
164
+ source_info.mtime = 0
165
+ source_info.mode = 0o644
166
+ tout.addfile(source_info, io.BytesIO(source_data))
167
+
168
+ temp_path.replace(sdist_path)
169
+ finally:
170
+ if temp_path.exists():
171
+ temp_path.unlink()
172
+
173
+
174
+ def get_requires_for_build_wheel(config_settings=None):
175
+ return poetry_api.get_requires_for_build_wheel(config_settings)
176
+
177
+
178
+ def get_requires_for_build_sdist(config_settings=None):
179
+ return poetry_api.get_requires_for_build_sdist(config_settings)
180
+
181
+
182
+ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
183
+ _sync_cpp_source()
184
+ try:
185
+ return poetry_api.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
186
+ finally:
187
+ _clean_cpp_source()
188
+
189
+
190
+ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
191
+ _sync_cpp_source()
192
+ try:
193
+ wheel_name = poetry_api.build_wheel(wheel_directory, config_settings, metadata_directory)
194
+ final_path = _rewrite_wheel_with_source(Path(wheel_directory) / wheel_name)
195
+ return final_path.name
196
+ finally:
197
+ _clean_cpp_source()
198
+
199
+
200
+ def build_sdist(sdist_directory, config_settings=None):
201
+ _sync_cpp_source()
202
+ try:
203
+ sdist_name = poetry_api.build_sdist(sdist_directory, config_settings)
204
+ _rewrite_sdist_with_source(Path(sdist_directory) / sdist_name)
205
+ return sdist_name
206
+ finally:
207
+ _clean_cpp_source()
@@ -1,25 +1,29 @@
1
- from .main import (
2
- CppkhInterfaceError,
3
- compile_cppkh,
4
- compute_many_pd,
5
- compute_pd,
6
- get_cppkh_executable,
7
- normalize_pd_code,
8
- normalize_pd_codes,
9
- simplify_pd,
10
- solve_many_khovanov,
11
- solve_khovanov,
12
- )
13
-
14
- __all__ = [
15
- "CppkhInterfaceError",
16
- "compile_cppkh",
17
- "compute_many_pd",
18
- "compute_pd",
19
- "get_cppkh_executable",
20
- "normalize_pd_code",
21
- "normalize_pd_codes",
22
- "simplify_pd",
23
- "solve_many_khovanov",
24
- "solve_khovanov",
25
- ]
1
+ from .main import (
2
+ CppkhInterfaceError,
3
+ compile_cppkh,
4
+ compile_cppkh_shared,
5
+ compute_signed_variants,
6
+ compute_many_pd,
7
+ compute_pd,
8
+ get_cppkh_executable,
9
+ normalize_pd_code,
10
+ normalize_pd_codes,
11
+ simplify_pd,
12
+ solve_many_khovanov,
13
+ solve_khovanov,
14
+ )
15
+
16
+ __all__ = [
17
+ "CppkhInterfaceError",
18
+ "compile_cppkh",
19
+ "compile_cppkh_shared",
20
+ "compute_signed_variants",
21
+ "compute_many_pd",
22
+ "compute_pd",
23
+ "get_cppkh_executable",
24
+ "normalize_pd_code",
25
+ "normalize_pd_codes",
26
+ "simplify_pd",
27
+ "solve_many_khovanov",
28
+ "solve_khovanov",
29
+ ]
@@ -1,4 +1,4 @@
1
- from .main import main
2
-
3
- if __name__ == "__main__":
4
- raise SystemExit(main())
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())