keep-awake 1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kevin Chen
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,17 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ include setup.py
5
+
6
+ recursive-include native_code *.c *.h
7
+
8
+ exclude AGENTS.md
9
+ exclude .gitignore
10
+ prune .venv
11
+ prune .github
12
+ prune .claude
13
+ prune .vscode
14
+ prune tests
15
+ prune docs
16
+ prune build
17
+ prune dist
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: keep_awake
3
+ Version: 1.2.0
4
+ Summary: Prevent your computer from going to sleep.
5
+ Author-email: Kevin Chen <1354016594@qq.com>
6
+ License: MIT
7
+ Project-URL: Home-page, https://github.com/czf0613/keep_awake_py
8
+ Keywords: sleep,prevent,awake,idle
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: linux
17
+ Requires-Dist: jeepney>=0.9.0; extra == "linux"
18
+ Dynamic: license
19
+ Dynamic: license-file
20
+
21
+ # Keep Awake
22
+
23
+ Prevent idle system sleep and keep the display awake while Python is doing work.
24
+ Supports **CPython 3.8+**, including free-threaded (no-GIL) CPython 3.13/3.14.
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ # Windows / macOS
30
+ pip install keep_awake
31
+
32
+ # Linux desktop
33
+ pip install 'keep_awake[linux]'
34
+ ```
35
+
36
+ Windows and macOS use a C extension. A matching wheel avoids a local compiler;
37
+ otherwise installation builds from source. Linux uses the pure Python
38
+ [Jeepney](https://jeepney.readthedocs.io/en/latest/) D-Bus client and requires no
39
+ project C extension or D-Bus development headers.
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ from keep_awake import prevent_sleep, allow_sleep
45
+
46
+ if not prevent_sleep():
47
+ raise RuntimeError("Could not acquire a sleep inhibitor")
48
+ try:
49
+ # Do long-running work here.
50
+ pass
51
+ finally:
52
+ allow_sleep()
53
+ ```
54
+
55
+ For automatic cleanup:
56
+
57
+ ```python
58
+ from keep_awake import KeepAwakeGuard
59
+
60
+ with KeepAwakeGuard():
61
+ # Do long-running work here.
62
+ pass
63
+ ```
64
+
65
+ - Every successful `prevent_sleep()` adds one reference and returns `True`.
66
+ Failure returns `False` without adding a reference.
67
+ - Pair each successful acquisition with one `allow_sleep()`, which returns `None`.
68
+ The OS inhibitor is released only when the shared reference count reaches zero.
69
+ Calling `allow_sleep()` when the count is already zero does nothing.
70
+ - Calls are synchronized across threads, including no-GIL builds. All callers
71
+ share **one OS inhibitor with a reference count**. If thread A acquires and
72
+ thread B acquires then releases, the count changes `1 → 2 → 1`, so sleep remains
73
+ inhibited until A releases. A release may happen on a different thread.
74
+ - Use the public API only. Calling the private C extension directly bypasses
75
+ synchronization, reference counting and exit cleanup, and is unsupported.
76
+ - Nested and overlapping `KeepAwakeGuard` scopes are supported, including reuse
77
+ of the same guard. Each scope releases only its own successful acquisition.
78
+ Acquisition failure still does not raise; use the boolean-returning API when
79
+ success is required.
80
+ - On normal interpreter shutdown, one shared `atexit` handler releases the backend
81
+ even if several references remain. Acquisitions after this cleanup return `False`.
82
+ Forced termination, interpreter crashes and `os._exit()` bypass this handler;
83
+ use a guard or `try/finally` for cleanup during normal execution.
84
+ These requests do not override manual sleep, lid closure, or administrator power
85
+ policy.
86
+
87
+ ## Platforms
88
+
89
+ | Platform | Implementation |
90
+ | --- | --- |
91
+ | macOS arm64 / x86_64 | IOKit display-idle assertion, serialized by the Python API |
92
+ | Windows x86_64 / ARM64 | `SetThreadExecutionState` on a dedicated, synchronized worker |
93
+ | Linux desktop | GNOME SessionManager, falling back to `org.freedesktop.ScreenSaver` |
94
+
95
+ On Linux, run inside a logged-in graphical session with
96
+ `DBUS_SESSION_BUS_ADDRESS` set and a supported inhibition service. GNOME requests
97
+ suspend and idle inhibition; the freedesktop fallback depends on the desktop's
98
+ implementation of idle inhibition. Headless servers and minimal compositors may
99
+ not provide either service. An unavailable bus/service returns `False`; omitting
100
+ the required `linux` extra produces a dependency import error.
101
+
102
+ The connection opens on first acquisition and closes on release, including when
103
+ release fails. If the desktop/session service restarts, release and reacquire the
104
+ inhibitor. Actual GNOME/KDE power behavior still requires desktop validation.
105
+
106
+ Native Windows ARM64 wheels are built for Python 3.11–3.14 and 3.13t/3.14t.
107
+ Python 3.8–3.10 remains supported on Windows x86_64. This matches the native ARM64
108
+ interpreters available through uv.
109
+
110
+ ## Migrating from the idempotent API
111
+
112
+ Repeated successful calls now require matching releases. Code that previously
113
+ called `prevent_sleep()` many times but released only once must balance its calls
114
+ or use a guard around each unit of work. Do not manually release an acquisition
115
+ already owned by a guard, or release after a failed acquisition: the counter is
116
+ shared and cannot identify a mismatched manual release.
117
+
118
+ ## Development and publishing
119
+
120
+ ```sh
121
+ uv sync --frozen
122
+ uv run --frozen pytest -q
123
+ uv build
124
+ ```
125
+
126
+ CI checks Python 3.8–3.14 and free-threaded 3.13t/3.14t on Linux, Windows x86_64 and
127
+ both macOS architectures, plus the six Windows ARM64 configurations above,
128
+ whenever changes reach `master` or a pull request targets it.
129
+ Publishing a GitHub Release runs the checks and builds before uploading the
130
+ sdist and native wheels to PyPI through Trusted Publishing.
131
+
132
+ See [development and release instructions](https://github.com/czf0613/keep_awake_py/blob/master/docs/DEVELOPMENT.md),
133
+ [compatibility findings and verification limits](https://github.com/czf0613/keep_awake_py/blob/master/docs/COMPATIBILITY.md), and
134
+ [Codex repository guidance](https://github.com/czf0613/keep_awake_py/blob/master/AGENTS.md).
135
+
136
+ ## License
137
+
138
+ [MIT](https://github.com/czf0613/keep_awake_py/blob/master/LICENSE)
@@ -0,0 +1,118 @@
1
+ # Keep Awake
2
+
3
+ Prevent idle system sleep and keep the display awake while Python is doing work.
4
+ Supports **CPython 3.8+**, including free-threaded (no-GIL) CPython 3.13/3.14.
5
+
6
+ ## Installation
7
+
8
+ ```sh
9
+ # Windows / macOS
10
+ pip install keep_awake
11
+
12
+ # Linux desktop
13
+ pip install 'keep_awake[linux]'
14
+ ```
15
+
16
+ Windows and macOS use a C extension. A matching wheel avoids a local compiler;
17
+ otherwise installation builds from source. Linux uses the pure Python
18
+ [Jeepney](https://jeepney.readthedocs.io/en/latest/) D-Bus client and requires no
19
+ project C extension or D-Bus development headers.
20
+
21
+ ## Usage
22
+
23
+ ```python
24
+ from keep_awake import prevent_sleep, allow_sleep
25
+
26
+ if not prevent_sleep():
27
+ raise RuntimeError("Could not acquire a sleep inhibitor")
28
+ try:
29
+ # Do long-running work here.
30
+ pass
31
+ finally:
32
+ allow_sleep()
33
+ ```
34
+
35
+ For automatic cleanup:
36
+
37
+ ```python
38
+ from keep_awake import KeepAwakeGuard
39
+
40
+ with KeepAwakeGuard():
41
+ # Do long-running work here.
42
+ pass
43
+ ```
44
+
45
+ - Every successful `prevent_sleep()` adds one reference and returns `True`.
46
+ Failure returns `False` without adding a reference.
47
+ - Pair each successful acquisition with one `allow_sleep()`, which returns `None`.
48
+ The OS inhibitor is released only when the shared reference count reaches zero.
49
+ Calling `allow_sleep()` when the count is already zero does nothing.
50
+ - Calls are synchronized across threads, including no-GIL builds. All callers
51
+ share **one OS inhibitor with a reference count**. If thread A acquires and
52
+ thread B acquires then releases, the count changes `1 → 2 → 1`, so sleep remains
53
+ inhibited until A releases. A release may happen on a different thread.
54
+ - Use the public API only. Calling the private C extension directly bypasses
55
+ synchronization, reference counting and exit cleanup, and is unsupported.
56
+ - Nested and overlapping `KeepAwakeGuard` scopes are supported, including reuse
57
+ of the same guard. Each scope releases only its own successful acquisition.
58
+ Acquisition failure still does not raise; use the boolean-returning API when
59
+ success is required.
60
+ - On normal interpreter shutdown, one shared `atexit` handler releases the backend
61
+ even if several references remain. Acquisitions after this cleanup return `False`.
62
+ Forced termination, interpreter crashes and `os._exit()` bypass this handler;
63
+ use a guard or `try/finally` for cleanup during normal execution.
64
+ These requests do not override manual sleep, lid closure, or administrator power
65
+ policy.
66
+
67
+ ## Platforms
68
+
69
+ | Platform | Implementation |
70
+ | --- | --- |
71
+ | macOS arm64 / x86_64 | IOKit display-idle assertion, serialized by the Python API |
72
+ | Windows x86_64 / ARM64 | `SetThreadExecutionState` on a dedicated, synchronized worker |
73
+ | Linux desktop | GNOME SessionManager, falling back to `org.freedesktop.ScreenSaver` |
74
+
75
+ On Linux, run inside a logged-in graphical session with
76
+ `DBUS_SESSION_BUS_ADDRESS` set and a supported inhibition service. GNOME requests
77
+ suspend and idle inhibition; the freedesktop fallback depends on the desktop's
78
+ implementation of idle inhibition. Headless servers and minimal compositors may
79
+ not provide either service. An unavailable bus/service returns `False`; omitting
80
+ the required `linux` extra produces a dependency import error.
81
+
82
+ The connection opens on first acquisition and closes on release, including when
83
+ release fails. If the desktop/session service restarts, release and reacquire the
84
+ inhibitor. Actual GNOME/KDE power behavior still requires desktop validation.
85
+
86
+ Native Windows ARM64 wheels are built for Python 3.11–3.14 and 3.13t/3.14t.
87
+ Python 3.8–3.10 remains supported on Windows x86_64. This matches the native ARM64
88
+ interpreters available through uv.
89
+
90
+ ## Migrating from the idempotent API
91
+
92
+ Repeated successful calls now require matching releases. Code that previously
93
+ called `prevent_sleep()` many times but released only once must balance its calls
94
+ or use a guard around each unit of work. Do not manually release an acquisition
95
+ already owned by a guard, or release after a failed acquisition: the counter is
96
+ shared and cannot identify a mismatched manual release.
97
+
98
+ ## Development and publishing
99
+
100
+ ```sh
101
+ uv sync --frozen
102
+ uv run --frozen pytest -q
103
+ uv build
104
+ ```
105
+
106
+ CI checks Python 3.8–3.14 and free-threaded 3.13t/3.14t on Linux, Windows x86_64 and
107
+ both macOS architectures, plus the six Windows ARM64 configurations above,
108
+ whenever changes reach `master` or a pull request targets it.
109
+ Publishing a GitHub Release runs the checks and builds before uploading the
110
+ sdist and native wheels to PyPI through Trusted Publishing.
111
+
112
+ See [development and release instructions](https://github.com/czf0613/keep_awake_py/blob/master/docs/DEVELOPMENT.md),
113
+ [compatibility findings and verification limits](https://github.com/czf0613/keep_awake_py/blob/master/docs/COMPATIBILITY.md), and
114
+ [Codex repository guidance](https://github.com/czf0613/keep_awake_py/blob/master/AGENTS.md).
115
+
116
+ ## License
117
+
118
+ [MIT](https://github.com/czf0613/keep_awake_py/blob/master/LICENSE)
@@ -0,0 +1,11 @@
1
+ #ifndef PM_H
2
+ #define PM_H
3
+
4
+ #include <Python.h>
5
+ #include <stdbool.h>
6
+
7
+ PyObject *pm_prevent_sleep(PyObject *self, PyObject *args);
8
+
9
+ PyObject *pm_allow_sleep(PyObject *self, PyObject *args);
10
+
11
+ #endif // PM_H
@@ -0,0 +1,27 @@
1
+ #include <Python.h>
2
+ #include "pm.h"
3
+
4
+ static PyMethodDef ModMethods[] = {
5
+ {"_prevent_sleep", pm_prevent_sleep, METH_NOARGS, NULL},
6
+ {"_allow_sleep", pm_allow_sleep, METH_NOARGS, NULL},
7
+ {NULL, NULL, 0, NULL}};
8
+
9
+ static struct PyModuleDef module = {
10
+ PyModuleDef_HEAD_INIT,
11
+ "_native_api",
12
+ NULL,
13
+ -1,
14
+ ModMethods};
15
+
16
+ PyMODINIT_FUNC PyInit__native_api(void)
17
+ {
18
+ PyObject *m = PyModule_Create(&module);
19
+ if (m == NULL)
20
+ {
21
+ return NULL;
22
+ }
23
+ #ifdef Py_GIL_DISABLED
24
+ PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
25
+ #endif
26
+ return m;
27
+ }
@@ -0,0 +1,47 @@
1
+ #include "pm.h"
2
+ #include <IOKit/pwr_mgt/IOPMLib.h>
3
+ #include <CoreFoundation/CoreFoundation.h>
4
+
5
+ /* All entry points require the public Python API's lock. */
6
+ static IOPMAssertionID sleepAssertion = kIOPMNullAssertionID;
7
+ #define reasonForActive CFSTR("需要保持系统活动以确保后台任务执行")
8
+
9
+ static bool prevent_sleep(void)
10
+ {
11
+ bool success = true;
12
+ if (sleepAssertion == kIOPMNullAssertionID)
13
+ {
14
+ IOReturn status = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, reasonForActive, &sleepAssertion);
15
+
16
+ success &= (status == kIOReturnSuccess);
17
+ }
18
+
19
+ return success;
20
+ }
21
+
22
+ static void allow_sleep(void)
23
+ {
24
+ if (sleepAssertion != kIOPMNullAssertionID)
25
+ {
26
+ IOPMAssertionRelease(sleepAssertion);
27
+ sleepAssertion = kIOPMNullAssertionID;
28
+ }
29
+ }
30
+
31
+ PyObject *pm_prevent_sleep(PyObject *self, PyObject *args)
32
+ {
33
+ if (prevent_sleep())
34
+ {
35
+ Py_RETURN_TRUE;
36
+ }
37
+ else
38
+ {
39
+ Py_RETURN_FALSE;
40
+ }
41
+ }
42
+
43
+ PyObject *pm_allow_sleep(PyObject *self, PyObject *args)
44
+ {
45
+ allow_sleep();
46
+ Py_RETURN_NONE;
47
+ }
@@ -0,0 +1,99 @@
1
+ #include "pm.h"
2
+ #include <windows.h>
3
+
4
+ /* All entry points require the public Python API's lock.
5
+ * The ready/stop events synchronize communication with the native worker. */
6
+ static HANDLE h_thread = NULL;
7
+ static HANDLE stop_event = NULL;
8
+ static HANDLE ready_event = NULL;
9
+ static bool started = false;
10
+
11
+ static DWORD WINAPI run_forever(LPVOID args)
12
+ {
13
+ started = SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED) != 0;
14
+ SetEvent(ready_event);
15
+ if (started)
16
+ {
17
+ WaitForSingleObject(stop_event, INFINITE);
18
+ SetThreadExecutionState(ES_CONTINUOUS);
19
+ }
20
+ return 0;
21
+ }
22
+
23
+ /* Called with the public Python lock held, after any worker has exited. */
24
+ static void close_handles(void)
25
+ {
26
+ if (h_thread != NULL)
27
+ {
28
+ CloseHandle(h_thread);
29
+ h_thread = NULL;
30
+ }
31
+ if (stop_event != NULL)
32
+ {
33
+ CloseHandle(stop_event);
34
+ stop_event = NULL;
35
+ }
36
+ if (ready_event != NULL)
37
+ {
38
+ CloseHandle(ready_event);
39
+ ready_event = NULL;
40
+ }
41
+ }
42
+
43
+ static bool prevent_sleep(void)
44
+ {
45
+ if (h_thread != NULL)
46
+ {
47
+ return true;
48
+ }
49
+ stop_event = CreateEventW(NULL, TRUE, FALSE, NULL);
50
+ ready_event = CreateEventW(NULL, TRUE, FALSE, NULL);
51
+ if (stop_event == NULL || ready_event == NULL)
52
+ {
53
+ close_handles();
54
+ return false;
55
+ }
56
+ started = false;
57
+ h_thread = CreateThread(NULL, 0, run_forever, NULL, 0, NULL);
58
+ if (h_thread == NULL)
59
+ {
60
+ close_handles();
61
+ return false;
62
+ }
63
+ WaitForSingleObject(ready_event, INFINITE);
64
+ bool success = started;
65
+ if (!success)
66
+ {
67
+ WaitForSingleObject(h_thread, INFINITE);
68
+ close_handles();
69
+ }
70
+ return success;
71
+ }
72
+
73
+ static void allow_sleep(void)
74
+ {
75
+ if (h_thread != NULL)
76
+ {
77
+ SetEvent(stop_event);
78
+ WaitForSingleObject(h_thread, INFINITE);
79
+ close_handles();
80
+ }
81
+ }
82
+
83
+ PyObject *pm_prevent_sleep(PyObject *self, PyObject *args)
84
+ {
85
+ if (prevent_sleep())
86
+ {
87
+ Py_RETURN_TRUE;
88
+ }
89
+ else
90
+ {
91
+ Py_RETURN_FALSE;
92
+ }
93
+ }
94
+
95
+ PyObject *pm_allow_sleep(PyObject *self, PyObject *args)
96
+ {
97
+ allow_sleep();
98
+ Py_RETURN_NONE;
99
+ }
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "keep_awake"
3
+ version = "1.2.0"
4
+ description = "Prevent your computer from going to sleep."
5
+ readme = "README.md"
6
+ keywords = ["sleep", "prevent", "awake", "idle"]
7
+ authors = [{ name = "Kevin Chen", email = "1354016594@qq.com" }]
8
+ dynamic = ["license"]
9
+ requires-python = ">=3.8"
10
+ classifiers = [
11
+ "Intended Audience :: Developers",
12
+ "Operating System :: Microsoft :: Windows",
13
+ "Operating System :: MacOS :: MacOS X",
14
+ "Operating System :: POSIX :: Linux",
15
+ ]
16
+ dependencies = []
17
+ optional-dependencies = { linux = ["jeepney>=0.9.0"] }
18
+
19
+ [project.urls]
20
+ Home-page = "https://github.com/czf0613/keep_awake_py"
21
+
22
+ [build-system]
23
+ requires = ["setuptools>=64", "wheel"]
24
+ build-backend = "setuptools.build_meta"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pytest>=7.0.1",
29
+ "setuptools>=64",
30
+ "jeepney>=0.9.0",
31
+ ]
32
+
33
+ [tool.pytest.ini_options]
34
+ markers = ["desktop: requires a real Linux desktop session (opt in with KEEP_AWAKE_DESKTOP_TEST=1)"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ from setuptools import setup, Extension
2
+ import sys
3
+ import sysconfig
4
+
5
+ c_modules = []
6
+ os_platform = sys.platform
7
+ macros = (
8
+ [("Py_GIL_DISABLED", "1")] if sysconfig.get_config_var("Py_GIL_DISABLED") else []
9
+ )
10
+
11
+ if os_platform == "win32":
12
+ c_modules.append(
13
+ Extension(
14
+ "keep_awake._native_api",
15
+ sources=["native_code/src/pm_windows.c", "native_code/src/ext.c"],
16
+ include_dirs=["native_code/include"],
17
+ extra_compile_args=["/utf-8"],
18
+ define_macros=macros,
19
+ )
20
+ )
21
+ elif os_platform == "darwin":
22
+ c_modules.append(
23
+ Extension(
24
+ "keep_awake._native_api",
25
+ sources=["native_code/src/pm_macos.c", "native_code/src/ext.c"],
26
+ include_dirs=["native_code/include"],
27
+ extra_link_args=["-framework", "CoreFoundation", "-framework", "IOKit"],
28
+ )
29
+ )
30
+ elif os_platform == "linux":
31
+ # nothing todo, native python implementation
32
+ pass
33
+ else:
34
+ raise ValueError("Unsupported platform")
35
+
36
+ # SPDX project.license strings require setuptools versions unavailable on Python 3.8.
37
+ setup(ext_modules=c_modules, license="MIT")
@@ -0,0 +1,109 @@
1
+ import atexit
2
+ import sys
3
+ from threading import Lock, local
4
+ from typing import final
5
+
6
+ __all__ = ["prevent_sleep", "allow_sleep", "KeepAwakeGuard"]
7
+ os_platform = sys.platform
8
+ _mutex = Lock()
9
+ _references = 0
10
+ _shutting_down = False
11
+
12
+
13
+ def prevent_sleep() -> bool:
14
+ """Acquire one sleep-prevention reference; failed requests are not counted."""
15
+ global _references
16
+ with _mutex:
17
+ if _shutting_down:
18
+ return False
19
+ if _references == 0 and not _acquire_backend():
20
+ return False
21
+ _references += 1
22
+ return True
23
+
24
+
25
+ def allow_sleep() -> None:
26
+ """Release one reference, allowing sleep only after the last release."""
27
+ global _references
28
+ with _mutex:
29
+ if _references == 0:
30
+ return
31
+ if _references == 1:
32
+ try:
33
+ _release_backend()
34
+ finally:
35
+ # Backends clean up even if release is interrupted. Never cache
36
+ # success after the underlying inhibitor may have been removed.
37
+ _references = 0
38
+ else:
39
+ _references -= 1
40
+
41
+
42
+ def _shutdown() -> None:
43
+ """Release all outstanding references and prevent new work during exit."""
44
+ global _references, _shutting_down
45
+ with _mutex:
46
+ _shutting_down = True
47
+ try:
48
+ if _references:
49
+ _release_backend()
50
+ finally:
51
+ _references = 0
52
+
53
+
54
+ def _acquire_backend() -> bool:
55
+
56
+ # in macOS and windows, just call the native api
57
+ if os_platform in ["darwin", "win32"]:
58
+ from ._native_api import _prevent_sleep
59
+
60
+ return _prevent_sleep()
61
+ elif os_platform == "linux":
62
+ # in linux, use dbus to send a message to avoid sleep
63
+ from .dbus_api import session_on
64
+
65
+ return session_on()
66
+ else:
67
+ raise NotImplementedError(f"Platform '{os_platform}' is not supported.")
68
+
69
+
70
+ def _release_backend() -> None:
71
+ if os_platform in ["darwin", "win32"]:
72
+ from ._native_api import _allow_sleep
73
+
74
+ _allow_sleep()
75
+ elif os_platform == "linux":
76
+ from .dbus_api import session_off
77
+
78
+ session_off()
79
+ else:
80
+ raise NotImplementedError(f"Platform '{os_platform}' is not supported.")
81
+
82
+
83
+ atexit.register(_shutdown)
84
+
85
+
86
+ @final
87
+ class KeepAwakeGuard:
88
+ """Own a reference for each successful entry, including nested scopes."""
89
+
90
+ def __init__(self):
91
+ # One guard may be nested or shared across threads. Failed entries must
92
+ # never consume another entry's successful acquisition.
93
+ self._state = local()
94
+
95
+ def __enter__(self):
96
+ if not hasattr(self._state, "entries"):
97
+ self._state.entries = []
98
+ entries = self._state.entries
99
+ entries.append(False)
100
+ try:
101
+ entries[-1] = prevent_sleep()
102
+ except BaseException:
103
+ entries.pop()
104
+ raise
105
+
106
+ def __exit__(self, exc_type, exc_value, traceback):
107
+ entries = getattr(self._state, "entries", ())
108
+ if entries and entries.pop():
109
+ allow_sleep()
@@ -0,0 +1,18 @@
1
+ # Native API for keep_awake module
2
+
3
+ def _prevent_sleep() -> bool:
4
+ """Prevent the system from sleeping. Returns True if successful, False otherwise.
5
+ Now the screen will not turn off and system will not go to sleep.
6
+ Requires the public Python API's lock, including on free-threaded CPython.
7
+ Direct calls to this private extension are unsupported and are not synchronized.
8
+ Repeated calls share one process-wide inhibitor; they are not reference counted.
9
+ The public Python API manages reference counting around this private backend.
10
+ """
11
+ pass
12
+
13
+ def _allow_sleep() -> None:
14
+ """Release the inhibitor; requires the public Python API's lock.
15
+
16
+ Sequential repeated releases are safe. Direct calls are unsupported.
17
+ """
18
+ pass
@@ -0,0 +1,112 @@
1
+ """Linux desktop inhibition over a private, lazily opened D-Bus connection."""
2
+
3
+ import logging
4
+ from threading import Lock
5
+ from typing import Optional
6
+
7
+ from jeepney import DBusAddress, DBusErrorResponse, MessageType, new_method_call
8
+ from jeepney.io.blocking import DBusConnection, open_dbus_connection
9
+
10
+ _logger = logging.getLogger(__name__)
11
+ _mutex = Lock()
12
+ _connection: Optional[DBusConnection] = None
13
+ _cookie: Optional[int] = None
14
+ _backend = None
15
+ _timeout = 5.0
16
+ _app_id = "org.python.keep_awake"
17
+ _reason = "Keep system and screen awake"
18
+
19
+ # GNOME flags: suspend (4) | idle (8). Method capitalization differs by API.
20
+ _backends = (
21
+ (
22
+ DBusAddress(
23
+ "/org/gnome/SessionManager",
24
+ "org.gnome.SessionManager",
25
+ "org.gnome.SessionManager",
26
+ ),
27
+ "susu",
28
+ (_app_id, 0, _reason, 12),
29
+ "Uninhibit",
30
+ ),
31
+ (
32
+ DBusAddress(
33
+ "/org/freedesktop/ScreenSaver",
34
+ "org.freedesktop.ScreenSaver",
35
+ "org.freedesktop.ScreenSaver",
36
+ ),
37
+ "ss",
38
+ (_app_id, _reason),
39
+ "UnInhibit",
40
+ ),
41
+ )
42
+
43
+
44
+ def _call(connection, address, method, signature, body):
45
+ message = new_method_call(address, method, signature, body)
46
+ reply = connection.send_and_get_reply(message, timeout=_timeout)
47
+ if reply.header.message_type == MessageType.error:
48
+ raise DBusErrorResponse(reply)
49
+ return reply.body
50
+
51
+
52
+ def _close(connection):
53
+ try:
54
+ connection.close()
55
+ except OSError:
56
+ _logger.debug("Failed to close the D-Bus connection", exc_info=True)
57
+
58
+
59
+ def session_on() -> bool:
60
+ """Acquire one process-wide inhibitor, returning False if unavailable."""
61
+ global _connection, _cookie, _backend
62
+ with _mutex:
63
+ if _cookie is not None:
64
+ return True
65
+ connection = None
66
+ try:
67
+ connection = open_dbus_connection(bus="SESSION")
68
+ for backend in _backends:
69
+ address, signature, body, _ = backend
70
+ try:
71
+ result = _call(connection, address, "Inhibit", signature, body)
72
+ except DBusErrorResponse:
73
+ continue
74
+ if (
75
+ len(result) != 1
76
+ or type(result[0]) is not int
77
+ or not 0 <= result[0] <= 0xFFFFFFFF
78
+ ):
79
+ raise ValueError("Inhibit must return a UInt32 cookie")
80
+ _connection, _cookie, _backend = connection, result[0], backend
81
+ return True
82
+ except (
83
+ DBusErrorResponse,
84
+ OSError,
85
+ EOFError,
86
+ KeyError,
87
+ ValueError,
88
+ RuntimeError,
89
+ StopIteration,
90
+ ) as exc:
91
+ _logger.debug("Unable to inhibit the desktop session: %s", exc)
92
+ finally:
93
+ if connection is not None and connection is not _connection:
94
+ # Also releases an inhibitor whose reply was lost, malformed,
95
+ # or interrupted before ownership transferred to module state.
96
+ _close(connection)
97
+ return False
98
+
99
+
100
+ def session_off() -> None:
101
+ """Release the inhibitor; disconnect even when the service cannot reply."""
102
+ global _connection, _cookie, _backend
103
+ with _mutex:
104
+ if _connection is None:
105
+ return
106
+ try:
107
+ _call(_connection, _backend[0], _backend[3], "u", (_cookie,))
108
+ except (DBusErrorResponse, OSError, EOFError, ValueError) as exc:
109
+ _logger.debug("Unable to release the desktop inhibitor: %s", exc)
110
+ finally:
111
+ _close(_connection)
112
+ _connection, _cookie, _backend = None, None, None
@@ -0,0 +1,2 @@
1
+ # This package contains type annotations.
2
+ # See PEP 561 for more information.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: keep_awake
3
+ Version: 1.2.0
4
+ Summary: Prevent your computer from going to sleep.
5
+ Author-email: Kevin Chen <1354016594@qq.com>
6
+ License: MIT
7
+ Project-URL: Home-page, https://github.com/czf0613/keep_awake_py
8
+ Keywords: sleep,prevent,awake,idle
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: linux
17
+ Requires-Dist: jeepney>=0.9.0; extra == "linux"
18
+ Dynamic: license
19
+ Dynamic: license-file
20
+
21
+ # Keep Awake
22
+
23
+ Prevent idle system sleep and keep the display awake while Python is doing work.
24
+ Supports **CPython 3.8+**, including free-threaded (no-GIL) CPython 3.13/3.14.
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ # Windows / macOS
30
+ pip install keep_awake
31
+
32
+ # Linux desktop
33
+ pip install 'keep_awake[linux]'
34
+ ```
35
+
36
+ Windows and macOS use a C extension. A matching wheel avoids a local compiler;
37
+ otherwise installation builds from source. Linux uses the pure Python
38
+ [Jeepney](https://jeepney.readthedocs.io/en/latest/) D-Bus client and requires no
39
+ project C extension or D-Bus development headers.
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ from keep_awake import prevent_sleep, allow_sleep
45
+
46
+ if not prevent_sleep():
47
+ raise RuntimeError("Could not acquire a sleep inhibitor")
48
+ try:
49
+ # Do long-running work here.
50
+ pass
51
+ finally:
52
+ allow_sleep()
53
+ ```
54
+
55
+ For automatic cleanup:
56
+
57
+ ```python
58
+ from keep_awake import KeepAwakeGuard
59
+
60
+ with KeepAwakeGuard():
61
+ # Do long-running work here.
62
+ pass
63
+ ```
64
+
65
+ - Every successful `prevent_sleep()` adds one reference and returns `True`.
66
+ Failure returns `False` without adding a reference.
67
+ - Pair each successful acquisition with one `allow_sleep()`, which returns `None`.
68
+ The OS inhibitor is released only when the shared reference count reaches zero.
69
+ Calling `allow_sleep()` when the count is already zero does nothing.
70
+ - Calls are synchronized across threads, including no-GIL builds. All callers
71
+ share **one OS inhibitor with a reference count**. If thread A acquires and
72
+ thread B acquires then releases, the count changes `1 → 2 → 1`, so sleep remains
73
+ inhibited until A releases. A release may happen on a different thread.
74
+ - Use the public API only. Calling the private C extension directly bypasses
75
+ synchronization, reference counting and exit cleanup, and is unsupported.
76
+ - Nested and overlapping `KeepAwakeGuard` scopes are supported, including reuse
77
+ of the same guard. Each scope releases only its own successful acquisition.
78
+ Acquisition failure still does not raise; use the boolean-returning API when
79
+ success is required.
80
+ - On normal interpreter shutdown, one shared `atexit` handler releases the backend
81
+ even if several references remain. Acquisitions after this cleanup return `False`.
82
+ Forced termination, interpreter crashes and `os._exit()` bypass this handler;
83
+ use a guard or `try/finally` for cleanup during normal execution.
84
+ These requests do not override manual sleep, lid closure, or administrator power
85
+ policy.
86
+
87
+ ## Platforms
88
+
89
+ | Platform | Implementation |
90
+ | --- | --- |
91
+ | macOS arm64 / x86_64 | IOKit display-idle assertion, serialized by the Python API |
92
+ | Windows x86_64 / ARM64 | `SetThreadExecutionState` on a dedicated, synchronized worker |
93
+ | Linux desktop | GNOME SessionManager, falling back to `org.freedesktop.ScreenSaver` |
94
+
95
+ On Linux, run inside a logged-in graphical session with
96
+ `DBUS_SESSION_BUS_ADDRESS` set and a supported inhibition service. GNOME requests
97
+ suspend and idle inhibition; the freedesktop fallback depends on the desktop's
98
+ implementation of idle inhibition. Headless servers and minimal compositors may
99
+ not provide either service. An unavailable bus/service returns `False`; omitting
100
+ the required `linux` extra produces a dependency import error.
101
+
102
+ The connection opens on first acquisition and closes on release, including when
103
+ release fails. If the desktop/session service restarts, release and reacquire the
104
+ inhibitor. Actual GNOME/KDE power behavior still requires desktop validation.
105
+
106
+ Native Windows ARM64 wheels are built for Python 3.11–3.14 and 3.13t/3.14t.
107
+ Python 3.8–3.10 remains supported on Windows x86_64. This matches the native ARM64
108
+ interpreters available through uv.
109
+
110
+ ## Migrating from the idempotent API
111
+
112
+ Repeated successful calls now require matching releases. Code that previously
113
+ called `prevent_sleep()` many times but released only once must balance its calls
114
+ or use a guard around each unit of work. Do not manually release an acquisition
115
+ already owned by a guard, or release after a failed acquisition: the counter is
116
+ shared and cannot identify a mismatched manual release.
117
+
118
+ ## Development and publishing
119
+
120
+ ```sh
121
+ uv sync --frozen
122
+ uv run --frozen pytest -q
123
+ uv build
124
+ ```
125
+
126
+ CI checks Python 3.8–3.14 and free-threaded 3.13t/3.14t on Linux, Windows x86_64 and
127
+ both macOS architectures, plus the six Windows ARM64 configurations above,
128
+ whenever changes reach `master` or a pull request targets it.
129
+ Publishing a GitHub Release runs the checks and builds before uploading the
130
+ sdist and native wheels to PyPI through Trusted Publishing.
131
+
132
+ See [development and release instructions](https://github.com/czf0613/keep_awake_py/blob/master/docs/DEVELOPMENT.md),
133
+ [compatibility findings and verification limits](https://github.com/czf0613/keep_awake_py/blob/master/docs/COMPATIBILITY.md), and
134
+ [Codex repository guidance](https://github.com/czf0613/keep_awake_py/blob/master/AGENTS.md).
135
+
136
+ ## License
137
+
138
+ [MIT](https://github.com/czf0613/keep_awake_py/blob/master/LICENSE)
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ native_code/include/pm.h
7
+ native_code/src/ext.c
8
+ native_code/src/pm_macos.c
9
+ native_code/src/pm_windows.c
10
+ src/keep_awake/__init__.py
11
+ src/keep_awake/_native_api.pyi
12
+ src/keep_awake/dbus_api.py
13
+ src/keep_awake/py.typed
14
+ src/keep_awake.egg-info/PKG-INFO
15
+ src/keep_awake.egg-info/SOURCES.txt
16
+ src/keep_awake.egg-info/dependency_links.txt
17
+ src/keep_awake.egg-info/requires.txt
18
+ src/keep_awake.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [linux]
3
+ jeepney>=0.9.0
@@ -0,0 +1 @@
1
+ keep_awake