{"id":49405010,"url":"https://github.com/teaishealthy/roughly","last_synced_at":"2026-04-28T20:42:02.717Z","repository":{"id":334274599,"uuid":"1125409025","full_name":"teaishealthy/roughly","owner":"teaishealthy","description":"An asynchronous Python implementation of the Roughtime protocol ","archived":false,"fork":false,"pushed_at":"2026-01-30T22:40:54.000Z","size":240,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-31T14:29:29.276Z","etag":null,"topics":["async","cli","click","python","roughtime","time","wip"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/teaishealthy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-30T17:14:11.000Z","updated_at":"2026-01-30T22:42:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/teaishealthy/roughly","commit_stats":null,"previous_names":["teaishealthy/roughly"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/teaishealthy/roughly","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teaishealthy%2Froughly","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teaishealthy%2Froughly/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teaishealthy%2Froughly/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teaishealthy%2Froughly/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/teaishealthy","download_url":"https://codeload.github.com/teaishealthy/roughly/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/teaishealthy%2Froughly/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32399007,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T19:38:08.556Z","status":"ssl_error","status_checked_at":"2026-04-28T19:37:55.688Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","cli","click","python","roughtime","time","wip"],"created_at":"2026-04-28T20:42:01.567Z","updated_at":"2026-04-28T20:42:02.712Z","avatar_url":"https://github.com/teaishealthy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# roughly\n\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/teaishealthy/teaishealthy/refs/heads/main/ruff-badge.json\u0026style=flat-square)](https://github.com/astral-sh/ruff)\n![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/teaishealthy/roughly/tests.yml?style=flat-square\u0026label=tests)\n![Coveralls](https://img.shields.io/coverallsCoverage/github/teaishealthy/roughly?style=flat-square)\n[![Roughtime draft 07-15](https://img.shields.io/badge/draft%2007--15-f2d3ff?style=flat-square)](https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-15)\n![WIP](https://img.shields.io/badge/WIP-ffb1b1?style=flat-square)\n\nAn asynchronous implemenation of the Roughtime protocol for Python.\n\nImplements the Roughtime protocol as described in https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-15.\n\nDraft versions 07 through 15 are supported for querying servers.\\\nDraft versions 10 through 15 are supported for running a server. Also supports queries from Google Roughtime clients.\n\n\n## Quickstart\n\n### Installation\nYou can install `roughly` from GitHub using your favorite package manager, for example with `pip`:\n\n```bash\npip install \"git+https://github.com/teaishealthy/roughly.git\"\n# or with the cli extra\npip install \"git+https://github.com/teaishealthy/roughly.git#egg=project[cli]\"\n```\n\n### As a CLI\n\n#### Querying\n\nYou can use `roughly` as a command line tool to query Roughtime servers.\nInstall `roughly` with the `cli` extra using your favorite CLI package manager, for example with `uv` (or `pipx`):\n\n```bash\n# Assuming you cloned the repository\nuv tool install .[cli]\npipx install .[cli]\n```\n\nThen you can query a Roughtime server like so:\n\n```bash\nroughly query time.teax.dev 2002 84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=\n```\n\nOr run ecosystem queries (assuming you have an `ecosystem.json` file):\n\n```bash\nroughly ecosystem malfeasance\nroughly ecosystem state\n```\n\n#### Running a server\n\nYou can also run your own Roughtime server using `roughly`.\n\nFirst, generate a keypair:\n\n```bash\nroughly server keygen\n```\nThis will output a .env file containing the server's private key.\n\nYou can then run the server like so:\n\n```bash\nROUGHLY_SERVER_PRIVATE_KEY=\"your_private_key_here\" roughly -v server run\n```\n\nBy default, the server will bind to `0.0.0.0:2002`. You can change this using the `--host` and `--port` flags.\nI recommend running the server with verbose logging enabled (`-v`), so you can see incoming requests and debug any issues.\n\n### As a library\n\n#### Querying\n\n`roughly` can be used as an asynchronous library to query Roughtime servers from your own Python code.\n\n```python\nimport roughly.client\n\nresponse = await roughly.client.send_request(\n    host=\"time.teax.dev\",\n    port=2002,\n    public_key=base64.b64decode(b\"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=\")\n)\n# Responses are always verified before being returned\n\nprint(\"Current time:\", response.signed_response.midpoint)\n```\n\nYou can also use the built-in ecosystem tools to query multiple servers and check for malfeasance as described in the RFC.\n\n```python\nfrom pathlib import Path\nimport json\n\nfrom roughly.ecosystem import (\n    confirm_malfeasance,\n    load_ecosystem,\n    malfeasance_report,\n    pick_servers,\n    query_servers,\n)\n\necosystem = load_ecosystem(Path(\"ecosystem.json\"))\nselected_servers = await pick_servers(ecosystem)\nresponses = await query_servers(selected_servers)\nreport = malfeasance_report(responses, selected_servers)\n\nif confirm_malfeasance(report):\n    print(\"something scary is going on!\")\n    with open(\"malfeasance_report.json\", \"w\") as f:\n        json.dump(report, f, indent=2)\n```\n\n#### Running a server\n\nYou can also programmatically run your own Roughtime server:\n\n```python\nimport roughly.server\n\nserver = roughly.server.Server.create() # generates a new keypair\nawait roughly.server.serve(server)\n```\n\nWhy? You can subclass `roughly.server.UDPHandler` and `roughly.server.Server` to implement custom behavior. Like a malfeasant server for testing:\n\n```python\nimport roughly\nimport roughly.server\n\nclass ScaryServer(roughly.server.Server):\n    @staticmethod\n    def get_time() -\u003e int:\n        # return a wrong-ish time\n        return int(time.time()) + random.randint(-3600, 3600)\n\nawait roughly.server.serve(ScaryServer.create())\n```\n\n## Ecosystem\n\nAn example ecosystem file can be found at [ecosystem.json](ecosystem.json), I tried my best to include as many servers as I could find.\n\nIf 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!\n\n\n## Interoperability\n\nThe interopability matrix of `roughly` against Roughtime servers looks like this:\n\n### Roughly as a client\n\n| Server | Result |\n|---|---:|\n| [butterfield](https://github.com/signalsforgranted/butterfield) | ✅ |\n| [cloudflare](https://github.com/cloudflare/roughtime) | ✅ |\n| [pyroughtime](https://github.com/dansarie/pyroughtime) | ✅ |\n| [roughenough](https://github.com/int08h/roughenough/) | ⚠️ |\n| [roughtimed](https://github.com/dansarie/roughtimed) | ✅ |\n| roughly | ✅ |\n\n⚠️ `roughenough` only expects version `0x8000000c` and does not ignore unknown versions.\nMake sure to explicitly request only version `0x8000000c` when querying `roughenough` servers, i.e.:\n\n```python\nawait roughly.client.send_request(\n    # \u003csnip!\u003e\n    versions=(0x8000000c,),\n)\n```\n\n### Roughly as a server\n\n| Client | Result |\n|---|---:|\n| cloudflare | ✅ |\n| craggy | ✅ |\n| node-roughtime | ✅ |\n| pyroughtime | ✅ |\n| roughenough | ❌ |\n| roughly | ✅ |\n| vroughtime | ✅ |\n\n\n\n\n### draft-7\n\nSupport for draft-7 is limited, in the sense that `roughly` will fit responses from draft-7 servers into the draft-15 data structures.\nThis means that some fields that are not present in draft-8+ (such as DUT1, DTAI, and LEAP) will be missing.\nAdditionally 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.\n\n### VDIFF comments\n\nThroughout the codebase, comments beginning with `# VDIFF` mark sections that accommodate differences between Roughtime protocol drafts. These annotations help track changes made for compatibility and make it easier to identify code adjusted for specific draft versions.\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteaishealthy%2Froughly","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fteaishealthy%2Froughly","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteaishealthy%2Froughly/lists"}