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.
Files changed (153) hide show
  1. pychrony-3.0.2/MANIFEST.in +22 -0
  2. {pychrony-2.0.3/src/pychrony.egg-info → pychrony-3.0.2}/PKG-INFO +59 -12
  3. {pychrony-2.0.3 → pychrony-3.0.2}/README.md +51 -8
  4. pychrony-3.0.2/pyproject.toml +164 -0
  5. pychrony-3.0.2/setup.py +93 -0
  6. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/__init__.py +29 -2
  7. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/__init__.py +1 -1
  8. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/_bindings.py +286 -95
  9. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/_build_bindings.py +23 -14
  10. pychrony-3.0.2/src/pychrony/_core/_cffi_bindings.pyi +86 -0
  11. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/models.py +6 -0
  12. pychrony-3.0.2/src/pychrony/pytest_plugin.py +56 -0
  13. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/testing.py +8 -33
  14. {pychrony-2.0.3 → pychrony-3.0.2/src/pychrony.egg-info}/PKG-INFO +59 -12
  15. pychrony-3.0.2/src/pychrony.egg-info/SOURCES.txt +60 -0
  16. pychrony-3.0.2/src/pychrony.egg-info/requires.txt +15 -0
  17. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony.egg-info/scm_file_list.json +8 -0
  18. pychrony-3.0.2/src/pychrony.egg-info/scm_version.json +8 -0
  19. {pychrony-2.0.3 → pychrony-3.0.2}/tests/conftest.py +3 -0
  20. {pychrony-2.0.3 → pychrony-3.0.2}/tests/contract/test_api.py +10 -0
  21. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/conftest.py +4 -2
  22. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_connection.py +45 -0
  23. {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/config.py +14 -0
  24. {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/context.py +1 -0
  25. {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/session.py +68 -14
  26. pychrony-3.0.2/tests/unit/test_cffi_stub.py +66 -0
  27. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_connection.py +55 -52
  28. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_enums.py +9 -9
  29. pychrony-3.0.2/tests/unit/test_free_threading.py +34 -0
  30. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_mock_scenarios.py +4 -1
  31. pychrony-3.0.2/tests/unit/test_request_timeout.py +222 -0
  32. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_rtc_scenarios.py +64 -1
  33. pychrony-3.0.2/tests/unit/test_setup.py +111 -0
  34. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_testing.py +87 -1
  35. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_validation.py +19 -0
  36. pychrony-2.0.3/.claude/commands/speckit.analyze.md +0 -184
  37. pychrony-2.0.3/.claude/commands/speckit.checklist.md +0 -294
  38. pychrony-2.0.3/.claude/commands/speckit.clarify.md +0 -181
  39. pychrony-2.0.3/.claude/commands/speckit.constitution.md +0 -82
  40. pychrony-2.0.3/.claude/commands/speckit.implement.md +0 -135
  41. pychrony-2.0.3/.claude/commands/speckit.plan.md +0 -89
  42. pychrony-2.0.3/.claude/commands/speckit.specify.md +0 -258
  43. pychrony-2.0.3/.claude/commands/speckit.tasks.md +0 -137
  44. pychrony-2.0.3/.claude/commands/speckit.taskstoissues.md +0 -30
  45. pychrony-2.0.3/.dockerignore +0 -48
  46. pychrony-2.0.3/.github/dependabot.yml +0 -38
  47. pychrony-2.0.3/.github/workflows/ci.yml +0 -152
  48. pychrony-2.0.3/.github/workflows/docs.yml +0 -62
  49. pychrony-2.0.3/.github/workflows/release.yml +0 -181
  50. pychrony-2.0.3/.gitignore +0 -215
  51. pychrony-2.0.3/.gitmodules +0 -4
  52. pychrony-2.0.3/.python-version +0 -1
  53. pychrony-2.0.3/.specify/memory/constitution.md +0 -42
  54. pychrony-2.0.3/.specify/scripts/bash/check-prerequisites.sh +0 -166
  55. pychrony-2.0.3/.specify/scripts/bash/common.sh +0 -156
  56. pychrony-2.0.3/.specify/scripts/bash/create-new-feature.sh +0 -297
  57. pychrony-2.0.3/.specify/scripts/bash/execute-task-generation.sh +0 -255
  58. pychrony-2.0.3/.specify/scripts/bash/setup-plan.sh +0 -61
  59. pychrony-2.0.3/.specify/scripts/bash/update-agent-context.sh +0 -799
  60. pychrony-2.0.3/.specify/templates/agent-file-template.md +0 -28
  61. pychrony-2.0.3/.specify/templates/checklist-template.md +0 -40
  62. pychrony-2.0.3/.specify/templates/plan-template.md +0 -112
  63. pychrony-2.0.3/.specify/templates/spec-template.md +0 -118
  64. pychrony-2.0.3/.specify/templates/tasks-template.md +0 -254
  65. pychrony-2.0.3/CLAUDE.md +0 -90
  66. pychrony-2.0.3/TESTING.md +0 -466
  67. pychrony-2.0.3/docker/Dockerfile.test +0 -94
  68. pychrony-2.0.3/docker/docker-compose.test.yml +0 -67
  69. pychrony-2.0.3/docs/api/index.md +0 -85
  70. pychrony-2.0.3/docs/index.md +0 -208
  71. pychrony-2.0.3/mkdocs.yml +0 -52
  72. pychrony-2.0.3/pyproject.toml +0 -121
  73. pychrony-2.0.3/setup.py +0 -23
  74. pychrony-2.0.3/specs/001-python-scaffold/checklists/requirements.md +0 -34
  75. pychrony-2.0.3/specs/001-python-scaffold/contracts/package.md +0 -137
  76. pychrony-2.0.3/specs/001-python-scaffold/data-model.md +0 -74
  77. pychrony-2.0.3/specs/001-python-scaffold/plan.md +0 -109
  78. pychrony-2.0.3/specs/001-python-scaffold/quickstart.md +0 -164
  79. pychrony-2.0.3/specs/001-python-scaffold/research.md +0 -159
  80. pychrony-2.0.3/specs/001-python-scaffold/spec.md +0 -167
  81. pychrony-2.0.3/specs/001-python-scaffold/tasks.md +0 -217
  82. pychrony-2.0.3/specs/002-tracking-binding/checklists/requirements.md +0 -34
  83. pychrony-2.0.3/specs/002-tracking-binding/contracts/python-api.md +0 -292
  84. pychrony-2.0.3/specs/002-tracking-binding/data-model.md +0 -317
  85. pychrony-2.0.3/specs/002-tracking-binding/plan.md +0 -146
  86. pychrony-2.0.3/specs/002-tracking-binding/quickstart.md +0 -262
  87. pychrony-2.0.3/specs/002-tracking-binding/research.md +0 -324
  88. pychrony-2.0.3/specs/002-tracking-binding/spec.md +0 -112
  89. pychrony-2.0.3/specs/002-tracking-binding/tasks.md +0 -220
  90. pychrony-2.0.3/specs/003-multiple-reports-bindings/checklists/requirements.md +0 -37
  91. pychrony-2.0.3/specs/003-multiple-reports-bindings/contracts/api.md +0 -215
  92. pychrony-2.0.3/specs/003-multiple-reports-bindings/contracts/python-api.md +0 -281
  93. pychrony-2.0.3/specs/003-multiple-reports-bindings/data-model.md +0 -289
  94. pychrony-2.0.3/specs/003-multiple-reports-bindings/plan.md +0 -125
  95. pychrony-2.0.3/specs/003-multiple-reports-bindings/quickstart.md +0 -220
  96. pychrony-2.0.3/specs/003-multiple-reports-bindings/research.md +0 -255
  97. pychrony-2.0.3/specs/003-multiple-reports-bindings/spec.md +0 -147
  98. pychrony-2.0.3/specs/003-multiple-reports-bindings/tasks.md +0 -271
  99. pychrony-2.0.3/specs/004-categorical-enums/checklists/requirements.md +0 -36
  100. pychrony-2.0.3/specs/004-categorical-enums/data-model.md +0 -206
  101. pychrony-2.0.3/specs/004-categorical-enums/plan.md +0 -85
  102. pychrony-2.0.3/specs/004-categorical-enums/quickstart.md +0 -142
  103. pychrony-2.0.3/specs/004-categorical-enums/research.md +0 -152
  104. pychrony-2.0.3/specs/004-categorical-enums/spec.md +0 -104
  105. pychrony-2.0.3/specs/004-categorical-enums/tasks.md +0 -220
  106. pychrony-2.0.3/specs/005-pypi-packaging/checklists/requirements.md +0 -37
  107. pychrony-2.0.3/specs/005-pypi-packaging/plan.md +0 -83
  108. pychrony-2.0.3/specs/005-pypi-packaging/quickstart.md +0 -165
  109. pychrony-2.0.3/specs/005-pypi-packaging/research.md +0 -191
  110. pychrony-2.0.3/specs/005-pypi-packaging/spec.md +0 -146
  111. pychrony-2.0.3/specs/005-pypi-packaging/tasks.md +0 -213
  112. pychrony-2.0.3/specs/006-protocol-test-mocks/analysis-report.md +0 -197
  113. pychrony-2.0.3/specs/006-protocol-test-mocks/checklists/requirements.md +0 -50
  114. pychrony-2.0.3/specs/006-protocol-test-mocks/contracts/mock-api.md +0 -384
  115. pychrony-2.0.3/specs/006-protocol-test-mocks/data-model.md +0 -434
  116. pychrony-2.0.3/specs/006-protocol-test-mocks/plan.md +0 -100
  117. pychrony-2.0.3/specs/006-protocol-test-mocks/quickstart.md +0 -470
  118. pychrony-2.0.3/specs/006-protocol-test-mocks/research.md +0 -379
  119. pychrony-2.0.3/specs/006-protocol-test-mocks/spec.md +0 -176
  120. pychrony-2.0.3/specs/006-protocol-test-mocks/tasks.md +0 -370
  121. pychrony-2.0.3/specs/007-api-docs-github-pages/tasks.md +0 -222
  122. pychrony-2.0.3/src/pychrony.egg-info/SOURCES.txt +0 -139
  123. pychrony-2.0.3/src/pychrony.egg-info/entry_points.txt +0 -2
  124. pychrony-2.0.3/src/pychrony.egg-info/requires.txt +0 -1
  125. pychrony-2.0.3/src/pychrony.egg-info/scm_version.json +0 -8
  126. pychrony-2.0.3/uv.lock +0 -1100
  127. {pychrony-2.0.3 → pychrony-3.0.2}/LICENSE +0 -0
  128. {pychrony-2.0.3 → pychrony-3.0.2}/LICENSES/LGPL-2.1-or-later.txt +0 -0
  129. {pychrony-2.0.3 → pychrony-3.0.2}/setup.cfg +0 -0
  130. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/_core/_fields.py +0 -0
  131. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/exceptions.py +0 -0
  132. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony/py.typed +0 -0
  133. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony.egg-info/dependency_links.txt +0 -0
  134. {pychrony-2.0.3 → pychrony-3.0.2}/src/pychrony.egg-info/top_level.txt +0 -0
  135. {pychrony-2.0.3 → pychrony-3.0.2}/tests/__init__.py +0 -0
  136. {pychrony-2.0.3 → pychrony-3.0.2}/tests/contract/__init__.py +0 -0
  137. {pychrony-2.0.3 → pychrony-3.0.2}/tests/contract/test_api_stability.py +0 -0
  138. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/__init__.py +0 -0
  139. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_enums.py +0 -0
  140. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_rtcdata.py +0 -0
  141. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_sources.py +0 -0
  142. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_sourcestats.py +0 -0
  143. {pychrony-2.0.3 → pychrony-3.0.2}/tests/integration/test_tracking.py +0 -0
  144. {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/__init__.py +0 -0
  145. {pychrony-2.0.3 → pychrony-3.0.2}/tests/mocks/scenarios.py +0 -0
  146. {pychrony-2.0.3 → pychrony-3.0.2}/tests/test_import.py +0 -0
  147. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/__init__.py +0 -0
  148. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_exceptions.py +0 -0
  149. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_leap_scenarios.py +0 -0
  150. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_mock_config.py +0 -0
  151. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_models.py +0 -0
  152. {pychrony-2.0.3 → pychrony-3.0.2}/tests/unit/test_source_list_changes.py +0 -0
  153. {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: 2.0.3
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.10
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.14.0
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
- Source installs (sdist or development) require [libchrony](https://gitlab.com/chrony/libchrony) to be installed on your system.
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 require it:
148
+ consumers should prefer the command port, and can ask for it explicitly:
125
149
 
126
150
  ```python
127
- from pychrony import ChronyConnection, Transport
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.10, 3.11, 3.12, 3.13, 3.14, 3.15
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
- Source installs (sdist or development) require [libchrony](https://gitlab.com/chrony/libchrony) to be installed on your system.
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 require it:
115
+ consumers should prefer the command port, and can ask for it explicitly:
96
116
 
97
117
  ```python
98
- from pychrony import ChronyConnection, Transport
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.10, 3.11, 3.12, 3.13, 3.14, 3.15
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
+ ]
@@ -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 ChronyConnection, ChronyError
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
- ... assert conn.transport is Transport.COMMAND_PORT
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/
@@ -3,5 +3,5 @@
3
3
  This module contains the CFFI interface to libchrony for
4
4
  retrieving tracking status from chronyd.
5
5
 
6
- Internal implementation - use pychrony.get_tracking() instead.
6
+ Internal implementation - use pychrony.ChronyConnection instead.
7
7
  """