timeout-dead 0.1.1__tar.gz → 0.1.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.
- timeout_dead-0.1.2/.vscode/settings.json +8 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/AGENTS.md +25 -13
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/PKG-INFO +3 -2
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/README.md +2 -1
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/src/timeout_dead/_version.py +3 -3
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/src/timeout_dead/cli.py +38 -17
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/src/timeout_dead.egg-info/SOURCES.txt +1 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/tests/conftest.py +1 -1
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/tests/test_cli.py +18 -3
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/.github/workflows/publish.yml +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/.github/workflows/pyright.yml +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/.github/workflows/ruff.yml +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/.github/workflows/tests.yml +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/.gitignore +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/CODE-STYLE.md +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/CONTRIBUTING.md +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/LICENSE +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/MANIFEST.in +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/icon.png +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/pyproject.toml +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/ruff.toml +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/setup.cfg +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/src/timeout_dead/__init__.py +0 -0
- {timeout_dead-0.1.1 → timeout_dead-0.1.2}/uv.lock +0 -0
|
@@ -37,20 +37,32 @@ If the user asks "what should the commit message be?" — **suggest a message bu
|
|
|
37
37
|
|
|
38
38
|
## Workflow & Verification Commands
|
|
39
39
|
|
|
40
|
+
### Setup
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install timeout-dead
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Если уже установлена — убедись что работает:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
time-d -h
|
|
50
|
+
```
|
|
51
|
+
|
|
40
52
|
### Time limit (HARD REQUIREMENT)
|
|
41
53
|
|
|
42
54
|
**Every bash command MUST complete within 60 seconds.** All commands must be invoked through the timeout wrapper:
|
|
43
55
|
|
|
44
56
|
```bash
|
|
45
|
-
|
|
57
|
+
time-d --sec 60 "<your command>"
|
|
46
58
|
```
|
|
47
59
|
|
|
48
60
|
Long-running daemons must use `nohup ... >/dev/null 2>&1 &` so the wrapper returns immediately.
|
|
49
61
|
|
|
50
|
-
###
|
|
62
|
+
### Install dependencies
|
|
51
63
|
|
|
52
64
|
```bash
|
|
53
|
-
|
|
65
|
+
time-d --sec 60 "uv sync"
|
|
54
66
|
```
|
|
55
67
|
|
|
56
68
|
### Verify after changes
|
|
@@ -58,16 +70,16 @@ timeout-dead --sec 60 "uv sync"
|
|
|
58
70
|
Run **all** checks in this order — treat errors as blockers:
|
|
59
71
|
|
|
60
72
|
```bash
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
time-d --sec 60 "ruff check ."
|
|
74
|
+
time-d --sec 60 "ruff format --check ."
|
|
75
|
+
time-d --sec 60 "pyright ."
|
|
76
|
+
time-d --sec 60 "python -m pytest tests/ -v"
|
|
65
77
|
```
|
|
66
78
|
|
|
67
79
|
### Fix formatting & imports
|
|
68
80
|
|
|
69
81
|
```bash
|
|
70
|
-
|
|
82
|
+
time-d --sec 60 "ruff check --fix . && ruff format ."
|
|
71
83
|
```
|
|
72
84
|
|
|
73
85
|
**LSP is mandatory.** Configure `pyright-langserver` and `ruff server` in your editor. After every change, confirm lint, format, and type-check show **0 errors**. `ruff format` is the single source of truth for formatting — no `black`, no `isort`.
|
|
@@ -75,7 +87,7 @@ timeout-dead --sec 60 "ruff check --fix . && ruff format ."
|
|
|
75
87
|
### Run a single test
|
|
76
88
|
|
|
77
89
|
```bash
|
|
78
|
-
|
|
90
|
+
time-d --sec 60 "python -m pytest tests/test_file.py::test_name -v"
|
|
79
91
|
```
|
|
80
92
|
|
|
81
93
|
### Mandatory testing
|
|
@@ -170,19 +182,19 @@ class ConflictError(BaseError):
|
|
|
170
182
|
### Unit Tests
|
|
171
183
|
|
|
172
184
|
- Tests live in `tests/` mirroring the source structure.
|
|
173
|
-
- Run unit tests: `
|
|
185
|
+
- Run unit tests: `time-d --sec 60 "pytest tests/ -v --ignore=tests/integration"`.
|
|
174
186
|
|
|
175
187
|
### Integration Tests
|
|
176
188
|
|
|
177
189
|
- Place integration tests in `tests/integration/`. Use `@pytest.mark.integration` marker.
|
|
178
|
-
- Run integration tests: `
|
|
179
|
-
- Run unit tests only: `
|
|
190
|
+
- Run integration tests: `time-d --sec 60 "pytest tests/integration/ -v -m \"integration\""`.
|
|
191
|
+
- Run unit tests only: `time-d --sec 60 "pytest tests/ -v -m \"not integration\""`.
|
|
180
192
|
|
|
181
193
|
### Coverage
|
|
182
194
|
|
|
183
195
|
- Aim for high coverage of core business logic. Use `pytest-cov` to measure:
|
|
184
196
|
```bash
|
|
185
|
-
|
|
197
|
+
time-d --sec 60 "pytest tests/ --cov=src/timeout_dead --cov-report=term-missing"
|
|
186
198
|
```
|
|
187
199
|
|
|
188
200
|
## Environment & Configuration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: timeout-dead
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Lightweight command timeout utility with zero runtime dependencies
|
|
5
5
|
Author-email: Dmitry Krivoruchko <umbrella.leaf.for.work@gmail.com>
|
|
6
6
|
License-Expression: Unlicense
|
|
@@ -77,7 +77,7 @@ timeout-dead --no-output "curl -s https://example.com"
|
|
|
77
77
|
## Usage
|
|
78
78
|
|
|
79
79
|
```
|
|
80
|
-
usage: timeout-dead [-h] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
|
|
80
|
+
usage: timeout-dead [-h] [-v] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
|
|
81
81
|
|
|
82
82
|
Lightweight command timeout utility.
|
|
83
83
|
|
|
@@ -86,6 +86,7 @@ positional arguments:
|
|
|
86
86
|
|
|
87
87
|
options:
|
|
88
88
|
-h, --help show this help message and exit
|
|
89
|
+
-v, --version show version and exit
|
|
89
90
|
--sec SECONDS timeout in seconds (default: 60)
|
|
90
91
|
--signal SIGNAL signal to send on timeout (TERM, KILL, HUP, INT)
|
|
91
92
|
--no-output suppress normal output (stdout, stderr, header, footer)
|
|
@@ -51,7 +51,7 @@ timeout-dead --no-output "curl -s https://example.com"
|
|
|
51
51
|
## Usage
|
|
52
52
|
|
|
53
53
|
```
|
|
54
|
-
usage: timeout-dead [-h] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
|
|
54
|
+
usage: timeout-dead [-h] [-v] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
|
|
55
55
|
|
|
56
56
|
Lightweight command timeout utility.
|
|
57
57
|
|
|
@@ -60,6 +60,7 @@ positional arguments:
|
|
|
60
60
|
|
|
61
61
|
options:
|
|
62
62
|
-h, --help show this help message and exit
|
|
63
|
+
-v, --version show version and exit
|
|
63
64
|
--sec SECONDS timeout in seconds (default: 60)
|
|
64
65
|
--signal SIGNAL signal to send on timeout (TERM, KILL, HUP, INT)
|
|
65
66
|
--no-output suppress normal output (stdout, stderr, header, footer)
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g92736f7f7'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
"""
|
|
3
|
+
"""Lightweight command timeout utility."""
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import os
|
|
@@ -10,6 +10,8 @@ import subprocess
|
|
|
10
10
|
import sys
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
+
from importlib.metadata import PackageNotFoundError
|
|
14
|
+
from importlib.metadata import version as _get_version
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
# MARK: Constants
|
|
@@ -22,7 +24,7 @@ class _Const:
|
|
|
22
24
|
HEADER_SEPARATOR: str = "-" * 60
|
|
23
25
|
|
|
24
26
|
MSG_NO_COMMAND: str = "Error: no command specified"
|
|
25
|
-
MSG_BASH_NOT_FOUND: str = "bash not found in PATH
|
|
27
|
+
MSG_BASH_NOT_FOUND: str = "bash not found in PATH"
|
|
26
28
|
MSG_TIMEOUT: str = "Timeout exceeded {} seconds"
|
|
27
29
|
MSG_EXEC_ERROR: str = "Execution error: {}"
|
|
28
30
|
|
|
@@ -35,6 +37,17 @@ class _Const:
|
|
|
35
37
|
"INT": signal.SIGINT,
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _resolve_version() -> str:
|
|
42
|
+
try:
|
|
43
|
+
return _get_version("timeout-dead")
|
|
44
|
+
|
|
45
|
+
except PackageNotFoundError:
|
|
46
|
+
return "unknown"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_PROJECT_VERSION: str = _Const._resolve_version()
|
|
50
|
+
|
|
38
51
|
|
|
39
52
|
# MARK: Private Helpers
|
|
40
53
|
# ------------------------------------------------
|
|
@@ -49,13 +62,13 @@ def _is_windows() -> bool:
|
|
|
49
62
|
|
|
50
63
|
def _find_bash() -> str:
|
|
51
64
|
"""
|
|
52
|
-
|
|
65
|
+
Locate bash executable in PATH.
|
|
53
66
|
|
|
54
67
|
Returns:
|
|
55
|
-
str:
|
|
68
|
+
str: path to the bash executable
|
|
56
69
|
|
|
57
70
|
Raises:
|
|
58
|
-
SystemExit:
|
|
71
|
+
SystemExit: if bash is not found
|
|
59
72
|
"""
|
|
60
73
|
|
|
61
74
|
bash_path = shutil.which("bash")
|
|
@@ -77,7 +90,7 @@ def _terminate_process(
|
|
|
77
90
|
force: bool = False,
|
|
78
91
|
signal_num: int = signal.SIGTERM,
|
|
79
92
|
) -> None:
|
|
80
|
-
"""
|
|
93
|
+
"""Terminate the process — gracefully (chosen signal) or forcefully."""
|
|
81
94
|
|
|
82
95
|
if process.poll() is not None:
|
|
83
96
|
return
|
|
@@ -115,7 +128,7 @@ def _kill_with_timeout(
|
|
|
115
128
|
timeout: int,
|
|
116
129
|
signal_num: int = signal.SIGTERM,
|
|
117
130
|
) -> None:
|
|
118
|
-
"""
|
|
131
|
+
"""Kill the process after timeout with two-stage logic."""
|
|
119
132
|
|
|
120
133
|
if process.poll() is not None:
|
|
121
134
|
return
|
|
@@ -140,16 +153,16 @@ def run_command(
|
|
|
140
153
|
no_output: bool = False,
|
|
141
154
|
) -> int:
|
|
142
155
|
"""
|
|
143
|
-
|
|
156
|
+
Run a command with timeout via bash.
|
|
144
157
|
|
|
145
158
|
Args:
|
|
146
|
-
command_string (str):
|
|
147
|
-
timeout (int):
|
|
148
|
-
signal_name (str):
|
|
149
|
-
no_output (bool):
|
|
159
|
+
command_string (str): command to execute
|
|
160
|
+
timeout (int): timeout in seconds
|
|
161
|
+
signal_name (str): signal name for graceful termination
|
|
162
|
+
no_output (bool): suppress normal output
|
|
150
163
|
|
|
151
164
|
Returns:
|
|
152
|
-
int:
|
|
165
|
+
int: process return code (-1 on launch error)
|
|
153
166
|
"""
|
|
154
167
|
|
|
155
168
|
signal_num = _Const.SIGNAL_MAP.get(signal_name, signal.SIGTERM)
|
|
@@ -190,6 +203,7 @@ def run_command(
|
|
|
190
203
|
except Exception as e:
|
|
191
204
|
if timer:
|
|
192
205
|
timer.cancel()
|
|
206
|
+
|
|
193
207
|
if process and process.poll() is None:
|
|
194
208
|
try:
|
|
195
209
|
_terminate_process(process, force=True)
|
|
@@ -206,13 +220,20 @@ def run_command(
|
|
|
206
220
|
|
|
207
221
|
|
|
208
222
|
def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
|
|
209
|
-
"""
|
|
223
|
+
"""Parse command-line arguments."""
|
|
210
224
|
|
|
211
225
|
parser = argparse.ArgumentParser(
|
|
212
226
|
description="Lightweight command timeout utility.",
|
|
213
227
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
214
228
|
)
|
|
215
229
|
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"-v",
|
|
232
|
+
"--version",
|
|
233
|
+
action="version",
|
|
234
|
+
version=f"timeout-dead {_PROJECT_VERSION}",
|
|
235
|
+
)
|
|
236
|
+
|
|
216
237
|
parser.add_argument(
|
|
217
238
|
"--sec",
|
|
218
239
|
type=int,
|
|
@@ -251,7 +272,7 @@ def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
251
272
|
|
|
252
273
|
|
|
253
274
|
def print_header(command: str, timeout: int) -> None:
|
|
254
|
-
"""
|
|
275
|
+
"""Print execution header."""
|
|
255
276
|
|
|
256
277
|
print(f"Running: {command}")
|
|
257
278
|
print(f"Timeout: {timeout} seconds")
|
|
@@ -262,7 +283,7 @@ def print_header(command: str, timeout: int) -> None:
|
|
|
262
283
|
|
|
263
284
|
|
|
264
285
|
def print_footer(return_code: int) -> None:
|
|
265
|
-
"""
|
|
286
|
+
"""Print execution footer."""
|
|
266
287
|
|
|
267
288
|
print(_Const.HEADER_SEPARATOR)
|
|
268
289
|
print(f"Exit code: {return_code}")
|
|
@@ -272,7 +293,7 @@ def print_footer(return_code: int) -> None:
|
|
|
272
293
|
|
|
273
294
|
|
|
274
295
|
def main(argv: list[str] | None = None) -> None:
|
|
275
|
-
"""
|
|
296
|
+
"""Main entry point."""
|
|
276
297
|
|
|
277
298
|
args = parse_arguments(argv)
|
|
278
299
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Tests for timeout-dead CLI utility."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import signal
|
|
@@ -30,7 +30,7 @@ def _run_cli(
|
|
|
30
30
|
*args: str,
|
|
31
31
|
timeout: int = 10,
|
|
32
32
|
) -> subprocess.CompletedProcess[str]:
|
|
33
|
-
"""
|
|
33
|
+
"""Run timeout-dead as a subprocess."""
|
|
34
34
|
|
|
35
35
|
return subprocess.run(
|
|
36
36
|
[sys.executable, "-m", "timeout_dead.cli", *args],
|
|
@@ -245,7 +245,7 @@ class TestSignalSelection:
|
|
|
245
245
|
reason="SIGINT file-based test requires Unix signal handling",
|
|
246
246
|
)
|
|
247
247
|
def test_signal_int_handling(self, tmp_path: Path) -> None:
|
|
248
|
-
"""Python
|
|
248
|
+
"""Python script catches SIGINT and writes signal number to file."""
|
|
249
249
|
|
|
250
250
|
marker = tmp_path / "signal.txt"
|
|
251
251
|
script_path = tmp_path / "handler.py"
|
|
@@ -395,6 +395,21 @@ class TestIntegration:
|
|
|
395
395
|
assert "--sec" in result.stdout
|
|
396
396
|
assert "--signal" in result.stdout
|
|
397
397
|
assert "--no-output" in result.stdout
|
|
398
|
+
assert "--version" in result.stdout
|
|
399
|
+
|
|
400
|
+
# ------------------------------------------------
|
|
401
|
+
|
|
402
|
+
def test_cli_version_long(self) -> None:
|
|
403
|
+
result = _run_cli("--version")
|
|
404
|
+
assert result.returncode == 0
|
|
405
|
+
assert "timeout-dead" in result.stdout
|
|
406
|
+
|
|
407
|
+
# ------------------------------------------------
|
|
408
|
+
|
|
409
|
+
def test_cli_version_short(self) -> None:
|
|
410
|
+
result = _run_cli("-v")
|
|
411
|
+
assert result.returncode == 0
|
|
412
|
+
assert "timeout-dead" in result.stdout
|
|
398
413
|
|
|
399
414
|
# ------------------------------------------------
|
|
400
415
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|