pychrony 2.0.3__tar.gz → 3.0.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.
- pychrony-3.0.2/MANIFEST.in +22 -0
- {pychrony-2.0.3/src/pychrony.egg-info → pychrony-3.0.2}/PKG-INFO +59 -12
- {pychrony-2.0.3 → pychrony-3.0.2}/README.md +51 -8
- pychrony-3.0.2/pyproject.toml +164 -0
- pychrony-3.0.2/setup.py +93 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/__init__.py +29 -2
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/__init__.py +1 -1
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/_bindings.py +286 -95
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/_build_bindings.py +23 -14
- pychrony-3.0.2/src/pychrony/_core/_cffi_bindings.pyi +86 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/models.py +6 -0
- pychrony-3.0.2/src/pychrony/pytest_plugin.py +56 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/testing.py +8 -33
- {pychrony-2.0.3 → pychrony-3.0.2/src/pychrony.egg-info}/PKG-INFO +59 -12
- pychrony-3.0.2/src/pychrony.egg-info/SOURCES.txt +60 -0
- pychrony-3.0.2/src/pychrony.egg-info/requires.txt +15 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony.egg-info/scm_file_list.json +8 -0
- pychrony-3.0.2/src/pychrony.egg-info/scm_version.json +8 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/conftest.py +3 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/contract/test_api.py +10 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/conftest.py +4 -2
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_connection.py +45 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/config.py +14 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/context.py +1 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/session.py +68 -14
- pychrony-3.0.2/tests/unit/test_cffi_stub.py +66 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_connection.py +55 -52
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_enums.py +9 -9
- pychrony-3.0.2/tests/unit/test_free_threading.py +34 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_mock_scenarios.py +4 -1
- pychrony-3.0.2/tests/unit/test_request_timeout.py +222 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_rtc_scenarios.py +64 -1
- pychrony-3.0.2/tests/unit/test_setup.py +111 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_testing.py +87 -1
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_validation.py +19 -0
- pychrony-2.0.3/.claude/commands/speckit.analyze.md +0 -184
- pychrony-2.0.3/.claude/commands/speckit.checklist.md +0 -294
- pychrony-2.0.3/.claude/commands/speckit.clarify.md +0 -181
- pychrony-2.0.3/.claude/commands/speckit.constitution.md +0 -82
- pychrony-2.0.3/.claude/commands/speckit.implement.md +0 -135
- pychrony-2.0.3/.claude/commands/speckit.plan.md +0 -89
- pychrony-2.0.3/.claude/commands/speckit.specify.md +0 -258
- pychrony-2.0.3/.claude/commands/speckit.tasks.md +0 -137
- pychrony-2.0.3/.claude/commands/speckit.taskstoissues.md +0 -30
- pychrony-2.0.3/.dockerignore +0 -48
- pychrony-2.0.3/.github/dependabot.yml +0 -38
- pychrony-2.0.3/.github/workflows/ci.yml +0 -152
- pychrony-2.0.3/.github/workflows/docs.yml +0 -62
- pychrony-2.0.3/.github/workflows/release.yml +0 -181
- pychrony-2.0.3/.gitignore +0 -215
- pychrony-2.0.3/.gitmodules +0 -4
- pychrony-2.0.3/.python-version +0 -1
- pychrony-2.0.3/.specify/memory/constitution.md +0 -42
- pychrony-2.0.3/.specify/scripts/bash/check-prerequisites.sh +0 -166
- pychrony-2.0.3/.specify/scripts/bash/common.sh +0 -156
- pychrony-2.0.3/.specify/scripts/bash/create-new-feature.sh +0 -297
- pychrony-2.0.3/.specify/scripts/bash/execute-task-generation.sh +0 -255
- pychrony-2.0.3/.specify/scripts/bash/setup-plan.sh +0 -61
- pychrony-2.0.3/.specify/scripts/bash/update-agent-context.sh +0 -799
- pychrony-2.0.3/.specify/templates/agent-file-template.md +0 -28
- pychrony-2.0.3/.specify/templates/checklist-template.md +0 -40
- pychrony-2.0.3/.specify/templates/plan-template.md +0 -112
- pychrony-2.0.3/.specify/templates/spec-template.md +0 -118
- pychrony-2.0.3/.specify/templates/tasks-template.md +0 -254
- pychrony-2.0.3/CLAUDE.md +0 -90
- pychrony-2.0.3/TESTING.md +0 -466
- pychrony-2.0.3/docker/Dockerfile.test +0 -94
- pychrony-2.0.3/docker/docker-compose.test.yml +0 -67
- pychrony-2.0.3/docs/api/index.md +0 -85
- pychrony-2.0.3/docs/index.md +0 -208
- pychrony-2.0.3/mkdocs.yml +0 -52
- pychrony-2.0.3/pyproject.toml +0 -121
- pychrony-2.0.3/setup.py +0 -23
- pychrony-2.0.3/specs/001-python-scaffold/checklists/requirements.md +0 -34
- pychrony-2.0.3/specs/001-python-scaffold/contracts/package.md +0 -137
- pychrony-2.0.3/specs/001-python-scaffold/data-model.md +0 -74
- pychrony-2.0.3/specs/001-python-scaffold/plan.md +0 -109
- pychrony-2.0.3/specs/001-python-scaffold/quickstart.md +0 -164
- pychrony-2.0.3/specs/001-python-scaffold/research.md +0 -159
- pychrony-2.0.3/specs/001-python-scaffold/spec.md +0 -167
- pychrony-2.0.3/specs/001-python-scaffold/tasks.md +0 -217
- pychrony-2.0.3/specs/002-tracking-binding/checklists/requirements.md +0 -34
- pychrony-2.0.3/specs/002-tracking-binding/contracts/python-api.md +0 -292
- pychrony-2.0.3/specs/002-tracking-binding/data-model.md +0 -317
- pychrony-2.0.3/specs/002-tracking-binding/plan.md +0 -146
- pychrony-2.0.3/specs/002-tracking-binding/quickstart.md +0 -262
- pychrony-2.0.3/specs/002-tracking-binding/research.md +0 -324
- pychrony-2.0.3/specs/002-tracking-binding/spec.md +0 -112
- pychrony-2.0.3/specs/002-tracking-binding/tasks.md +0 -220
- pychrony-2.0.3/specs/003-multiple-reports-bindings/checklists/requirements.md +0 -37
- pychrony-2.0.3/specs/003-multiple-reports-bindings/contracts/api.md +0 -215
- pychrony-2.0.3/specs/003-multiple-reports-bindings/contracts/python-api.md +0 -281
- pychrony-2.0.3/specs/003-multiple-reports-bindings/data-model.md +0 -289
- pychrony-2.0.3/specs/003-multiple-reports-bindings/plan.md +0 -125
- pychrony-2.0.3/specs/003-multiple-reports-bindings/quickstart.md +0 -220
- pychrony-2.0.3/specs/003-multiple-reports-bindings/research.md +0 -255
- pychrony-2.0.3/specs/003-multiple-reports-bindings/spec.md +0 -147
- pychrony-2.0.3/specs/003-multiple-reports-bindings/tasks.md +0 -271
- pychrony-2.0.3/specs/004-categorical-enums/checklists/requirements.md +0 -36
- pychrony-2.0.3/specs/004-categorical-enums/data-model.md +0 -206
- pychrony-2.0.3/specs/004-categorical-enums/plan.md +0 -85
- pychrony-2.0.3/specs/004-categorical-enums/quickstart.md +0 -142
- pychrony-2.0.3/specs/004-categorical-enums/research.md +0 -152
- pychrony-2.0.3/specs/004-categorical-enums/spec.md +0 -104
- pychrony-2.0.3/specs/004-categorical-enums/tasks.md +0 -220
- pychrony-2.0.3/specs/005-pypi-packaging/checklists/requirements.md +0 -37
- pychrony-2.0.3/specs/005-pypi-packaging/plan.md +0 -83
- pychrony-2.0.3/specs/005-pypi-packaging/quickstart.md +0 -165
- pychrony-2.0.3/specs/005-pypi-packaging/research.md +0 -191
- pychrony-2.0.3/specs/005-pypi-packaging/spec.md +0 -146
- pychrony-2.0.3/specs/005-pypi-packaging/tasks.md +0 -213
- pychrony-2.0.3/specs/006-protocol-test-mocks/analysis-report.md +0 -197
- pychrony-2.0.3/specs/006-protocol-test-mocks/checklists/requirements.md +0 -50
- pychrony-2.0.3/specs/006-protocol-test-mocks/contracts/mock-api.md +0 -384
- pychrony-2.0.3/specs/006-protocol-test-mocks/data-model.md +0 -434
- pychrony-2.0.3/specs/006-protocol-test-mocks/plan.md +0 -100
- pychrony-2.0.3/specs/006-protocol-test-mocks/quickstart.md +0 -470
- pychrony-2.0.3/specs/006-protocol-test-mocks/research.md +0 -379
- pychrony-2.0.3/specs/006-protocol-test-mocks/spec.md +0 -176
- pychrony-2.0.3/specs/006-protocol-test-mocks/tasks.md +0 -370
- pychrony-2.0.3/specs/007-api-docs-github-pages/tasks.md +0 -222
- pychrony-2.0.3/src/pychrony.egg-info/SOURCES.txt +0 -139
- pychrony-2.0.3/src/pychrony.egg-info/entry_points.txt +0 -2
- pychrony-2.0.3/src/pychrony.egg-info/requires.txt +0 -1
- pychrony-2.0.3/src/pychrony.egg-info/scm_version.json +0 -8
- pychrony-2.0.3/uv.lock +0 -1100
- {pychrony-2.0.3 → pychrony-3.0.2}/LICENSE +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/LICENSES/LGPL-2.1-or-later.txt +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/setup.cfg +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/_fields.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/exceptions.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/py.typed +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony.egg-info/dependency_links.txt +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony.egg-info/top_level.txt +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/__init__.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/contract/__init__.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/contract/test_api_stability.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/__init__.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_enums.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_rtcdata.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_sources.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_sourcestats.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_tracking.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/__init__.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/scenarios.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/test_import.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/__init__.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_exceptions.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_leap_scenarios.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_mock_config.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_models.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_source_list_changes.py +0 -0
- {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_source_scenarios.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# setuptools-scm adds every git-tracked file to the sdist. Strip the
|
|
2
|
+
# development tooling so the sdist carries only what a downstream packager
|
|
3
|
+
# needs: src/, tests/, setup.py, pyproject.toml, README.md, LICENSE, LICENSES/.
|
|
4
|
+
|
|
5
|
+
prune .claude
|
|
6
|
+
prune .github
|
|
7
|
+
prune .specify
|
|
8
|
+
prune docker
|
|
9
|
+
prune docs
|
|
10
|
+
prune specs
|
|
11
|
+
exclude .dockerignore .gitignore .gitmodules .python-version
|
|
12
|
+
exclude CLAUDE.md TESTING.md mkdocs.yml uv.lock
|
|
13
|
+
|
|
14
|
+
# vendor/libchrony is deliberately left out. The release workflow's
|
|
15
|
+
# build-sdist job checks out without submodules, so the published sdist never
|
|
16
|
+
# had it; pruning it here keeps local builds (where the submodule may be
|
|
17
|
+
# populated) identical to the released one. Building from the sdist links
|
|
18
|
+
# against the system libchrony, as distro packagers expect, and leaving the
|
|
19
|
+
# LGPL source out keeps the sdist itself MIT code plus license texts. The
|
|
20
|
+
# LGPL source offer for the libchrony bundled into the binary wheels is met
|
|
21
|
+
# by the pinned submodule commit in the public git repository.
|
|
22
|
+
prune vendor
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pychrony
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: Python bindings for chrony NTP client
|
|
5
5
|
Author-email: arunderwood <arunderwood@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT AND LGPL-2.1-or-later
|
|
@@ -11,20 +11,24 @@ Project-URL: Issues, https://github.com/arunderwood/pychrony/issues
|
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: System Administrators
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
|
20
20
|
Classifier: Topic :: System :: Networking :: Time Synchronization
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Operating System :: POSIX :: Linux
|
|
23
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
License-File: LICENSES/LGPL-2.1-or-later.txt
|
|
27
|
-
Requires-Dist: cffi>=1.
|
|
27
|
+
Requires-Dist: cffi>=1.15.1; python_version == "3.11"
|
|
28
|
+
Requires-Dist: cffi>=1.16.0; python_version == "3.12"
|
|
29
|
+
Requires-Dist: cffi>=1.17.0; python_version == "3.13"
|
|
30
|
+
Requires-Dist: cffi>=2.0.0; python_version == "3.14"
|
|
31
|
+
Requires-Dist: cffi>=2.1.0; python_version >= "3.15"
|
|
28
32
|
Dynamic: license-file
|
|
29
33
|
|
|
30
34
|
# PyChrony: Python bindings for chrony NTP client
|
|
@@ -56,7 +60,18 @@ Pre-built wheels include libchrony—no system dependencies needed.
|
|
|
56
60
|
<details>
|
|
57
61
|
<summary><strong>Building from Source</strong></summary>
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
Installing from source (the sdist, or any platform without a pre-built wheel)
|
|
64
|
+
compiles the CFFI bindings, so it needs a C compiler, the libffi headers, and
|
|
65
|
+
[libchrony](https://gitlab.com/chrony/libchrony) with its headers. libchrony is
|
|
66
|
+
found through `pkg-config`; for a non-standard prefix, add
|
|
67
|
+
`<prefix>/lib/pkgconfig` to `PKG_CONFIG_PATH`. A missing `chrony.h` or library
|
|
68
|
+
stops the install with an error naming it.
|
|
69
|
+
|
|
70
|
+
To build from source even where a wheel exists:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install --no-binary pychrony pychrony
|
|
74
|
+
```
|
|
60
75
|
|
|
61
76
|
#### libchrony Package Availability
|
|
62
77
|
|
|
@@ -73,7 +88,7 @@ Source installs (sdist or development) require [libchrony](https://gitlab.com/ch
|
|
|
73
88
|
```bash
|
|
74
89
|
# Install build dependencies
|
|
75
90
|
# Debian/Ubuntu:
|
|
76
|
-
sudo apt-get install build-essential libtool libffi-dev
|
|
91
|
+
sudo apt-get install build-essential libtool-bin libffi-dev python3-dev
|
|
77
92
|
|
|
78
93
|
# Alpine:
|
|
79
94
|
sudo apk add gcc make libtool libffi-dev musl-dev
|
|
@@ -96,9 +111,18 @@ git clone https://github.com/arunderwood/pychrony.git
|
|
|
96
111
|
cd pychrony
|
|
97
112
|
git submodule update --init
|
|
98
113
|
uv sync --all-groups
|
|
99
|
-
uv pip install -e .
|
|
100
114
|
```
|
|
101
115
|
|
|
116
|
+
`uv sync` and `uv pip install` from the checkout install pychrony **without**
|
|
117
|
+
the compiled bindings: `[tool.uv]` in `pyproject.toml` sets
|
|
118
|
+
`PYCHRONY_SKIP_CFFI=1`, so development and the unit and contract tests need no
|
|
119
|
+
libchrony. Connecting to chronyd from such an install raises
|
|
120
|
+
`ChronyLibraryError`. Integration tests build the bindings in Docker (see
|
|
121
|
+
[TESTING.md](TESTING.md)).
|
|
122
|
+
|
|
123
|
+
With libchrony installed, `PYCHRONY_BUILD_CFFI=1 uv sync` or `pip install -e .`
|
|
124
|
+
builds the bindings. With other installers, `PYCHRONY_SKIP_CFFI=1` skips them.
|
|
125
|
+
|
|
102
126
|
</details>
|
|
103
127
|
|
|
104
128
|
## Usage
|
|
@@ -121,13 +145,12 @@ command port, using the first that connects — the same candidates
|
|
|
121
145
|
The two are not equivalent in privilege. The Unix socket is chronyd's control
|
|
122
146
|
channel, restricted to the root or `chrony` user; the command port serves
|
|
123
147
|
monitoring commands only, which is everything pychrony reads. Read-only
|
|
124
|
-
consumers should prefer the command port and can
|
|
148
|
+
consumers should prefer the command port, and can ask for it explicitly:
|
|
125
149
|
|
|
126
150
|
```python
|
|
127
|
-
from pychrony import ChronyConnection
|
|
151
|
+
from pychrony import ChronyConnection
|
|
128
152
|
|
|
129
|
-
with ChronyConnection() as conn:
|
|
130
|
-
assert conn.transport is Transport.COMMAND_PORT
|
|
153
|
+
with ChronyConnection("127.0.0.1") as conn:
|
|
131
154
|
status = conn.get_tracking()
|
|
132
155
|
```
|
|
133
156
|
|
|
@@ -135,9 +158,33 @@ Note that joining the `chrony` group does **not** grant access to the Unix
|
|
|
135
158
|
socket. See [Choosing a Transport](https://pychrony.org/#choosing-a-transport)
|
|
136
159
|
for details.
|
|
137
160
|
|
|
161
|
+
## Testing Your Code
|
|
162
|
+
|
|
163
|
+
`pychrony.testing` has factories that build pychrony objects with realistic
|
|
164
|
+
defaults. They work in any test framework and do not need pytest:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pychrony.testing import make_tracking
|
|
168
|
+
|
|
169
|
+
status = make_tracking(stratum=16, reference_id=0)
|
|
170
|
+
assert not status.is_synchronized()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
pytest users can enable fixtures (`chrony_tracking_status`, `chrony_source`,
|
|
174
|
+
`chrony_source_stats`, `chrony_rtc_data`) in their top-level `conftest.py`:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
pytest_plugins = ["pychrony.pytest_plugin"]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The plugin is not loaded automatically, so installing pychrony adds no
|
|
181
|
+
fixtures to other projects' test runs. See
|
|
182
|
+
[TESTING.md](https://github.com/arunderwood/pychrony/blob/main/TESTING.md#pytest-fixtures)
|
|
183
|
+
for details.
|
|
184
|
+
|
|
138
185
|
## Compatibility
|
|
139
186
|
|
|
140
|
-
- **Python**: 3.
|
|
187
|
+
- **Python**: 3.11, 3.12, 3.13, 3.14, 3.15
|
|
141
188
|
- **Platform**: Linux (primary), other platforms where libchrony is available
|
|
142
189
|
- **chrony**: 4.x and later
|
|
143
190
|
|
|
@@ -27,7 +27,18 @@ Pre-built wheels include libchrony—no system dependencies needed.
|
|
|
27
27
|
<details>
|
|
28
28
|
<summary><strong>Building from Source</strong></summary>
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Installing from source (the sdist, or any platform without a pre-built wheel)
|
|
31
|
+
compiles the CFFI bindings, so it needs a C compiler, the libffi headers, and
|
|
32
|
+
[libchrony](https://gitlab.com/chrony/libchrony) with its headers. libchrony is
|
|
33
|
+
found through `pkg-config`; for a non-standard prefix, add
|
|
34
|
+
`<prefix>/lib/pkgconfig` to `PKG_CONFIG_PATH`. A missing `chrony.h` or library
|
|
35
|
+
stops the install with an error naming it.
|
|
36
|
+
|
|
37
|
+
To build from source even where a wheel exists:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install --no-binary pychrony pychrony
|
|
41
|
+
```
|
|
31
42
|
|
|
32
43
|
#### libchrony Package Availability
|
|
33
44
|
|
|
@@ -44,7 +55,7 @@ Source installs (sdist or development) require [libchrony](https://gitlab.com/ch
|
|
|
44
55
|
```bash
|
|
45
56
|
# Install build dependencies
|
|
46
57
|
# Debian/Ubuntu:
|
|
47
|
-
sudo apt-get install build-essential libtool libffi-dev
|
|
58
|
+
sudo apt-get install build-essential libtool-bin libffi-dev python3-dev
|
|
48
59
|
|
|
49
60
|
# Alpine:
|
|
50
61
|
sudo apk add gcc make libtool libffi-dev musl-dev
|
|
@@ -67,9 +78,18 @@ git clone https://github.com/arunderwood/pychrony.git
|
|
|
67
78
|
cd pychrony
|
|
68
79
|
git submodule update --init
|
|
69
80
|
uv sync --all-groups
|
|
70
|
-
uv pip install -e .
|
|
71
81
|
```
|
|
72
82
|
|
|
83
|
+
`uv sync` and `uv pip install` from the checkout install pychrony **without**
|
|
84
|
+
the compiled bindings: `[tool.uv]` in `pyproject.toml` sets
|
|
85
|
+
`PYCHRONY_SKIP_CFFI=1`, so development and the unit and contract tests need no
|
|
86
|
+
libchrony. Connecting to chronyd from such an install raises
|
|
87
|
+
`ChronyLibraryError`. Integration tests build the bindings in Docker (see
|
|
88
|
+
[TESTING.md](TESTING.md)).
|
|
89
|
+
|
|
90
|
+
With libchrony installed, `PYCHRONY_BUILD_CFFI=1 uv sync` or `pip install -e .`
|
|
91
|
+
builds the bindings. With other installers, `PYCHRONY_SKIP_CFFI=1` skips them.
|
|
92
|
+
|
|
73
93
|
</details>
|
|
74
94
|
|
|
75
95
|
## Usage
|
|
@@ -92,13 +112,12 @@ command port, using the first that connects — the same candidates
|
|
|
92
112
|
The two are not equivalent in privilege. The Unix socket is chronyd's control
|
|
93
113
|
channel, restricted to the root or `chrony` user; the command port serves
|
|
94
114
|
monitoring commands only, which is everything pychrony reads. Read-only
|
|
95
|
-
consumers should prefer the command port and can
|
|
115
|
+
consumers should prefer the command port, and can ask for it explicitly:
|
|
96
116
|
|
|
97
117
|
```python
|
|
98
|
-
from pychrony import ChronyConnection
|
|
118
|
+
from pychrony import ChronyConnection
|
|
99
119
|
|
|
100
|
-
with ChronyConnection() as conn:
|
|
101
|
-
assert conn.transport is Transport.COMMAND_PORT
|
|
120
|
+
with ChronyConnection("127.0.0.1") as conn:
|
|
102
121
|
status = conn.get_tracking()
|
|
103
122
|
```
|
|
104
123
|
|
|
@@ -106,9 +125,33 @@ Note that joining the `chrony` group does **not** grant access to the Unix
|
|
|
106
125
|
socket. See [Choosing a Transport](https://pychrony.org/#choosing-a-transport)
|
|
107
126
|
for details.
|
|
108
127
|
|
|
128
|
+
## Testing Your Code
|
|
129
|
+
|
|
130
|
+
`pychrony.testing` has factories that build pychrony objects with realistic
|
|
131
|
+
defaults. They work in any test framework and do not need pytest:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pychrony.testing import make_tracking
|
|
135
|
+
|
|
136
|
+
status = make_tracking(stratum=16, reference_id=0)
|
|
137
|
+
assert not status.is_synchronized()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
pytest users can enable fixtures (`chrony_tracking_status`, `chrony_source`,
|
|
141
|
+
`chrony_source_stats`, `chrony_rtc_data`) in their top-level `conftest.py`:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
pytest_plugins = ["pychrony.pytest_plugin"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The plugin is not loaded automatically, so installing pychrony adds no
|
|
148
|
+
fixtures to other projects' test runs. See
|
|
149
|
+
[TESTING.md](https://github.com/arunderwood/pychrony/blob/main/TESTING.md#pytest-fixtures)
|
|
150
|
+
for details.
|
|
151
|
+
|
|
109
152
|
## Compatibility
|
|
110
153
|
|
|
111
|
-
- **Python**: 3.
|
|
154
|
+
- **Python**: 3.11, 3.12, 3.13, 3.14, 3.15
|
|
112
155
|
- **Platform**: Linux (primary), other platforms where libchrony is available
|
|
113
156
|
- **chrony**: 4.x and later
|
|
114
157
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# PEP 639 license expressions + license-files need setuptools 77; the first
|
|
3
|
+
# 77.x on PyPI is 77.0.1. cffi floors are the first release supporting each
|
|
4
|
+
# Python (keep in sync with [project] dependencies).
|
|
5
|
+
requires = [
|
|
6
|
+
"setuptools>=77.0.1",
|
|
7
|
+
"setuptools-scm>=8.0",
|
|
8
|
+
"cffi>=1.15.1; python_version == '3.11'",
|
|
9
|
+
"cffi>=1.16.0; python_version == '3.12'",
|
|
10
|
+
"cffi>=1.17.0; python_version == '3.13'",
|
|
11
|
+
"cffi>=2.0.0; python_version == '3.14'",
|
|
12
|
+
"cffi>=2.1.0; python_version >= '3.15'",
|
|
13
|
+
]
|
|
14
|
+
build-backend = "setuptools.build_meta"
|
|
15
|
+
|
|
16
|
+
[project]
|
|
17
|
+
name = "pychrony"
|
|
18
|
+
dynamic = ["version"]
|
|
19
|
+
description = "Python bindings for chrony NTP client"
|
|
20
|
+
readme = "README.md"
|
|
21
|
+
license = "MIT AND LGPL-2.1-or-later"
|
|
22
|
+
license-files = ["LICENSE", "LICENSES/*"]
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
authors = [
|
|
25
|
+
{name = "arunderwood", email = "arunderwood@users.noreply.github.com"}
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 5 - Production/Stable",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Intended Audience :: System Administrators",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Programming Language :: Python :: 3.14",
|
|
35
|
+
"Programming Language :: Python :: 3.15",
|
|
36
|
+
"Programming Language :: Python :: Free Threading :: 3 - Stable",
|
|
37
|
+
"Topic :: System :: Networking :: Time Synchronization",
|
|
38
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
39
|
+
"Operating System :: POSIX :: Linux",
|
|
40
|
+
]
|
|
41
|
+
dependencies = [
|
|
42
|
+
# First cffi release supporting each Python version.
|
|
43
|
+
"cffi>=1.15.1; python_version == '3.11'",
|
|
44
|
+
"cffi>=1.16.0; python_version == '3.12'",
|
|
45
|
+
"cffi>=1.17.0; python_version == '3.13'",
|
|
46
|
+
"cffi>=2.0.0; python_version == '3.14'",
|
|
47
|
+
"cffi>=2.1.0; python_version >= '3.15'",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/arunderwood/pychrony"
|
|
52
|
+
Documentation = "https://pychrony.org/"
|
|
53
|
+
Repository = "https://github.com/arunderwood/pychrony.git"
|
|
54
|
+
Issues = "https://github.com/arunderwood/pychrony/issues"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools_scm]
|
|
57
|
+
fallback_version = "0.0.0.dev0"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
|
|
62
|
+
# _cffi_bindings.pyi types the CFFI extension, which only exists once built.
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
pychrony = ["py.typed", "_core/*.pyi"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 88
|
|
68
|
+
target-version = "py311"
|
|
69
|
+
# specs/ holds frozen spec-kit design documents; they are historical records and
|
|
70
|
+
# are not kept in sync with the code, so exclude them from linting/formatting.
|
|
71
|
+
extend-exclude = ["specs/"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint]
|
|
74
|
+
extend-select = [
|
|
75
|
+
"B", # flake8-bugbear
|
|
76
|
+
"BLE", # flake8-blind-except
|
|
77
|
+
"C4", # flake8-comprehensions
|
|
78
|
+
"I", # isort
|
|
79
|
+
"PERF", # perflint
|
|
80
|
+
"PIE", # flake8-pie
|
|
81
|
+
"PT", # flake8-pytest-style
|
|
82
|
+
"RET", # flake8-return
|
|
83
|
+
"RUF", # ruff-specific rules
|
|
84
|
+
"SIM", # flake8-simplify
|
|
85
|
+
"UP", # pyupgrade
|
|
86
|
+
]
|
|
87
|
+
ignore = [
|
|
88
|
+
# __all__ entries are grouped by category (enums, models, exceptions) with
|
|
89
|
+
# explanatory comments; alphabetical sorting would destroy that structure.
|
|
90
|
+
"RUF022",
|
|
91
|
+
# try/except inside a loop is free on 3.11+ (zero-cost exceptions), which is
|
|
92
|
+
# our minimum, and the source-list retry loop needs a handler per attempt.
|
|
93
|
+
"PERF203",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
testpaths = ["tests"]
|
|
98
|
+
python_files = ["test_*.py"]
|
|
99
|
+
python_classes = ["Test*"]
|
|
100
|
+
python_functions = ["test_*"]
|
|
101
|
+
addopts = ["-v", "--tb=short"]
|
|
102
|
+
|
|
103
|
+
[tool.uv]
|
|
104
|
+
# The first uv with extra-build-variables; older uv rejects this file.
|
|
105
|
+
required-version = ">=0.8.6"
|
|
106
|
+
# Lets development and the CI test/lint/docs jobs run without libchrony; unit
|
|
107
|
+
# and contract tests mock the bindings. Applies to uv installs of the project
|
|
108
|
+
# only, not `uv build`, pip or cibuildwheel. PYCHRONY_BUILD_CFFI=1 overrides it.
|
|
109
|
+
extra-build-variables = { pychrony = { PYCHRONY_SKIP_CFFI = "1" } }
|
|
110
|
+
# Replaces uv's default (pyproject.toml, setup.py, setup.cfg), so those are
|
|
111
|
+
# listed too.
|
|
112
|
+
cache-keys = [
|
|
113
|
+
{ file = "pyproject.toml" },
|
|
114
|
+
{ file = "setup.py" },
|
|
115
|
+
{ file = "src/pychrony/_core/_build_bindings.py" },
|
|
116
|
+
# setuptools-scm takes the version from git.
|
|
117
|
+
{ git = { commit = true, tags = true } },
|
|
118
|
+
{ env = "PYCHRONY_BUILD_CFFI" },
|
|
119
|
+
{ env = "PYCHRONY_SKIP_CFFI" },
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[tool.cibuildwheel]
|
|
123
|
+
build-frontend = "build[uv]"
|
|
124
|
+
build = "cp311-* cp312-* cp313-* cp314-* cp314t-* cp315-* cp315t-*"
|
|
125
|
+
|
|
126
|
+
[tool.cibuildwheel.linux]
|
|
127
|
+
archs = ["x86_64", "aarch64"]
|
|
128
|
+
environment = { CIBUILDWHEEL = "1" }
|
|
129
|
+
test-requires = "pytest"
|
|
130
|
+
# The unit and contract tests mock the C library, so check the compiled bindings
|
|
131
|
+
# directly: a wheel missing _cffi_bindings*.so would otherwise pass.
|
|
132
|
+
test-command = [
|
|
133
|
+
"python -c 'import pychrony._core._cffi_bindings, pychrony._core._bindings as b; assert b._LIBRARY_AVAILABLE'",
|
|
134
|
+
"python -m pytest {project}/tests/unit {project}/tests/contract -v",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
[[tool.cibuildwheel.overrides]]
|
|
138
|
+
select = "*-manylinux_*"
|
|
139
|
+
before-all = """
|
|
140
|
+
dnf install -y gcc make libtool libffi-devel && \
|
|
141
|
+
cd vendor/libchrony && make && make install prefix=/usr && ldconfig
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
[[tool.cibuildwheel.overrides]]
|
|
145
|
+
select = "*-musllinux_*"
|
|
146
|
+
before-all = """
|
|
147
|
+
apk add --no-cache gcc make libtool libffi-dev musl-dev && \
|
|
148
|
+
cd vendor/libchrony && make && make install prefix=/usr
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
[dependency-groups]
|
|
152
|
+
dev = [
|
|
153
|
+
"pytest>=9.0.2",
|
|
154
|
+
"pytest-cov>=7.0.0",
|
|
155
|
+
"pyyaml>=6.0.3",
|
|
156
|
+
"ruff>=0.14.11",
|
|
157
|
+
"setuptools>=77.0.1",
|
|
158
|
+
"ty>=0.0.1",
|
|
159
|
+
]
|
|
160
|
+
docs = [
|
|
161
|
+
"mkdocs-material>=9.0.0",
|
|
162
|
+
"mkdocstrings[python]>=0.24.0",
|
|
163
|
+
"mike>=2.0.0",
|
|
164
|
+
]
|
pychrony-3.0.2/setup.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Compiling by default means a source install either works or fails at build
|
|
2
|
+
time, never yielding a package that cannot reach chronyd.
|
|
3
|
+
|
|
4
|
+
PYCHRONY_SKIP_CFFI=1 is for development without libchrony; ``[tool.uv]`` in
|
|
5
|
+
pyproject.toml sets it for uv installs of the project. Such an install imports,
|
|
6
|
+
but connecting raises ChronyLibraryError. cibuildwheel always compiles because a
|
|
7
|
+
wheel without bindings is broken.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
from setuptools import setup
|
|
14
|
+
from setuptools.command.build_ext import build_ext
|
|
15
|
+
from setuptools.errors import CompileError, LinkError, PlatformError
|
|
16
|
+
|
|
17
|
+
CFFI_EXTENSION = "pychrony._core._cffi_bindings"
|
|
18
|
+
|
|
19
|
+
LIBCHRONY_HELP = """\
|
|
20
|
+
pychrony needs libchrony to compile its CFFI bindings.
|
|
21
|
+
Install libchrony and a C compiler, then retry:
|
|
22
|
+
|
|
23
|
+
Fedora 42+: dnf install libchrony-devel gcc
|
|
24
|
+
RHEL 9: dnf install epel-release
|
|
25
|
+
dnf install libchrony-devel gcc
|
|
26
|
+
Others: build https://gitlab.com/chrony/libchrony
|
|
27
|
+
make && make install prefix=/usr && ldconfig
|
|
28
|
+
|
|
29
|
+
For a non-standard prefix, add <prefix>/lib/pkgconfig to
|
|
30
|
+
PKG_CONFIG_PATH (or set CFLAGS=-I<prefix>/include and
|
|
31
|
+
LDFLAGS=-L<prefix>/lib).
|
|
32
|
+
|
|
33
|
+
To install without the bindings (development only:
|
|
34
|
+
pychrony cannot query chronyd), set PYCHRONY_SKIP_CFFI=1."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class build_ext_with_libchrony_check(build_ext):
|
|
38
|
+
"""Without this, a missing libchrony surfaces as a raw compiler or linker
|
|
39
|
+
error from the CFFI-generated source."""
|
|
40
|
+
|
|
41
|
+
def build_extension(self, ext):
|
|
42
|
+
if ext.name == CFFI_EXTENSION:
|
|
43
|
+
self._check_libchrony(ext)
|
|
44
|
+
super().build_extension(ext)
|
|
45
|
+
|
|
46
|
+
def _check_libchrony(self, ext):
|
|
47
|
+
include_dirs = list(ext.include_dirs) + list(self.include_dirs or [])
|
|
48
|
+
library_dirs = list(ext.library_dirs) + list(self.library_dirs or [])
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
50
|
+
src = os.path.join(tmp, "check_libchrony.c")
|
|
51
|
+
with open(src, "w") as f:
|
|
52
|
+
f.write(
|
|
53
|
+
"#include <chrony.h>\n"
|
|
54
|
+
"int main(void) { return chrony_open_socket == 0; }\n"
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
objects = self.compiler.compile(
|
|
58
|
+
[src], output_dir=tmp, include_dirs=include_dirs
|
|
59
|
+
)
|
|
60
|
+
except CompileError:
|
|
61
|
+
raise PlatformError(
|
|
62
|
+
f"chrony.h not found.\n\n{LIBCHRONY_HELP}"
|
|
63
|
+
) from None
|
|
64
|
+
try:
|
|
65
|
+
self.compiler.link_executable(
|
|
66
|
+
objects,
|
|
67
|
+
"check_libchrony",
|
|
68
|
+
output_dir=tmp,
|
|
69
|
+
libraries=["chrony"],
|
|
70
|
+
library_dirs=library_dirs,
|
|
71
|
+
)
|
|
72
|
+
except LinkError:
|
|
73
|
+
raise PlatformError(
|
|
74
|
+
f"libchrony shared library not found.\n\n{LIBCHRONY_HELP}"
|
|
75
|
+
) from None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def should_build_cffi(environ=os.environ):
|
|
79
|
+
if environ.get("CIBUILDWHEEL") == "1" or environ.get("PYCHRONY_BUILD_CFFI") == "1":
|
|
80
|
+
return True
|
|
81
|
+
return environ.get("PYCHRONY_SKIP_CFFI") != "1"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# setuptools' PEP 517 backend runs this file as __main__; the guard lets tests
|
|
85
|
+
# import it without running setup().
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
if should_build_cffi():
|
|
88
|
+
setup(
|
|
89
|
+
cffi_modules=["src/pychrony/_core/_build_bindings.py:ffibuilder"],
|
|
90
|
+
cmdclass={"build_ext": build_ext_with_libchrony_check},
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
setup()
|
|
@@ -45,7 +45,12 @@ RTC Data:
|
|
|
45
45
|
... print("RTC tracking not configured")
|
|
46
46
|
|
|
47
47
|
Error Handling:
|
|
48
|
-
>>> from pychrony import
|
|
48
|
+
>>> from pychrony import (
|
|
49
|
+
... ChronyConnection,
|
|
50
|
+
... ChronyConnectionError,
|
|
51
|
+
... ChronyLibraryError,
|
|
52
|
+
... ChronyPermissionError,
|
|
53
|
+
... )
|
|
49
54
|
>>> try:
|
|
50
55
|
... with ChronyConnection() as conn:
|
|
51
56
|
... status = conn.get_tracking()
|
|
@@ -67,9 +72,19 @@ Choosing a Transport:
|
|
|
67
72
|
reads. Read-only consumers should prefer the command port. Note that
|
|
68
73
|
joining the chrony group does not grant access to the Unix socket.
|
|
69
74
|
|
|
75
|
+
To use the command port, ask for it explicitly; an explicit address is
|
|
76
|
+
used as given, with no fallback:
|
|
77
|
+
|
|
70
78
|
>>> from pychrony import ChronyConnection, Transport
|
|
79
|
+
>>> with ChronyConnection("127.0.0.1") as conn:
|
|
80
|
+
... status = conn.get_tracking()
|
|
81
|
+
|
|
82
|
+
With auto-detect, check what the connection settled on and refuse a
|
|
83
|
+
control channel rather than assume:
|
|
84
|
+
|
|
71
85
|
>>> with ChronyConnection() as conn:
|
|
72
|
-
...
|
|
86
|
+
... if conn.transport is not Transport.COMMAND_PORT:
|
|
87
|
+
... raise RuntimeError(f"refusing a control channel ({conn.address})")
|
|
73
88
|
... status = conn.get_tracking()
|
|
74
89
|
|
|
75
90
|
Custom Socket Path:
|
|
@@ -84,6 +99,15 @@ Remote chronyd via UDP:
|
|
|
84
99
|
>>> with ChronyConnection("192.168.1.100") as conn:
|
|
85
100
|
... status = conn.get_tracking()
|
|
86
101
|
|
|
102
|
+
Timeouts:
|
|
103
|
+
Each request waits `timeout` seconds (default 1.0) for chronyd's answer and
|
|
104
|
+
is resent up to twice, so an unanswered request raises
|
|
105
|
+
ChronyConnectionError after at most three times `timeout`. The command
|
|
106
|
+
port is UDP: a dropped datagram or a firewalled port is never answered.
|
|
107
|
+
|
|
108
|
+
>>> with ChronyConnection("192.168.1.100", timeout=0.5) as conn:
|
|
109
|
+
... status = conn.get_tracking()
|
|
110
|
+
|
|
87
111
|
Thread Safety:
|
|
88
112
|
ChronyConnection is NOT thread-safe. The underlying libchrony session
|
|
89
113
|
maintains stateful request/response cycles that cannot be safely shared
|
|
@@ -109,6 +133,9 @@ Thread Safety:
|
|
|
109
133
|
The returned dataclasses (TrackingStatus, Source, etc.) are frozen and
|
|
110
134
|
immutable, so they can be safely shared across threads after retrieval.
|
|
111
135
|
|
|
136
|
+
The same rules apply on free-threaded CPython builds (3.14t and later):
|
|
137
|
+
disabling the GIL does not make a single ChronyConnection safe to share.
|
|
138
|
+
|
|
112
139
|
For more information, see:
|
|
113
140
|
- https://github.com/arunderwood/pychrony
|
|
114
141
|
- https://chrony-project.org/
|