autoneat 0.2.2__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.
- autoneat-0.2.2/LICENSE +21 -0
- autoneat-0.2.2/PKG-INFO +102 -0
- autoneat-0.2.2/README.md +84 -0
- autoneat-0.2.2/pyproject.toml +39 -0
- autoneat-0.2.2/setup.cfg +4 -0
- autoneat-0.2.2/src/autoneat/__init__.py +5 -0
- autoneat-0.2.2/src/autoneat/__main__.py +3 -0
- autoneat-0.2.2/src/autoneat/api.py +56 -0
- autoneat-0.2.2/src/autoneat/cli.py +101 -0
- autoneat-0.2.2/src/autoneat/core/__init__.py +1 -0
- autoneat-0.2.2/src/autoneat/core/batch.py +526 -0
- autoneat-0.2.2/src/autoneat/core/click.py +31 -0
- autoneat-0.2.2/src/autoneat/core/neat_ofx.py +195 -0
- autoneat-0.2.2/src/autoneat/core/ocr.py +337 -0
- autoneat-0.2.2/src/autoneat/core/recorder.py +39 -0
- autoneat-0.2.2/src/autoneat/core/resolve_client.py +84 -0
- autoneat-0.2.2/src/autoneat/core/shotid.py +82 -0
- autoneat-0.2.2/src/autoneat/core/subprocess_utils.py +61 -0
- autoneat-0.2.2/src/autoneat/core/ui_driver.py +229 -0
- autoneat-0.2.2/src/autoneat/core/windows.py +143 -0
- autoneat-0.2.2/src/autoneat/doctor.py +46 -0
- autoneat-0.2.2/src/autoneat/resolve.py +74 -0
- autoneat-0.2.2/src/autoneat/resources/vision_ocr.swift +69 -0
- autoneat-0.2.2/src/autoneat.egg-info/PKG-INFO +102 -0
- autoneat-0.2.2/src/autoneat.egg-info/SOURCES.txt +30 -0
- autoneat-0.2.2/src/autoneat.egg-info/dependency_links.txt +1 -0
- autoneat-0.2.2/src/autoneat.egg-info/entry_points.txt +2 -0
- autoneat-0.2.2/src/autoneat.egg-info/requires.txt +7 -0
- autoneat-0.2.2/src/autoneat.egg-info/top_level.txt +1 -0
- autoneat-0.2.2/tests/test_api.py +34 -0
- autoneat-0.2.2/tests/test_cli.py +35 -0
- autoneat-0.2.2/tests/test_resolve.py +83 -0
autoneat-0.2.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mhadifilms
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
autoneat-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autoneat
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Standalone Neat Video Auto Profile automation for DaVinci Resolve
|
|
5
|
+
Author: Mohammad Hadi
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mhadifilms/autoneat
|
|
8
|
+
Project-URL: Repository, https://github.com/mhadifilms/autoneat
|
|
9
|
+
Project-URL: Issues, https://github.com/mhadifilms/autoneat/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: dvr>=1.1
|
|
14
|
+
Requires-Dist: pyobjc-framework-Quartz>=11.0; platform_system == "Darwin"
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# autoneat
|
|
20
|
+
|
|
21
|
+
Standalone Neat Video Pro 6 Auto Profile automation for DaVinci Resolve.
|
|
22
|
+
|
|
23
|
+
Neat Video has no public scripting API for Auto Profile. `autoneat` scripts the
|
|
24
|
+
parts Resolve exposes, then uses macOS window automation plus Apple Vision OCR
|
|
25
|
+
for the two controls Neat does not expose: **Auto Profile** and **Apply**.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m pip install autoneat
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
DaVinci Resolve Studio, Neat Video Pro 6, and macOS Accessibility / Screen
|
|
34
|
+
Recording permissions are required. Run:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
autoneat doctor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CLI
|
|
41
|
+
|
|
42
|
+
Run against the current Resolve project/timeline:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
autoneat profile --state artifacts/neat/state.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run against a specific project/timeline and shot filter:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
autoneat profile \
|
|
52
|
+
--project "My Show" \
|
|
53
|
+
--timeline "My Show_Neat" \
|
|
54
|
+
--shot-ids 001,002,003 \
|
|
55
|
+
--state artifacts/neat/state.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Important options:
|
|
59
|
+
|
|
60
|
+
- `--continue` resumes from the state JSON.
|
|
61
|
+
- `--retry-failed` retries previously failed clips when resuming.
|
|
62
|
+
- `--all-tracks` processes all video tracks instead of `--track 1`.
|
|
63
|
+
- `--no-color-wrap` skips the ACES/HDR ColorSpaceTransform wrapper.
|
|
64
|
+
- `--json` prints the final summary object after the live log.
|
|
65
|
+
|
|
66
|
+
## Python API
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from autoneat import ProfileOptions, run_profile
|
|
70
|
+
|
|
71
|
+
result = run_profile(
|
|
72
|
+
ProfileOptions(
|
|
73
|
+
project_name="My Show",
|
|
74
|
+
timeline_name="My Show_Neat",
|
|
75
|
+
shot_ids=["001", "002"],
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
For each selected timeline clip, `autoneat`:
|
|
83
|
+
|
|
84
|
+
1. Moves the Resolve playhead to the clip.
|
|
85
|
+
2. Adds or reuses the Neat Video OFX node.
|
|
86
|
+
3. Wraps ACES/HDR clips with ColorSpaceTransform nodes so Neat sees
|
|
87
|
+
display-referred pixels.
|
|
88
|
+
4. Opens Neat via the OFX `Prepare Profile___` button control.
|
|
89
|
+
5. OCR-clicks Auto Profile, waits for readiness, then OCR-clicks Apply.
|
|
90
|
+
6. Writes a state JSON after every clip for resumable batches.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python3 -m venv .venv
|
|
96
|
+
./.venv/bin/python -m pip install -e ".[test]"
|
|
97
|
+
./.venv/bin/python -m pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
[MIT](LICENSE).
|
autoneat-0.2.2/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# autoneat
|
|
2
|
+
|
|
3
|
+
Standalone Neat Video Pro 6 Auto Profile automation for DaVinci Resolve.
|
|
4
|
+
|
|
5
|
+
Neat Video has no public scripting API for Auto Profile. `autoneat` scripts the
|
|
6
|
+
parts Resolve exposes, then uses macOS window automation plus Apple Vision OCR
|
|
7
|
+
for the two controls Neat does not expose: **Auto Profile** and **Apply**.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python3 -m pip install autoneat
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
DaVinci Resolve Studio, Neat Video Pro 6, and macOS Accessibility / Screen
|
|
16
|
+
Recording permissions are required. Run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
autoneat doctor
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CLI
|
|
23
|
+
|
|
24
|
+
Run against the current Resolve project/timeline:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
autoneat profile --state artifacts/neat/state.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run against a specific project/timeline and shot filter:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
autoneat profile \
|
|
34
|
+
--project "My Show" \
|
|
35
|
+
--timeline "My Show_Neat" \
|
|
36
|
+
--shot-ids 001,002,003 \
|
|
37
|
+
--state artifacts/neat/state.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Important options:
|
|
41
|
+
|
|
42
|
+
- `--continue` resumes from the state JSON.
|
|
43
|
+
- `--retry-failed` retries previously failed clips when resuming.
|
|
44
|
+
- `--all-tracks` processes all video tracks instead of `--track 1`.
|
|
45
|
+
- `--no-color-wrap` skips the ACES/HDR ColorSpaceTransform wrapper.
|
|
46
|
+
- `--json` prints the final summary object after the live log.
|
|
47
|
+
|
|
48
|
+
## Python API
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from autoneat import ProfileOptions, run_profile
|
|
52
|
+
|
|
53
|
+
result = run_profile(
|
|
54
|
+
ProfileOptions(
|
|
55
|
+
project_name="My Show",
|
|
56
|
+
timeline_name="My Show_Neat",
|
|
57
|
+
shot_ids=["001", "002"],
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
For each selected timeline clip, `autoneat`:
|
|
65
|
+
|
|
66
|
+
1. Moves the Resolve playhead to the clip.
|
|
67
|
+
2. Adds or reuses the Neat Video OFX node.
|
|
68
|
+
3. Wraps ACES/HDR clips with ColorSpaceTransform nodes so Neat sees
|
|
69
|
+
display-referred pixels.
|
|
70
|
+
4. Opens Neat via the OFX `Prepare Profile___` button control.
|
|
71
|
+
5. OCR-clicks Auto Profile, waits for readiness, then OCR-clicks Apply.
|
|
72
|
+
6. Writes a state JSON after every clip for resumable batches.
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python3 -m venv .venv
|
|
78
|
+
./.venv/bin/python -m pip install -e ".[test]"
|
|
79
|
+
./.venv/bin/python -m pytest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "autoneat"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Standalone Neat Video Auto Profile automation for DaVinci Resolve"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Mohammad Hadi" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"dvr>=1.1",
|
|
15
|
+
"pyobjc-framework-Quartz>=11.0; platform_system == 'Darwin'",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/mhadifilms/autoneat"
|
|
20
|
+
Repository = "https://github.com/mhadifilms/autoneat"
|
|
21
|
+
Issues = "https://github.com/mhadifilms/autoneat/issues"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = [
|
|
25
|
+
"pytest>=8",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
autoneat = "autoneat.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
autoneat = ["resources/*"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
pythonpath = ["src"]
|
autoneat-0.2.2/setup.cfg
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Public Python API for standalone autoneat runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from autoneat.core.batch import BatchSettings, run_batch
|
|
9
|
+
from autoneat.resolve import connect_resolve
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ProfileOptions(BatchSettings):
|
|
14
|
+
"""Options for a standalone Neat Video Auto Profile batch."""
|
|
15
|
+
|
|
16
|
+
project_name: Optional[str] = None
|
|
17
|
+
timeline_name: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_profile(
|
|
21
|
+
options: ProfileOptions,
|
|
22
|
+
*,
|
|
23
|
+
resolve: Any = None,
|
|
24
|
+
project: Any = None,
|
|
25
|
+
timeline: Any = None,
|
|
26
|
+
sink: Optional[Callable[[str], None]] = None,
|
|
27
|
+
cancel_event: Any = None,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""Run Auto Profile against a Resolve project/timeline.
|
|
30
|
+
|
|
31
|
+
Tests and embedding callers may pass explicit Resolve handles. Normal CLI
|
|
32
|
+
usage lets autoneat connect through ``dvr`` and select the requested
|
|
33
|
+
project/timeline.
|
|
34
|
+
"""
|
|
35
|
+
if resolve is not None and project is not None and timeline is not None:
|
|
36
|
+
return run_batch(
|
|
37
|
+
resolve,
|
|
38
|
+
project,
|
|
39
|
+
timeline,
|
|
40
|
+
options,
|
|
41
|
+
sink=sink,
|
|
42
|
+
cancel_event=cancel_event,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
with connect_resolve(
|
|
46
|
+
project_name=options.project_name,
|
|
47
|
+
timeline_name=options.timeline_name,
|
|
48
|
+
) as session:
|
|
49
|
+
return run_batch(
|
|
50
|
+
session.resolve,
|
|
51
|
+
session.project,
|
|
52
|
+
session.timeline,
|
|
53
|
+
options,
|
|
54
|
+
sink=sink,
|
|
55
|
+
cancel_event=cancel_event,
|
|
56
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Command-line interface for autoneat."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from autoneat.api import ProfileOptions, run_profile
|
|
11
|
+
from autoneat.doctor import check_environment, print_report
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_shot_ids(value: str | None) -> list[str]:
|
|
15
|
+
if not value:
|
|
16
|
+
return []
|
|
17
|
+
return [part.strip() for part in value.replace(",", " ").split() if part.strip()]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _profile(args: argparse.Namespace) -> int:
|
|
21
|
+
options = ProfileOptions(
|
|
22
|
+
project_name=args.project,
|
|
23
|
+
timeline_name=args.timeline,
|
|
24
|
+
track=args.track,
|
|
25
|
+
all_video_tracks=args.all_tracks,
|
|
26
|
+
shot_ids=_parse_shot_ids(args.shot_ids),
|
|
27
|
+
start_from=args.start_from,
|
|
28
|
+
limit=args.limit,
|
|
29
|
+
continue_run=args.continue_run,
|
|
30
|
+
retry_failed=args.retry_failed,
|
|
31
|
+
reuse_existing_neat=not args.no_reuse_existing,
|
|
32
|
+
no_color_wrap=args.no_color_wrap,
|
|
33
|
+
open_timeout=args.open_timeout,
|
|
34
|
+
editor_timeout=args.editor_timeout,
|
|
35
|
+
prepare_timeout=args.prepare_timeout,
|
|
36
|
+
profile_wait=args.profile_wait,
|
|
37
|
+
ready_timeout=args.ready_timeout,
|
|
38
|
+
apply_delay=args.apply_delay,
|
|
39
|
+
close_timeout=args.close_timeout,
|
|
40
|
+
step_delay=args.step_delay,
|
|
41
|
+
sidecar_path=Path(args.state).expanduser() if args.state else None,
|
|
42
|
+
)
|
|
43
|
+
result = run_profile(options, sink=lambda line: print(line, flush=True))
|
|
44
|
+
if args.json:
|
|
45
|
+
print(json.dumps(result, indent=2, sort_keys=True), flush=True)
|
|
46
|
+
return 0 if result.get("ok") else 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _doctor(_args: argparse.Namespace) -> int:
|
|
50
|
+
return print_report(check_environment())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
54
|
+
parser = argparse.ArgumentParser(prog="autoneat")
|
|
55
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
56
|
+
|
|
57
|
+
doctor = sub.add_parser("doctor", help="Check macOS/Resolve automation prerequisites")
|
|
58
|
+
doctor.set_defaults(func=_doctor)
|
|
59
|
+
|
|
60
|
+
profile = sub.add_parser("profile", help="Run Neat Video Auto Profile over timeline clips")
|
|
61
|
+
profile.add_argument("--project", help="Resolve project name (default: current project)")
|
|
62
|
+
profile.add_argument("--timeline", help="Resolve timeline name (default: current timeline)")
|
|
63
|
+
profile.add_argument("--track", type=int, default=1, help="Video track to process")
|
|
64
|
+
profile.add_argument("--all-tracks", action="store_true", help="Process all video tracks")
|
|
65
|
+
profile.add_argument("--shot-ids", help="Comma/space-separated shot ids to include")
|
|
66
|
+
profile.add_argument("--start-from", type=int, default=1, help="1-based clip offset after filters")
|
|
67
|
+
profile.add_argument("--limit", type=int, default=0, help="Maximum clips to process")
|
|
68
|
+
profile.add_argument("--continue", dest="continue_run", action="store_true", help="Resume from state")
|
|
69
|
+
profile.add_argument("--retry-failed", action="store_true", help="Retry failed clips on resume")
|
|
70
|
+
profile.add_argument("--no-reuse-existing", action="store_true", help="Add a fresh Neat node")
|
|
71
|
+
profile.add_argument("--no-color-wrap", action="store_true", help="Skip ACES/HDR CST wrapping")
|
|
72
|
+
profile.add_argument("--state", help="Path to run state JSON")
|
|
73
|
+
profile.add_argument("--open-timeout", type=float, default=18.0)
|
|
74
|
+
profile.add_argument("--editor-timeout", type=float, default=60.0)
|
|
75
|
+
profile.add_argument("--prepare-timeout", type=float, default=1800.0)
|
|
76
|
+
profile.add_argument("--profile-wait", type=float, default=3.0)
|
|
77
|
+
profile.add_argument("--ready-timeout", type=float, default=90.0)
|
|
78
|
+
profile.add_argument("--apply-delay", type=float, default=5.0)
|
|
79
|
+
profile.add_argument("--close-timeout", type=float, default=20.0)
|
|
80
|
+
profile.add_argument("--step-delay", type=float, default=1.0)
|
|
81
|
+
profile.add_argument("--json", action="store_true", help="Print final summary JSON")
|
|
82
|
+
profile.set_defaults(func=_profile)
|
|
83
|
+
|
|
84
|
+
return parser
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(argv: list[str] | None = None) -> int:
|
|
88
|
+
parser = build_parser()
|
|
89
|
+
args = parser.parse_args(argv)
|
|
90
|
+
try:
|
|
91
|
+
return int(args.func(args) or 0)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
print("Interrupted", file=sys.stderr)
|
|
94
|
+
return 130
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Engine layer — pure Python, no UI imports."""
|