roughly 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
roughly-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 teaishealthy
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.
roughly-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: roughly
3
+ Version: 0.1.0
4
+ Summary: An asynchronous Roughtime implementation for Python
5
+ Keywords: roughtime,time,ntp,synchronization,asyncio
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Topic :: Internet
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Classifier: Topic :: System :: Networking
18
+ Classifier: Topic :: System :: Networking :: Time Synchronization
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Classifier: Framework :: AsyncIO
22
+ Requires-Dist: cryptography>=46.0.3
23
+ Requires-Dist: click>=8.3.1 ; extra == 'cli'
24
+ Requires-Python: >=3.12
25
+ Project-URL: Repository, https://github.com/teaishealthy/roughly
26
+ Project-URL: Issues, https://github.com/teaishealthy/roughly/issues
27
+ Provides-Extra: cli
28
+ Description-Content-Type: text/markdown
29
+
30
+ # roughly
31
+
32
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/teaishealthy/teaishealthy/refs/heads/main/ruff-badge.json&style=flat-square)](https://github.com/astral-sh/ruff)
33
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/teaishealthy/roughly/tests.yml?style=flat-square&label=tests)
34
+ ![Coveralls](https://img.shields.io/coverallsCoverage/github/teaishealthy/roughly?style=flat-square)
35
+ [![Roughtime draft 07-19](https://img.shields.io/badge/draft%2007--19-f2d3ff?style=flat-square)](https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-19)
36
+ ![WIP](https://img.shields.io/badge/WIP-ffb1b1?style=flat-square)
37
+
38
+ An asynchronous implemenation of the Roughtime protocol for Python.
39
+
40
+ Implements the Roughtime protocol as described in https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-19.
41
+
42
+ Draft versions 07 through 19 are supported for querying servers.\
43
+ Draft versions 10 through 19 are supported for running a server. Also supports queries from Google Roughtime clients.
44
+
45
+
46
+ ## Quickstart
47
+
48
+ ### Installation
49
+ You can install `roughly` from PyPI using your favorite package manager, for example with `pip`:
50
+
51
+ ```bash
52
+ pip install roughly
53
+ # or with the cli extra
54
+ pip install roughly[cli]
55
+ ```
56
+
57
+ ### As a CLI
58
+
59
+ #### Querying
60
+
61
+ You can use `roughly` as a command line tool to query Roughtime servers.
62
+ Install `roughly` with the `cli` extra using your favorite CLI package manager, for example with `uv` (or `pipx`):
63
+
64
+ ```bash
65
+ uv tool install roughly[cli]
66
+ pipx install roughly[cli]
67
+ ```
68
+
69
+ Then you can query a Roughtime server like so:
70
+
71
+ ```bash
72
+ roughly query time.teax.dev 2002 84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=
73
+ ```
74
+
75
+ Or run ecosystem queries (assuming you have an `ecosystem.json` file):
76
+
77
+ ```bash
78
+ roughly ecosystem malfeasance
79
+ roughly ecosystem state
80
+ ```
81
+
82
+ #### Running a server
83
+
84
+ You can also run your own Roughtime server using `roughly`.
85
+
86
+ First, generate a keypair:
87
+
88
+ ```bash
89
+ roughly server keygen
90
+ ```
91
+ This will output a .env file containing the server's private key.
92
+
93
+ You can then run the server like so:
94
+
95
+ ```bash
96
+ ROUGHLY_SERVER_PRIVATE_KEY="your_private_key_here" roughly -v server run
97
+ ```
98
+
99
+ By default, the server will bind to `0.0.0.0:2002`. You can change this using the `--host` and `--port` flags.
100
+ I recommend running the server with verbose logging enabled (`-v`), so you can see incoming requests and debug any issues.
101
+ Additionally you might want to consider turning off response greasing while testing using the `--no-grease` flag.
102
+
103
+ ### As a library
104
+
105
+ #### Querying
106
+
107
+ `roughly` can be used as an asynchronous library to query Roughtime servers from your own Python code.
108
+
109
+ ```python
110
+ import roughly.client
111
+
112
+ response = await roughly.client.send_request(
113
+ host="time.teax.dev",
114
+ port=2002,
115
+ public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=")
116
+ )
117
+ # Responses are always verified before being returned
118
+
119
+ print("Current time:", response.signed_response.midpoint)
120
+ ```
121
+
122
+ You can also use the built-in ecosystem tools to query multiple servers and check for malfeasance as described in the RFC.
123
+
124
+ ```python
125
+ from pathlib import Path
126
+ import json
127
+
128
+ from roughly.ecosystem import (
129
+ confirm_malfeasance,
130
+ load_ecosystem,
131
+ malfeasance_report,
132
+ pick_servers,
133
+ query_servers,
134
+ )
135
+
136
+ ecosystem = load_ecosystem(Path("ecosystem.json"))
137
+ selected_servers = await pick_servers(ecosystem)
138
+ responses = await query_servers(selected_servers)
139
+ report = malfeasance_report(responses, selected_servers)
140
+
141
+ if confirm_malfeasance(report):
142
+ print("something scary is going on!")
143
+ with open("malfeasance_report.json", "w") as f:
144
+ json.dump(report, f, indent=2)
145
+ ```
146
+
147
+ #### Running a server
148
+
149
+ You can also programmatically run your own Roughtime server:
150
+
151
+ ```python
152
+ import roughly.server
153
+
154
+ server = roughly.server.Server.create() # generates a new keypair
155
+ await roughly.server.serve(server)
156
+ ```
157
+
158
+ Why? You can subclass `roughly.server.UDPHandler` and `roughly.server.Server` to implement custom behavior. Like a malfeasant server for testing:
159
+
160
+ ```python
161
+ import roughly
162
+ import roughly.server
163
+
164
+ class ScaryServer(roughly.server.Server):
165
+ @staticmethod
166
+ def get_time() -> int:
167
+ # return a wrong-ish time
168
+ return int(time.time()) + random.randint(-3600, 3600)
169
+
170
+ await roughly.server.serve(ScaryServer.create())
171
+ ```
172
+
173
+ ## Ecosystem
174
+
175
+ An example ecosystem file can be found at [ecosystem.json](ecosystem.json), I tried my best to include as many servers as I could find.
176
+
177
+ If you know of any other Roughtime servers, run your own server, or have updated public keys for any of the listed servers, please open a PR or an issue!
178
+
179
+
180
+ ## Interoperability
181
+
182
+ The interopability matrix of `roughly` against Roughtime servers looks like this:
183
+
184
+ ### Roughly as a client
185
+
186
+ | Server | Result |
187
+ |---|---:|
188
+ | [butterfield](https://github.com/signalsforgranted/butterfield) | ✅ |
189
+ | [cloudflare](https://github.com/cloudflare/roughtime) | ✅ |
190
+ | [pyroughtime](https://github.com/dansarie/pyroughtime) | ✅ |
191
+ | [roughenough](https://github.com/int08h/roughenough/) | ⚠️ |
192
+ | [roughtimed](https://github.com/dansarie/roughtimed) | ✅ |
193
+ | roughly | ✅ |
194
+
195
+ ⚠️ `roughenough` only expects version `0x8000000c` and does not ignore unknown versions.
196
+ Make sure to explicitly request only version `0x8000000c` when querying `roughenough` servers, i.e.:
197
+
198
+ ```python
199
+ await roughly.client.send_request(
200
+ # <snip!>
201
+ versions=(0x8000000c,),
202
+ )
203
+ ```
204
+
205
+ ### Roughly as a server
206
+
207
+ | Client | Result |
208
+ |---|---:|
209
+ | cloudflare | ✅ |
210
+ | craggy | ✅ |
211
+ | node-roughtime | ✅ |
212
+ | pyroughtime | ✅ |
213
+ | roughenough | ❌ |
214
+ | roughly | ✅ |
215
+ | vroughtime | ✅ |
216
+
217
+
218
+
219
+
220
+ ### draft-7
221
+
222
+ Support for draft-7 is limited, in the sense that `roughly` will fit responses from draft-7 servers into the draft-15 data structures.
223
+ This means that some fields that are not present in draft-8+ (such as DUT1, DTAI, and LEAP) will be missing.
224
+ Additionally draft-7 offered for the precision of radius to be in microseconds, while draft-8+ uses seconds, this precision will be lost when querying draft-7 servers, and be clamped to a minimum of one second.
225
+
226
+ ## License
227
+
228
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,199 @@
1
+ # roughly
2
+
3
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/teaishealthy/teaishealthy/refs/heads/main/ruff-badge.json&style=flat-square)](https://github.com/astral-sh/ruff)
4
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/teaishealthy/roughly/tests.yml?style=flat-square&label=tests)
5
+ ![Coveralls](https://img.shields.io/coverallsCoverage/github/teaishealthy/roughly?style=flat-square)
6
+ [![Roughtime draft 07-19](https://img.shields.io/badge/draft%2007--19-f2d3ff?style=flat-square)](https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-19)
7
+ ![WIP](https://img.shields.io/badge/WIP-ffb1b1?style=flat-square)
8
+
9
+ An asynchronous implemenation of the Roughtime protocol for Python.
10
+
11
+ Implements the Roughtime protocol as described in https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-19.
12
+
13
+ Draft versions 07 through 19 are supported for querying servers.\
14
+ Draft versions 10 through 19 are supported for running a server. Also supports queries from Google Roughtime clients.
15
+
16
+
17
+ ## Quickstart
18
+
19
+ ### Installation
20
+ You can install `roughly` from PyPI using your favorite package manager, for example with `pip`:
21
+
22
+ ```bash
23
+ pip install roughly
24
+ # or with the cli extra
25
+ pip install roughly[cli]
26
+ ```
27
+
28
+ ### As a CLI
29
+
30
+ #### Querying
31
+
32
+ You can use `roughly` as a command line tool to query Roughtime servers.
33
+ Install `roughly` with the `cli` extra using your favorite CLI package manager, for example with `uv` (or `pipx`):
34
+
35
+ ```bash
36
+ uv tool install roughly[cli]
37
+ pipx install roughly[cli]
38
+ ```
39
+
40
+ Then you can query a Roughtime server like so:
41
+
42
+ ```bash
43
+ roughly query time.teax.dev 2002 84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=
44
+ ```
45
+
46
+ Or run ecosystem queries (assuming you have an `ecosystem.json` file):
47
+
48
+ ```bash
49
+ roughly ecosystem malfeasance
50
+ roughly ecosystem state
51
+ ```
52
+
53
+ #### Running a server
54
+
55
+ You can also run your own Roughtime server using `roughly`.
56
+
57
+ First, generate a keypair:
58
+
59
+ ```bash
60
+ roughly server keygen
61
+ ```
62
+ This will output a .env file containing the server's private key.
63
+
64
+ You can then run the server like so:
65
+
66
+ ```bash
67
+ ROUGHLY_SERVER_PRIVATE_KEY="your_private_key_here" roughly -v server run
68
+ ```
69
+
70
+ By default, the server will bind to `0.0.0.0:2002`. You can change this using the `--host` and `--port` flags.
71
+ I recommend running the server with verbose logging enabled (`-v`), so you can see incoming requests and debug any issues.
72
+ Additionally you might want to consider turning off response greasing while testing using the `--no-grease` flag.
73
+
74
+ ### As a library
75
+
76
+ #### Querying
77
+
78
+ `roughly` can be used as an asynchronous library to query Roughtime servers from your own Python code.
79
+
80
+ ```python
81
+ import roughly.client
82
+
83
+ response = await roughly.client.send_request(
84
+ host="time.teax.dev",
85
+ port=2002,
86
+ public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=")
87
+ )
88
+ # Responses are always verified before being returned
89
+
90
+ print("Current time:", response.signed_response.midpoint)
91
+ ```
92
+
93
+ You can also use the built-in ecosystem tools to query multiple servers and check for malfeasance as described in the RFC.
94
+
95
+ ```python
96
+ from pathlib import Path
97
+ import json
98
+
99
+ from roughly.ecosystem import (
100
+ confirm_malfeasance,
101
+ load_ecosystem,
102
+ malfeasance_report,
103
+ pick_servers,
104
+ query_servers,
105
+ )
106
+
107
+ ecosystem = load_ecosystem(Path("ecosystem.json"))
108
+ selected_servers = await pick_servers(ecosystem)
109
+ responses = await query_servers(selected_servers)
110
+ report = malfeasance_report(responses, selected_servers)
111
+
112
+ if confirm_malfeasance(report):
113
+ print("something scary is going on!")
114
+ with open("malfeasance_report.json", "w") as f:
115
+ json.dump(report, f, indent=2)
116
+ ```
117
+
118
+ #### Running a server
119
+
120
+ You can also programmatically run your own Roughtime server:
121
+
122
+ ```python
123
+ import roughly.server
124
+
125
+ server = roughly.server.Server.create() # generates a new keypair
126
+ await roughly.server.serve(server)
127
+ ```
128
+
129
+ Why? You can subclass `roughly.server.UDPHandler` and `roughly.server.Server` to implement custom behavior. Like a malfeasant server for testing:
130
+
131
+ ```python
132
+ import roughly
133
+ import roughly.server
134
+
135
+ class ScaryServer(roughly.server.Server):
136
+ @staticmethod
137
+ def get_time() -> int:
138
+ # return a wrong-ish time
139
+ return int(time.time()) + random.randint(-3600, 3600)
140
+
141
+ await roughly.server.serve(ScaryServer.create())
142
+ ```
143
+
144
+ ## Ecosystem
145
+
146
+ An example ecosystem file can be found at [ecosystem.json](ecosystem.json), I tried my best to include as many servers as I could find.
147
+
148
+ If you know of any other Roughtime servers, run your own server, or have updated public keys for any of the listed servers, please open a PR or an issue!
149
+
150
+
151
+ ## Interoperability
152
+
153
+ The interopability matrix of `roughly` against Roughtime servers looks like this:
154
+
155
+ ### Roughly as a client
156
+
157
+ | Server | Result |
158
+ |---|---:|
159
+ | [butterfield](https://github.com/signalsforgranted/butterfield) | ✅ |
160
+ | [cloudflare](https://github.com/cloudflare/roughtime) | ✅ |
161
+ | [pyroughtime](https://github.com/dansarie/pyroughtime) | ✅ |
162
+ | [roughenough](https://github.com/int08h/roughenough/) | ⚠️ |
163
+ | [roughtimed](https://github.com/dansarie/roughtimed) | ✅ |
164
+ | roughly | ✅ |
165
+
166
+ ⚠️ `roughenough` only expects version `0x8000000c` and does not ignore unknown versions.
167
+ Make sure to explicitly request only version `0x8000000c` when querying `roughenough` servers, i.e.:
168
+
169
+ ```python
170
+ await roughly.client.send_request(
171
+ # <snip!>
172
+ versions=(0x8000000c,),
173
+ )
174
+ ```
175
+
176
+ ### Roughly as a server
177
+
178
+ | Client | Result |
179
+ |---|---:|
180
+ | cloudflare | ✅ |
181
+ | craggy | ✅ |
182
+ | node-roughtime | ✅ |
183
+ | pyroughtime | ✅ |
184
+ | roughenough | ❌ |
185
+ | roughly | ✅ |
186
+ | vroughtime | ✅ |
187
+
188
+
189
+
190
+
191
+ ### draft-7
192
+
193
+ Support for draft-7 is limited, in the sense that `roughly` will fit responses from draft-7 servers into the draft-15 data structures.
194
+ This means that some fields that are not present in draft-8+ (such as DUT1, DTAI, and LEAP) will be missing.
195
+ Additionally draft-7 offered for the precision of radius to be in microseconds, while draft-8+ uses seconds, this precision will be lost when querying draft-7 servers, and be clamped to a minimum of one second.
196
+
197
+ ## License
198
+
199
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,111 @@
1
+ [project]
2
+ name = "roughly"
3
+ version = "0.1.0"
4
+ description = "An asynchronous Roughtime implementation for Python"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ dependencies = ["cryptography>=46.0.3"]
10
+ keywords = ["roughtime", "time", "ntp", "synchronization", "asyncio"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Programming Language :: Python :: 3.14",
15
+
16
+ "Operating System :: OS Independent",
17
+
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Information Technology",
21
+
22
+ "Topic :: Internet",
23
+ "Topic :: Security :: Cryptography",
24
+ "Topic :: System :: Networking",
25
+ "Topic :: System :: Networking :: Time Synchronization",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ "Typing :: Typed",
28
+ "Framework :: AsyncIO",
29
+ ]
30
+
31
+ [project.urls]
32
+ "Repository" = "https://github.com/teaishealthy/roughly"
33
+ "Issues" = "https://github.com/teaishealthy/roughly/issues"
34
+
35
+ [project.optional-dependencies]
36
+ cli = ["click>=8.3.1"]
37
+
38
+ [project.scripts]
39
+ roughly = "roughly.cli:cli"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=9.0.2",
44
+ "pytest-asyncio>=1.3.0",
45
+ "pytest-cov>=7.0.0",
46
+ "ruff>=0.14.10",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ include = ["roughly/*"]
51
+ line-length = 100
52
+
53
+
54
+ [tool.ruff.lint]
55
+ select = ["ALL"]
56
+ ignore = [
57
+ "A001", # variable shadowing a built-in
58
+ "A002", # argument shadowing a built-in
59
+ "ANN401", # no Any
60
+
61
+ "ASYNC109", # async function with timeout parameter
62
+
63
+ "TC006", # runtime-cast-value, i think it looks bad
64
+ "TRY003", # error messages in exceptions
65
+
66
+ "TD002",
67
+ "TD003",
68
+ "FIX002",
69
+
70
+ # these are disabled because i can't configure what 'public' means
71
+ "D100", # missing docstring in public module
72
+ "D101", # missing docstring in public class
73
+ "D102", # missing docstring in public method
74
+ "D103", # missing docstring in public function
75
+ "D105", # missing docstring in magic method
76
+ "D107", # missing docstring in __init__
77
+
78
+ # disabled because of `ruff format`
79
+ "COM812",
80
+ "ISC001",
81
+
82
+ "EM101", # use of string literals in exceptions
83
+ "EM102", # use of f-string literals in exceptions
84
+
85
+ "SLF001", # private member access
86
+ ]
87
+
88
+ [tool.ruff.lint.per-file-ignores]
89
+ "tests/**/*" = [
90
+ "INP001", # implicit namespace packeges are "ok"
91
+ "S101", # allow asserts
92
+ "PLR2004", # magic numbers in tests are fine
93
+ ]
94
+
95
+ [tool.ruff.lint.pydocstyle]
96
+ convention = "google" # Accepts: "google", "numpy", or "pep257".
97
+
98
+ [tool.uv.build-backend]
99
+ module-root = ""
100
+
101
+
102
+ [tool.coverage.report]
103
+ exclude_also = ["pragma: no cover", "if TYPE_CHECKING:"]
104
+
105
+ [tool.pytest.ini_options]
106
+ log_cli = true
107
+ log_cli_level = "DEBUG"
108
+
109
+ [build-system]
110
+ requires = ["uv_build>=0.9.27,<0.10.0"]
111
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ """Roughtime protocol implementation in Python."""