envlock-cli 1.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Serber1990
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.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: envlock-cli
3
+ Version: 1.1.0
4
+ Summary: Freeze and compare Python, Node, and system environments — detect dependency drift before it breaks your builds.
5
+ Author-email: Serber1990 <serber1990@pm.me>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/serber1990/envlock
8
+ Project-URL: Repository, https://github.com/serber1990/envlock
9
+ Project-URL: Issues, https://github.com/serber1990/envlock/issues
10
+ Project-URL: Changelog, https://github.com/serber1990/envlock/blob/main/CHANGELOG.md
11
+ Keywords: environment,dependencies,drift,reproducibility,devops,ci,python,node
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: POSIX
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: shellcolorize>=1.1.0
23
+ Requires-Dist: tomli>=1.1; python_version < "3.11"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == "dev"
26
+ Requires-Dist: ruff>=0.4; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # envlock
30
+
31
+ [![CI](https://github.com/serber1990/envlock/actions/workflows/ci.yml/badge.svg)](https://github.com/serber1990/envlock/actions/workflows/ci.yml)
32
+ [![PyPI version](https://badge.fury.io/py/envlock-cli.svg)](https://badge.fury.io/py/envlock-cli)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
34
+
35
+ Freeze your environment. Detect when it drifts.
36
+
37
+ **envlock** snapshots a project's Python packages, Node.js dependencies and system runtimes, then tells you
38
+ exactly what changed — so "works on my machine" stops being an excuse. Use it locally, in CI, or to compare
39
+ staging with production.
40
+
41
+ ---
42
+
43
+ ## ✨ What it tracks
44
+
45
+ | Layer | What's captured |
46
+ |-------|----------------|
47
+ | **Python** | Interpreter and version (the project's `.venv` / `venv` / `env`, or the active virtualenv), installed packages, `requirements.txt`, `pyproject.toml` dependencies |
48
+ | **Node.js** | Node and npm versions, `package.json` dependencies + devDependencies, `package-lock.json` resolved versions, `yarn.lock` presence |
49
+ | **System** | OS, architecture, Go / Ruby / Java / Rust versions, relevant env vars (`VIRTUAL_ENV`, `NODE_ENV`, `JAVA_HOME`…) |
50
+
51
+ ### Severity levels
52
+
53
+ | Severity | Examples |
54
+ |----------|----------|
55
+ | ⛔ **CRITICAL** | Python/Node major or minor version changed (3.12 → 3.13), OS or architecture changed |
56
+ | ⚠️ **WARNING** | Package removed or downgraded, declared constraint changed, runtime patch release, lockfile or env var removed |
57
+ | ℹ️ **INFO** | Package added or upgraded, new lockfile or env var |
58
+
59
+ ---
60
+
61
+ ## 📥 Installation
62
+
63
+ ```bash
64
+ pip install envlock-cli # the command is `envlock`
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 🛠 Usage
70
+
71
+ ### 1 — Take a baseline snapshot
72
+
73
+ ```bash
74
+ envlock snapshot # saves .envlock.json in the current directory
75
+ envlock snapshot --path /srv/myapp # scan another project
76
+ envlock snapshot --output locks/prod.json # custom location
77
+ ```
78
+
79
+ ### 2 — Check for drift
80
+
81
+ ```bash
82
+ envlock check # terminal report
83
+ envlock check --format json | jq .summary # JSON on stdout (progress goes to stderr)
84
+ envlock check --format markdown -o drift-report.md # Markdown report file
85
+ envlock check --baseline locks/prod.json --fail-on critical
86
+ ```
87
+
88
+ ### 3 — Diff any two snapshots
89
+
90
+ ```bash
91
+ envlock diff staging.json production.json
92
+ envlock diff before.json after.json --format markdown -o drift.md
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 📄 Example output
98
+
99
+ ```
100
+ ╔════════════════════════════════════════╗
101
+ ║ envlock · environment drift report ║
102
+ ╚════════════════════════════════════════╝
103
+
104
+ Baseline 2026-05-10T09:00:00+00:00 (dev-laptop) /srv/app
105
+ Current 2026-05-12T14:22:18+00:00 (dev-laptop) /srv/app
106
+
107
+
108
+ ── Python Runtime ────────────────────────────
109
+ ⛔ ~ Python version changed 3.12.1 → 3.13.0
110
+
111
+ ── Python Packages ───────────────────────────
112
+ · + pip: httpx added 0.27.0
113
+ · ~ pip: requests upgraded 2.31.0 → 2.32.3
114
+ ⚠ ~ pip: urllib3 downgraded 2.0.7 → 1.26.18
115
+
116
+ ── Node.js Packages ──────────────────────────
117
+ · + npm dep: express added ^4.19.2
118
+
119
+ ────────────────────────────────────────────
120
+ 5 changes detected (1 critical, 1 warning, 3 info)
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 🔁 Use in CI
126
+
127
+ The exit code tells your pipeline what happened:
128
+
129
+ | Exit code | Meaning |
130
+ |-----------|---------|
131
+ | `0` | No drift at or above `--fail-on` |
132
+ | `1` | Drift detected |
133
+ | `2` | Error (missing or invalid baseline, unreadable path) |
134
+
135
+ ```yaml
136
+ # GitHub Actions — fail only on critical drift, keep the report as an artifact
137
+ - name: Check environment drift
138
+ run: envlock check --baseline .envlock.json --fail-on critical --format markdown -o drift-report.md
139
+ - uses: actions/upload-artifact@v4
140
+ if: always()
141
+ with:
142
+ name: drift-report
143
+ path: drift-report.md
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 📋 Options
149
+
150
+ ### `envlock snapshot`
151
+ | Option | Description |
152
+ |--------|-------------|
153
+ | `-p`, `--path DIR` | Project directory to scan (default: `.`) |
154
+ | `-o`, `--output FILE` | Where to save the baseline (default: `.envlock.json`) |
155
+
156
+ ### `envlock check` / `envlock diff BASELINE CURRENT`
157
+ | Option | Description |
158
+ |--------|-------------|
159
+ | `-b`, `--baseline FILE` | *(check)* Baseline to compare against (default: `.envlock.json`) |
160
+ | `-p`, `--path DIR` | *(check)* Project directory to scan (default: `.`) |
161
+ | `-f`, `--format` | `terminal` (default), `json` or `markdown` |
162
+ | `-o`, `--output FILE` | Write the report to a file (JSON or Markdown) |
163
+ | `--fail-on LEVEL` | `info` (default: any change), `warning`, `critical` or `never` |
164
+
165
+ ---
166
+
167
+ ## 🧪 Development
168
+
169
+ ```bash
170
+ pip install -e ".[dev]"
171
+ ruff check .
172
+ pytest
173
+ ```
174
+
175
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
176
+
177
+ ---
178
+
179
+ ## 📝 License
180
+
181
+ MIT — see [LICENSE](LICENSE).
182
+
183
+ ## 🌐 Connect
184
+
185
+ [![GitHub](https://img.shields.io/badge/GitHub-@serber1990-181717?style=flat-square&logo=github)](https://github.com/serber1990)
@@ -0,0 +1,157 @@
1
+ # envlock
2
+
3
+ [![CI](https://github.com/serber1990/envlock/actions/workflows/ci.yml/badge.svg)](https://github.com/serber1990/envlock/actions/workflows/ci.yml)
4
+ [![PyPI version](https://badge.fury.io/py/envlock-cli.svg)](https://badge.fury.io/py/envlock-cli)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+
7
+ Freeze your environment. Detect when it drifts.
8
+
9
+ **envlock** snapshots a project's Python packages, Node.js dependencies and system runtimes, then tells you
10
+ exactly what changed — so "works on my machine" stops being an excuse. Use it locally, in CI, or to compare
11
+ staging with production.
12
+
13
+ ---
14
+
15
+ ## ✨ What it tracks
16
+
17
+ | Layer | What's captured |
18
+ |-------|----------------|
19
+ | **Python** | Interpreter and version (the project's `.venv` / `venv` / `env`, or the active virtualenv), installed packages, `requirements.txt`, `pyproject.toml` dependencies |
20
+ | **Node.js** | Node and npm versions, `package.json` dependencies + devDependencies, `package-lock.json` resolved versions, `yarn.lock` presence |
21
+ | **System** | OS, architecture, Go / Ruby / Java / Rust versions, relevant env vars (`VIRTUAL_ENV`, `NODE_ENV`, `JAVA_HOME`…) |
22
+
23
+ ### Severity levels
24
+
25
+ | Severity | Examples |
26
+ |----------|----------|
27
+ | ⛔ **CRITICAL** | Python/Node major or minor version changed (3.12 → 3.13), OS or architecture changed |
28
+ | ⚠️ **WARNING** | Package removed or downgraded, declared constraint changed, runtime patch release, lockfile or env var removed |
29
+ | ℹ️ **INFO** | Package added or upgraded, new lockfile or env var |
30
+
31
+ ---
32
+
33
+ ## 📥 Installation
34
+
35
+ ```bash
36
+ pip install envlock-cli # the command is `envlock`
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 🛠 Usage
42
+
43
+ ### 1 — Take a baseline snapshot
44
+
45
+ ```bash
46
+ envlock snapshot # saves .envlock.json in the current directory
47
+ envlock snapshot --path /srv/myapp # scan another project
48
+ envlock snapshot --output locks/prod.json # custom location
49
+ ```
50
+
51
+ ### 2 — Check for drift
52
+
53
+ ```bash
54
+ envlock check # terminal report
55
+ envlock check --format json | jq .summary # JSON on stdout (progress goes to stderr)
56
+ envlock check --format markdown -o drift-report.md # Markdown report file
57
+ envlock check --baseline locks/prod.json --fail-on critical
58
+ ```
59
+
60
+ ### 3 — Diff any two snapshots
61
+
62
+ ```bash
63
+ envlock diff staging.json production.json
64
+ envlock diff before.json after.json --format markdown -o drift.md
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 📄 Example output
70
+
71
+ ```
72
+ ╔════════════════════════════════════════╗
73
+ ║ envlock · environment drift report ║
74
+ ╚════════════════════════════════════════╝
75
+
76
+ Baseline 2026-05-10T09:00:00+00:00 (dev-laptop) /srv/app
77
+ Current 2026-05-12T14:22:18+00:00 (dev-laptop) /srv/app
78
+
79
+
80
+ ── Python Runtime ────────────────────────────
81
+ ⛔ ~ Python version changed 3.12.1 → 3.13.0
82
+
83
+ ── Python Packages ───────────────────────────
84
+ · + pip: httpx added 0.27.0
85
+ · ~ pip: requests upgraded 2.31.0 → 2.32.3
86
+ ⚠ ~ pip: urllib3 downgraded 2.0.7 → 1.26.18
87
+
88
+ ── Node.js Packages ──────────────────────────
89
+ · + npm dep: express added ^4.19.2
90
+
91
+ ────────────────────────────────────────────
92
+ 5 changes detected (1 critical, 1 warning, 3 info)
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 🔁 Use in CI
98
+
99
+ The exit code tells your pipeline what happened:
100
+
101
+ | Exit code | Meaning |
102
+ |-----------|---------|
103
+ | `0` | No drift at or above `--fail-on` |
104
+ | `1` | Drift detected |
105
+ | `2` | Error (missing or invalid baseline, unreadable path) |
106
+
107
+ ```yaml
108
+ # GitHub Actions — fail only on critical drift, keep the report as an artifact
109
+ - name: Check environment drift
110
+ run: envlock check --baseline .envlock.json --fail-on critical --format markdown -o drift-report.md
111
+ - uses: actions/upload-artifact@v4
112
+ if: always()
113
+ with:
114
+ name: drift-report
115
+ path: drift-report.md
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 📋 Options
121
+
122
+ ### `envlock snapshot`
123
+ | Option | Description |
124
+ |--------|-------------|
125
+ | `-p`, `--path DIR` | Project directory to scan (default: `.`) |
126
+ | `-o`, `--output FILE` | Where to save the baseline (default: `.envlock.json`) |
127
+
128
+ ### `envlock check` / `envlock diff BASELINE CURRENT`
129
+ | Option | Description |
130
+ |--------|-------------|
131
+ | `-b`, `--baseline FILE` | *(check)* Baseline to compare against (default: `.envlock.json`) |
132
+ | `-p`, `--path DIR` | *(check)* Project directory to scan (default: `.`) |
133
+ | `-f`, `--format` | `terminal` (default), `json` or `markdown` |
134
+ | `-o`, `--output FILE` | Write the report to a file (JSON or Markdown) |
135
+ | `--fail-on LEVEL` | `info` (default: any change), `warning`, `critical` or `never` |
136
+
137
+ ---
138
+
139
+ ## 🧪 Development
140
+
141
+ ```bash
142
+ pip install -e ".[dev]"
143
+ ruff check .
144
+ pytest
145
+ ```
146
+
147
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
148
+
149
+ ---
150
+
151
+ ## 📝 License
152
+
153
+ MIT — see [LICENSE](LICENSE).
154
+
155
+ ## 🌐 Connect
156
+
157
+ [![GitHub](https://img.shields.io/badge/GitHub-@serber1990-181717?style=flat-square&logo=github)](https://github.com/serber1990)
@@ -0,0 +1,5 @@
1
+ __version__ = "1.1.0"
2
+
3
+ from .cli import main # noqa: E402 (after __version__: cli imports it)
4
+
5
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ envlock — freeze a project's Python/Node/system environment and detect drift.
4
+
5
+ Exit codes: 0 no drift (at or above --fail-on) 1 drift detected 2 usage or I/O error
6
+ """
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from shellcolorize import Color
12
+
13
+ from . import __version__
14
+ from .diff import diff_snapshots
15
+ from .renderer import render_json, render_markdown, render_terminal
16
+ from .snapshot import DEFAULT_PATH, EnvSnapshot, take_snapshot
17
+
18
+ EXIT_OK, EXIT_DRIFT, EXIT_ERROR = 0, 1, 2
19
+ FAIL_LEVELS = {'info': 'INFO', 'warning': 'WARNING', 'critical': 'CRITICAL'}
20
+
21
+
22
+ # Progress and errors go to stderr so stdout only carries the report (safe to pipe into jq).
23
+ def _ok(msg): print(f" {Color.GREEN}✔{Color.RESET} {msg}", file=sys.stderr)
24
+ def _step(msg): print(f" {Color.CYAN}▶{Color.RESET} {msg}", file=sys.stderr)
25
+ def _err(msg): print(f" {Color.RED}✖{Color.RESET} {msg}", file=sys.stderr)
26
+
27
+
28
+ # ── snapshot ──────────────────────────────────────────────────────────────────
29
+
30
+ def cmd_snapshot(args: argparse.Namespace) -> int:
31
+ output = Path(args.output) if args.output else DEFAULT_PATH
32
+ path = Path(args.path)
33
+ if not path.is_dir():
34
+ _err(f'Not a directory: {path}')
35
+ return EXIT_ERROR
36
+
37
+ print(f"\n {Color.CYAN}{Color.BOLD}envlock · snapshot{Color.RESET}\n", file=sys.stderr)
38
+ _step(f'Scanning environment at {path.resolve()}...')
39
+ snap = take_snapshot(path=str(path))
40
+
41
+ try:
42
+ snap.save(output)
43
+ except OSError as e:
44
+ _err(f'Cannot write {output}: {e.strerror}')
45
+ return EXIT_ERROR
46
+
47
+ py, nd, system = snap.python, snap.node, snap.system
48
+ n_pkgs = len(py.get('packages', {}).get('pip_installed', {}))
49
+ print()
50
+ _ok(f'Baseline saved: {output}')
51
+ print(f" Python : {py.get('python_version') or 'not found'} "
52
+ f"({n_pkgs} packages, {py.get('interpreter') or 'no interpreter'})")
53
+ print(f" Node : {nd.get('node_version') or 'not found'} "
54
+ f"(files: {', '.join(nd.get('lockfiles', [])) or 'none'})")
55
+ print(f" OS : {system.get('os') or '?'} [{system.get('arch') or '?'}]")
56
+ print(f" Runtimes : {', '.join(system.get('runtimes', {})) or 'none detected'}")
57
+ print()
58
+ return EXIT_OK
59
+
60
+ # ── check / diff ──────────────────────────────────────────────────────────────
61
+
62
+ def _load(path: Path):
63
+ try:
64
+ return EnvSnapshot.load(path)
65
+ except FileNotFoundError:
66
+ _err(f'Snapshot not found: {path}')
67
+ except (OSError, ValueError) as e:
68
+ _err(f'Cannot read snapshot {path}: {e}')
69
+ return None
70
+
71
+
72
+ def _report(report, args: argparse.Namespace) -> int:
73
+ fmt = args.format
74
+ if fmt == 'terminal' and args.output:
75
+ fmt = 'markdown' if str(args.output).endswith('.md') else 'json'
76
+
77
+ if fmt == 'terminal':
78
+ render_terminal(report)
79
+ else:
80
+ text = render_json(report) if fmt == 'json' else render_markdown(report)
81
+ if args.output:
82
+ try:
83
+ Path(args.output).write_text(text + '\n', encoding='utf-8')
84
+ except OSError as e:
85
+ _err(f'Cannot write {args.output}: {e.strerror}')
86
+ return EXIT_ERROR
87
+ _ok(f'Report saved to {args.output}')
88
+ else:
89
+ print(text)
90
+
91
+ if args.fail_on == 'never':
92
+ return EXIT_OK
93
+ return EXIT_DRIFT if report.has_at_least(FAIL_LEVELS[args.fail_on]) else EXIT_OK
94
+
95
+
96
+ def cmd_check(args: argparse.Namespace) -> int:
97
+ baseline_path = Path(args.baseline) if args.baseline else DEFAULT_PATH
98
+ if not baseline_path.exists():
99
+ _err(f'Baseline not found: {baseline_path}')
100
+ _err("Run 'envlock snapshot' first to create one.")
101
+ return EXIT_ERROR
102
+ baseline = _load(baseline_path)
103
+ if baseline is None:
104
+ return EXIT_ERROR
105
+
106
+ _step('Collecting current environment...')
107
+ current = take_snapshot(path=args.path)
108
+ return _report(diff_snapshots(baseline, current), args)
109
+
110
+
111
+ def cmd_diff(args: argparse.Namespace) -> int:
112
+ snap_a, snap_b = _load(Path(args.snapshot_a)), _load(Path(args.snapshot_b))
113
+ if snap_a is None or snap_b is None:
114
+ return EXIT_ERROR
115
+ return _report(diff_snapshots(snap_a, snap_b), args)
116
+
117
+ # ── main ──────────────────────────────────────────────────────────────────────
118
+
119
+ def _add_report_options(p: argparse.ArgumentParser) -> None:
120
+ p.add_argument('--format', '-f', choices=['terminal', 'json', 'markdown'], default='terminal',
121
+ help='Report format (default: terminal)')
122
+ p.add_argument('--output', '-o', metavar='FILE',
123
+ help='Write the report to FILE (json or markdown; inferred from .md/.json if needed)')
124
+ p.add_argument('--fail-on', choices=['info', 'warning', 'critical', 'never'], default='info',
125
+ help='Lowest severity that makes the exit code 1 (default: info = any change)')
126
+
127
+
128
+ def build_parser() -> argparse.ArgumentParser:
129
+ parser = argparse.ArgumentParser(
130
+ prog='envlock',
131
+ description='Freeze Python, Node and system environments — detect drift before it breaks your builds.',
132
+ epilog='exit codes: 0 no drift · 1 drift detected · 2 error',
133
+ )
134
+ parser.add_argument('-v', '--version', action='version', version=f'envlock {__version__}')
135
+ sub = parser.add_subparsers(dest='command', metavar='COMMAND')
136
+
137
+ p_snap = sub.add_parser('snapshot', help='Capture the current environment as baseline')
138
+ p_snap.add_argument('--path', '-p', metavar='DIR', default='.',
139
+ help='Project directory to scan (default: current dir)')
140
+ p_snap.add_argument('--output', '-o', metavar='FILE',
141
+ help=f'Output path (default: {DEFAULT_PATH})')
142
+
143
+ p_check = sub.add_parser('check', help='Compare the current environment against the baseline')
144
+ p_check.add_argument('--baseline', '-b', metavar='FILE',
145
+ help=f'Baseline to compare against (default: {DEFAULT_PATH})')
146
+ p_check.add_argument('--path', '-p', metavar='DIR', default='.',
147
+ help='Project directory to scan (default: current dir)')
148
+ _add_report_options(p_check)
149
+
150
+ p_diff = sub.add_parser('diff', help='Compare two snapshot files')
151
+ p_diff.add_argument('snapshot_a', metavar='BASELINE')
152
+ p_diff.add_argument('snapshot_b', metavar='CURRENT')
153
+ _add_report_options(p_diff)
154
+ return parser
155
+
156
+
157
+ def main(argv=None) -> None:
158
+ parser = build_parser()
159
+ args = parser.parse_args(argv)
160
+ Color.auto()
161
+
162
+ if args.command is None:
163
+ parser.print_help()
164
+ sys.exit(EXIT_OK)
165
+
166
+ dispatch = {'snapshot': cmd_snapshot, 'check': cmd_check, 'diff': cmd_diff}
167
+ try:
168
+ sys.exit(dispatch[args.command](args))
169
+ except KeyboardInterrupt:
170
+ _err('Interrupted')
171
+ sys.exit(130)
172
+
173
+
174
+ if __name__ == '__main__':
175
+ main()