connections-export 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.
Files changed (494) hide show
  1. connections_export-0.1.0/.github/workflows/binaries.yml +144 -0
  2. connections_export-0.1.0/.github/workflows/ci.yml +82 -0
  3. connections_export-0.1.0/.github/workflows/packages.yml +181 -0
  4. connections_export-0.1.0/.github/workflows/release.yml +41 -0
  5. connections_export-0.1.0/.gitignore +29 -0
  6. connections_export-0.1.0/LICENSE +28 -0
  7. connections_export-0.1.0/PKG-INFO +373 -0
  8. connections_export-0.1.0/README.md +334 -0
  9. connections_export-0.1.0/connections-export.example.toml +73 -0
  10. connections_export-0.1.0/connections_export/__init__.py +1 -0
  11. connections_export-0.1.0/connections_export/_licenses/Markdown/LICENSE.md +30 -0
  12. connections_export-0.1.0/connections_export/_licenses/NOTICE.txt +80 -0
  13. connections_export-0.1.0/connections_export/_licenses/annotated-doc/LICENSE +21 -0
  14. connections_export-0.1.0/connections_export/_licenses/annotated-types/LICENSE +21 -0
  15. connections_export-0.1.0/connections_export/_licenses/anyio/LICENSE +20 -0
  16. connections_export-0.1.0/connections_export/_licenses/beautifulsoup4/LICENSE +31 -0
  17. connections_export-0.1.0/connections_export/_licenses/certifi/LICENSE +20 -0
  18. connections_export-0.1.0/connections_export/_licenses/click/LICENSE.txt +28 -0
  19. connections_export-0.1.0/connections_export/_licenses/connections-export/LICENSE +28 -0
  20. connections_export-0.1.0/connections_export/_licenses/fastapi/LICENSE +21 -0
  21. connections_export-0.1.0/connections_export/_licenses/greenlet/LICENSE +30 -0
  22. connections_export-0.1.0/connections_export/_licenses/greenlet/LICENSE.PSF +47 -0
  23. connections_export-0.1.0/connections_export/_licenses/h11/LICENSE.txt +22 -0
  24. connections_export-0.1.0/connections_export/_licenses/httpcore/LICENSE.md +27 -0
  25. connections_export-0.1.0/connections_export/_licenses/httpx/LICENSE.md +12 -0
  26. connections_export-0.1.0/connections_export/_licenses/idna/LICENSE.md +31 -0
  27. connections_export-0.1.0/connections_export/_licenses/lxml/LICENSE.txt +31 -0
  28. connections_export-0.1.0/connections_export/_licenses/lxml/LICENSES.txt +29 -0
  29. connections_export-0.1.0/connections_export/_licenses/markdownify/LICENSE +21 -0
  30. connections_export-0.1.0/connections_export/_licenses/paged.js/LICENSE.txt +29 -0
  31. connections_export-0.1.0/connections_export/_licenses/pdf.js/LICENSE.txt +193 -0
  32. connections_export-0.1.0/connections_export/_licenses/playwright/LICENSE +202 -0
  33. connections_export-0.1.0/connections_export/_licenses/playwright/NOTICE +5 -0
  34. connections_export-0.1.0/connections_export/_licenses/pydantic/LICENSE +21 -0
  35. connections_export-0.1.0/connections_export/_licenses/pydantic_core/LICENSE +21 -0
  36. connections_export-0.1.0/connections_export/_licenses/pyee/LICENSE +21 -0
  37. connections_export-0.1.0/connections_export/_licenses/pypdf/LICENSE +29 -0
  38. connections_export-0.1.0/connections_export/_licenses/sbom.cyclonedx.json +550 -0
  39. connections_export-0.1.0/connections_export/_licenses/six/LICENSE +18 -0
  40. connections_export-0.1.0/connections_export/_licenses/soupsieve/LICENSE.md +21 -0
  41. connections_export-0.1.0/connections_export/_licenses/starlette/LICENSE.md +27 -0
  42. connections_export-0.1.0/connections_export/_licenses/typing-inspection/LICENSE +21 -0
  43. connections_export-0.1.0/connections_export/_licenses/typing_extensions/LICENSE +279 -0
  44. connections_export-0.1.0/connections_export/_licenses/uvicorn/LICENSE.md +27 -0
  45. connections_export-0.1.0/connections_export/adapters/__init__.py +114 -0
  46. connections_export-0.1.0/connections_export/adapters/atom.py +380 -0
  47. connections_export-0.1.0/connections_export/adapters/blogs.py +279 -0
  48. connections_export-0.1.0/connections_export/adapters/communities.py +120 -0
  49. connections_export-0.1.0/connections_export/adapters/directory.py +97 -0
  50. connections_export-0.1.0/connections_export/adapters/errors.py +33 -0
  51. connections_export-0.1.0/connections_export/adapters/files.py +177 -0
  52. connections_export-0.1.0/connections_export/adapters/forums.py +449 -0
  53. connections_export-0.1.0/connections_export/adapters/model.py +391 -0
  54. connections_export-0.1.0/connections_export/adapters/profiles.py +85 -0
  55. connections_export-0.1.0/connections_export/adapters/rte.py +265 -0
  56. connections_export-0.1.0/connections_export/adapters/search.py +165 -0
  57. connections_export-0.1.0/connections_export/adapters/since.py +110 -0
  58. connections_export-0.1.0/connections_export/adapters/wikis.py +453 -0
  59. connections_export-0.1.0/connections_export/apps.py +174 -0
  60. connections_export-0.1.0/connections_export/archive/__init__.py +11 -0
  61. connections_export-0.1.0/connections_export/archive/blobs.py +42 -0
  62. connections_export-0.1.0/connections_export/archive/feeds.py +60 -0
  63. connections_export-0.1.0/connections_export/archive/records.py +167 -0
  64. connections_export-0.1.0/connections_export/archive/redact.py +40 -0
  65. connections_export-0.1.0/connections_export/archive/source.py +296 -0
  66. connections_export-0.1.0/connections_export/archive/store.py +313 -0
  67. connections_export-0.1.0/connections_export/cli.py +1496 -0
  68. connections_export-0.1.0/connections_export/compare/__init__.py +1 -0
  69. connections_export-0.1.0/connections_export/compare/author_compare.py +131 -0
  70. connections_export-0.1.0/connections_export/config.py +376 -0
  71. connections_export-0.1.0/connections_export/crawler/__init__.py +33 -0
  72. connections_export-0.1.0/connections_export/crawler/apps/__init__.py +27 -0
  73. connections_export-0.1.0/connections_export/crawler/apps/_shared.py +70 -0
  74. connections_export-0.1.0/connections_export/crawler/apps/blogs.py +517 -0
  75. connections_export-0.1.0/connections_export/crawler/apps/community.py +82 -0
  76. connections_export-0.1.0/connections_export/crawler/apps/files.py +201 -0
  77. connections_export-0.1.0/connections_export/crawler/apps/forums.py +517 -0
  78. connections_export-0.1.0/connections_export/crawler/apps/rich_content.py +237 -0
  79. connections_export-0.1.0/connections_export/crawler/apps/wikis.py +561 -0
  80. connections_export-0.1.0/connections_export/crawler/assets.py +227 -0
  81. connections_export-0.1.0/connections_export/crawler/author_plan.py +157 -0
  82. connections_export-0.1.0/connections_export/crawler/community.py +500 -0
  83. connections_export-0.1.0/connections_export/crawler/crawl.py +59 -0
  84. connections_export-0.1.0/connections_export/crawler/dispatch.py +143 -0
  85. connections_export-0.1.0/connections_export/crawler/engine.py +715 -0
  86. connections_export-0.1.0/connections_export/crawler/events.py +311 -0
  87. connections_export-0.1.0/connections_export/crawler/provenance.py +118 -0
  88. connections_export-0.1.0/connections_export/crawler/report.py +48 -0
  89. connections_export-0.1.0/connections_export/crawler/session.py +52 -0
  90. connections_export-0.1.0/connections_export/derive/__init__.py +48 -0
  91. connections_export-0.1.0/connections_export/derive/apps/_shared.py +354 -0
  92. connections_export-0.1.0/connections_export/derive/apps/blogs.py +281 -0
  93. connections_export-0.1.0/connections_export/derive/apps/communities.py +105 -0
  94. connections_export-0.1.0/connections_export/derive/apps/files.py +151 -0
  95. connections_export-0.1.0/connections_export/derive/apps/forums.py +461 -0
  96. connections_export-0.1.0/connections_export/derive/apps/rich_content.py +143 -0
  97. connections_export-0.1.0/connections_export/derive/apps/wikis.py +291 -0
  98. connections_export-0.1.0/connections_export/derive/assemble.py +459 -0
  99. connections_export-0.1.0/connections_export/derive/assets.py +65 -0
  100. connections_export-0.1.0/connections_export/derive/author_filter.py +126 -0
  101. connections_export-0.1.0/connections_export/derive/crosslink.py +103 -0
  102. connections_export-0.1.0/connections_export/derive/index.py +529 -0
  103. connections_export-0.1.0/connections_export/derive/links.py +184 -0
  104. connections_export-0.1.0/connections_export/derive/model.py +477 -0
  105. connections_export-0.1.0/connections_export/derive/scope.py +312 -0
  106. connections_export-0.1.0/connections_export/derive/traverse.py +92 -0
  107. connections_export-0.1.0/connections_export/fakeserver/__init__.py +29 -0
  108. connections_export-0.1.0/connections_export/fakeserver/app.py +1034 -0
  109. connections_export-0.1.0/connections_export/fakeserver/atom.py +1182 -0
  110. connections_export-0.1.0/connections_export/fakeserver/content.py +3464 -0
  111. connections_export-0.1.0/connections_export/fakeserver/faults.py +39 -0
  112. connections_export-0.1.0/connections_export/fakeserver/model.py +540 -0
  113. connections_export-0.1.0/connections_export/fakeserver/navjson.py +122 -0
  114. connections_export-0.1.0/connections_export/fakeserver/prototype.py +815 -0
  115. connections_export-0.1.0/connections_export/fakeserver/searchfeed.py +76 -0
  116. connections_export-0.1.0/connections_export/fakeserver/synth.py +1329 -0
  117. connections_export-0.1.0/connections_export/gui/__init__.py +10 -0
  118. connections_export-0.1.0/connections_export/gui/app.py +220 -0
  119. connections_export-0.1.0/connections_export/gui/archives.py +512 -0
  120. connections_export-0.1.0/connections_export/gui/bridge.py +38 -0
  121. connections_export-0.1.0/connections_export/gui/demo.py +567 -0
  122. connections_export-0.1.0/connections_export/gui/events.py +167 -0
  123. connections_export-0.1.0/connections_export/gui/manual.py +68 -0
  124. connections_export-0.1.0/connections_export/gui/model_source.py +286 -0
  125. connections_export-0.1.0/connections_export/gui/requests.py +179 -0
  126. connections_export-0.1.0/connections_export/gui/routes/_lookup.py +81 -0
  127. connections_export-0.1.0/connections_export/gui/routes/archives.py +552 -0
  128. connections_export-0.1.0/connections_export/gui/routes/lookup.py +578 -0
  129. connections_export-0.1.0/connections_export/gui/routes/model.py +118 -0
  130. connections_export-0.1.0/connections_export/gui/routes/pdf.py +334 -0
  131. connections_export-0.1.0/connections_export/gui/routes/run.py +1161 -0
  132. connections_export-0.1.0/connections_export/gui/routes/settings.py +173 -0
  133. connections_export-0.1.0/connections_export/gui/routes/static.py +208 -0
  134. connections_export-0.1.0/connections_export/gui/security.py +96 -0
  135. connections_export-0.1.0/connections_export/gui/settings_store.py +90 -0
  136. connections_export-0.1.0/connections_export/gui/static/console.css +1598 -0
  137. connections_export-0.1.0/connections_export/gui/static/console.html +658 -0
  138. connections_export-0.1.0/connections_export/gui/static/console.js +7156 -0
  139. connections_export-0.1.0/connections_export/gui/static/vendor/pdf.min.LICENSE +193 -0
  140. connections_export-0.1.0/connections_export/gui/static/vendor/pdf.min.mjs +28 -0
  141. connections_export-0.1.0/connections_export/gui/static/vendor/pdf.worker.min.mjs +28 -0
  142. connections_export-0.1.0/connections_export/gui/static/vendor/pdfjs-boot.mjs +23 -0
  143. connections_export-0.1.0/connections_export/gui/support.py +463 -0
  144. connections_export-0.1.0/connections_export/gui/wiki_url.py +353 -0
  145. connections_export-0.1.0/connections_export/http/__init__.py +32 -0
  146. connections_export-0.1.0/connections_export/http/auth.py +167 -0
  147. connections_export-0.1.0/connections_export/http/client.py +210 -0
  148. connections_export-0.1.0/connections_export/http/results.py +52 -0
  149. connections_export-0.1.0/connections_export/identity.py +26 -0
  150. connections_export-0.1.0/connections_export/ingest/__init__.py +8 -0
  151. connections_export-0.1.0/connections_export/ingest/obsidian.py +419 -0
  152. connections_export-0.1.0/connections_export/interchange/__init__.py +21 -0
  153. connections_export-0.1.0/connections_export/interchange/filenames.py +206 -0
  154. connections_export-0.1.0/connections_export/interchange/manifest.py +218 -0
  155. connections_export-0.1.0/connections_export/interchange/package.py +250 -0
  156. connections_export-0.1.0/connections_export/paging.py +156 -0
  157. connections_export-0.1.0/connections_export/pdf/__init__.py +36 -0
  158. connections_export-0.1.0/connections_export/pdf/browser.py +300 -0
  159. connections_export-0.1.0/connections_export/pdf/browser_fidelity.py +255 -0
  160. connections_export-0.1.0/connections_export/pdf/html.py +1138 -0
  161. connections_export-0.1.0/connections_export/pdf/live.py +329 -0
  162. connections_export-0.1.0/connections_export/pdf/marks.py +350 -0
  163. connections_export-0.1.0/connections_export/pdf/paged.py +309 -0
  164. connections_export-0.1.0/connections_export/pdf/sample.py +75 -0
  165. connections_export-0.1.0/connections_export/pdf/vendor/paged.polyfill.LICENSE +29 -0
  166. connections_export-0.1.0/connections_export/pdf/vendor/paged.polyfill.js +33251 -0
  167. connections_export-0.1.0/connections_export/probes/__init__.py +26 -0
  168. connections_export-0.1.0/connections_export/probes/files_since.py +222 -0
  169. connections_export-0.1.0/connections_export/quick.py +205 -0
  170. connections_export-0.1.0/connections_export/sbom.py +748 -0
  171. connections_export-0.1.0/docs/manual.md +457 -0
  172. connections_export-0.1.0/docs/reference/architecture.md +485 -0
  173. connections_export-0.1.0/docs/reference/interchange-format.md +977 -0
  174. connections_export-0.1.0/justfile +45 -0
  175. connections_export-0.1.0/licenses/overrides/README.md +25 -0
  176. connections_export-0.1.0/pyproject.toml +151 -0
  177. connections_export-0.1.0/tests/__init__.py +0 -0
  178. connections_export-0.1.0/tests/_hostcheck.py +183 -0
  179. connections_export-0.1.0/tests/adapters/__init__.py +0 -0
  180. connections_export-0.1.0/tests/adapters/fixtures/blogs_entries_feed.xml +67 -0
  181. connections_export-0.1.0/tests/adapters/fixtures/blogs_entry_comments_feed.xml +39 -0
  182. connections_export-0.1.0/tests/adapters/fixtures/blogs_list_feed.xml +26 -0
  183. connections_export-0.1.0/tests/adapters/fixtures/forums_replies_feed_flat_chain.xml +58 -0
  184. connections_export-0.1.0/tests/adapters/fixtures/forums_topics_feed.xml +47 -0
  185. connections_export-0.1.0/tests/adapters/fixtures/nav_feed_real_80.json +26 -0
  186. connections_export-0.1.0/tests/adapters/fixtures/nav_feed_verbatim.json +18 -0
  187. connections_export-0.1.0/tests/adapters/fixtures/navigation_entry_verbatim.xml +10 -0
  188. connections_export-0.1.0/tests/adapters/fixtures/page_entry_reference_fields.xml +67 -0
  189. connections_export-0.1.0/tests/adapters/fixtures/page_entry_scheme_sn_type.xml +36 -0
  190. connections_export-0.1.0/tests/adapters/test_atom.py +323 -0
  191. connections_export-0.1.0/tests/adapters/test_blogs.py +170 -0
  192. connections_export-0.1.0/tests/adapters/test_directory.py +74 -0
  193. connections_export-0.1.0/tests/adapters/test_feeds_artifacts.py +139 -0
  194. connections_export-0.1.0/tests/adapters/test_files.py +117 -0
  195. connections_export-0.1.0/tests/adapters/test_forums.py +362 -0
  196. connections_export-0.1.0/tests/adapters/test_hierarchy_sources.py +57 -0
  197. connections_export-0.1.0/tests/adapters/test_model.py +225 -0
  198. connections_export-0.1.0/tests/adapters/test_navigation.py +117 -0
  199. connections_export-0.1.0/tests/adapters/test_page_entry.py +111 -0
  200. connections_export-0.1.0/tests/adapters/test_profiles.py +44 -0
  201. connections_export-0.1.0/tests/adapters/test_resilience.py +90 -0
  202. connections_export-0.1.0/tests/adapters/test_roundtrip.py +145 -0
  203. connections_export-0.1.0/tests/adapters/test_rte.py +222 -0
  204. connections_export-0.1.0/tests/adapters/test_rte_against_fake.py +104 -0
  205. connections_export-0.1.0/tests/adapters/test_rte_layout_shapes.py +138 -0
  206. connections_export-0.1.0/tests/adapters/test_search.py +94 -0
  207. connections_export-0.1.0/tests/adapters/test_since_parameters.py +211 -0
  208. connections_export-0.1.0/tests/adapters/test_subcommunities.py +79 -0
  209. connections_export-0.1.0/tests/adapters/test_urls.py +107 -0
  210. connections_export-0.1.0/tests/archive/__init__.py +0 -0
  211. connections_export-0.1.0/tests/archive/conftest.py +17 -0
  212. connections_export-0.1.0/tests/archive/test_blobs.py +61 -0
  213. connections_export-0.1.0/tests/archive/test_capability_roundtrip.py +162 -0
  214. connections_export-0.1.0/tests/archive/test_feeds.py +86 -0
  215. connections_export-0.1.0/tests/archive/test_open_source.py +77 -0
  216. connections_export-0.1.0/tests/archive/test_records.py +210 -0
  217. connections_export-0.1.0/tests/archive/test_redact.py +67 -0
  218. connections_export-0.1.0/tests/archive/test_report.py +82 -0
  219. connections_export-0.1.0/tests/archive/test_run_metadata.py +50 -0
  220. connections_export-0.1.0/tests/archive/test_run_provenance.py +99 -0
  221. connections_export-0.1.0/tests/archive/test_seen_urls.py +44 -0
  222. connections_export-0.1.0/tests/archive/test_source.py +75 -0
  223. connections_export-0.1.0/tests/archive/test_store.py +211 -0
  224. connections_export-0.1.0/tests/archive/test_zip_source.py +113 -0
  225. connections_export-0.1.0/tests/cli/test_compare_author.py +18 -0
  226. connections_export-0.1.0/tests/cli/test_it_calls_itself_by_its_own_name.py +57 -0
  227. connections_export-0.1.0/tests/cli/test_licenses_cmd.py +53 -0
  228. connections_export-0.1.0/tests/cli/test_open_cmd.py +67 -0
  229. connections_export-0.1.0/tests/cli/test_the_bare_launch_finds_a_port.py +79 -0
  230. connections_export-0.1.0/tests/cli/test_the_commands_agree.py +52 -0
  231. connections_export-0.1.0/tests/cli/test_zip_cli.py +65 -0
  232. connections_export-0.1.0/tests/compare/__init__.py +0 -0
  233. connections_export-0.1.0/tests/compare/test_author_compare.py +127 -0
  234. connections_export-0.1.0/tests/compare/test_search_roundtrip.py +41 -0
  235. connections_export-0.1.0/tests/config/__init__.py +0 -0
  236. connections_export-0.1.0/tests/config/test_cli.py +90 -0
  237. connections_export-0.1.0/tests/config/test_discovery.py +96 -0
  238. connections_export-0.1.0/tests/config/test_env_parsing.py +138 -0
  239. connections_export-0.1.0/tests/config/test_example_toml.py +56 -0
  240. connections_export-0.1.0/tests/config/test_model.py +130 -0
  241. connections_export-0.1.0/tests/config/test_precedence.py +86 -0
  242. connections_export-0.1.0/tests/config/test_secrets.py +121 -0
  243. connections_export-0.1.0/tests/config/test_the_sample_config_matches_the_defaults.py +72 -0
  244. connections_export-0.1.0/tests/conftest.py +50 -0
  245. connections_export-0.1.0/tests/crawler/__init__.py +0 -0
  246. connections_export-0.1.0/tests/crawler/conftest.py +113 -0
  247. connections_export-0.1.0/tests/crawler/test_assets.py +152 -0
  248. connections_export-0.1.0/tests/crawler/test_assets_in_crawl.py +335 -0
  249. connections_export-0.1.0/tests/crawler/test_author_plan.py +127 -0
  250. connections_export-0.1.0/tests/crawler/test_author_two_pass.py +293 -0
  251. connections_export-0.1.0/tests/crawler/test_blog_comment_index.py +320 -0
  252. connections_export-0.1.0/tests/crawler/test_blogs_forums.py +578 -0
  253. connections_export-0.1.0/tests/crawler/test_cache_policy.py +183 -0
  254. connections_export-0.1.0/tests/crawler/test_cli.py +197 -0
  255. connections_export-0.1.0/tests/crawler/test_community_discovery.py +197 -0
  256. connections_export-0.1.0/tests/crawler/test_community_logo_capture.py +114 -0
  257. connections_export-0.1.0/tests/crawler/test_dispatch.py +154 -0
  258. connections_export-0.1.0/tests/crawler/test_events.py +126 -0
  259. connections_export-0.1.0/tests/crawler/test_every_capture_can_anchor_an_update.py +112 -0
  260. connections_export-0.1.0/tests/crawler/test_every_discovery_concludes.py +105 -0
  261. connections_export-0.1.0/tests/crawler/test_failures_and_retries.py +126 -0
  262. connections_export-0.1.0/tests/crawler/test_fetch_modes.py +53 -0
  263. connections_export-0.1.0/tests/crawler/test_file_link_document_id.py +42 -0
  264. connections_export-0.1.0/tests/crawler/test_file_link_scan.py +63 -0
  265. connections_export-0.1.0/tests/crawler/test_files_author_filter.py +121 -0
  266. connections_export-0.1.0/tests/crawler/test_files_crawl.py +140 -0
  267. connections_export-0.1.0/tests/crawler/test_into_archive.py +89 -0
  268. connections_export-0.1.0/tests/crawler/test_into_archive_end_to_end.py +92 -0
  269. connections_export-0.1.0/tests/crawler/test_linked_file_capture.py +111 -0
  270. connections_export-0.1.0/tests/crawler/test_pagination.py +417 -0
  271. connections_export-0.1.0/tests/crawler/test_recheck_comments.py +173 -0
  272. connections_export-0.1.0/tests/crawler/test_report.py +54 -0
  273. connections_export-0.1.0/tests/crawler/test_report_orphans.py +106 -0
  274. connections_export-0.1.0/tests/crawler/test_resumability.py +86 -0
  275. connections_export-0.1.0/tests/crawler/test_roundtrip.py +526 -0
  276. connections_export-0.1.0/tests/crawler/test_rte_crawl.py +170 -0
  277. connections_export-0.1.0/tests/crawler/test_run_provenance.py +126 -0
  278. connections_export-0.1.0/tests/crawler/test_run_provenance_written.py +200 -0
  279. connections_export-0.1.0/tests/crawler/test_scoping.py +123 -0
  280. connections_export-0.1.0/tests/crawler/test_stop_ends_the_run.py +130 -0
  281. connections_export-0.1.0/tests/crawler/test_traversal.py +130 -0
  282. connections_export-0.1.0/tests/crawler/test_update_cutoff_is_real.py +84 -0
  283. connections_export-0.1.0/tests/crawler/test_update_finds_new_pages.py +251 -0
  284. connections_export-0.1.0/tests/crawler/test_update_run.py +339 -0
  285. connections_export-0.1.0/tests/crawler/test_update_sees_every_change.py +383 -0
  286. connections_export-0.1.0/tests/crawler/test_version_policy.py +127 -0
  287. connections_export-0.1.0/tests/crawler/test_wiki_community_attribution.py +66 -0
  288. connections_export-0.1.0/tests/crawler/test_wiki_page_index.py +145 -0
  289. connections_export-0.1.0/tests/derive/__init__.py +0 -0
  290. connections_export-0.1.0/tests/derive/test_assemble.py +594 -0
  291. connections_export-0.1.0/tests/derive/test_assets.py +135 -0
  292. connections_export-0.1.0/tests/derive/test_author_filter.py +131 -0
  293. connections_export-0.1.0/tests/derive/test_blogs_forums_assemble.py +397 -0
  294. connections_export-0.1.0/tests/derive/test_blogs_forums_model.py +87 -0
  295. connections_export-0.1.0/tests/derive/test_blogs_forums_roundtrip.py +233 -0
  296. connections_export-0.1.0/tests/derive/test_community_logo.py +78 -0
  297. connections_export-0.1.0/tests/derive/test_crosslink.py +126 -0
  298. connections_export-0.1.0/tests/derive/test_crosslink_files.py +69 -0
  299. connections_export-0.1.0/tests/derive/test_file_absence_reason.py +84 -0
  300. connections_export-0.1.0/tests/derive/test_file_link_resolution.py +62 -0
  301. connections_export-0.1.0/tests/derive/test_files_derive.py +126 -0
  302. connections_export-0.1.0/tests/derive/test_highlights_links_resolve.py +108 -0
  303. connections_export-0.1.0/tests/derive/test_index.py +178 -0
  304. connections_export-0.1.0/tests/derive/test_index_source.py +40 -0
  305. connections_export-0.1.0/tests/derive/test_links.py +182 -0
  306. connections_export-0.1.0/tests/derive/test_model.py +276 -0
  307. connections_export-0.1.0/tests/derive/test_pagination_cap.py +151 -0
  308. connections_export-0.1.0/tests/derive/test_paginator_contract.py +162 -0
  309. connections_export-0.1.0/tests/derive/test_phased_crosslinks.py +154 -0
  310. connections_export-0.1.0/tests/derive/test_reconstructed_reads_announce_themselves.py +230 -0
  311. connections_export-0.1.0/tests/derive/test_replays_the_recorded_walk.py +138 -0
  312. connections_export-0.1.0/tests/derive/test_roundtrip.py +254 -0
  313. connections_export-0.1.0/tests/derive/test_rte_derive.py +108 -0
  314. connections_export-0.1.0/tests/derive/test_run_provenance.py +155 -0
  315. connections_export-0.1.0/tests/derive/test_scope.py +166 -0
  316. connections_export-0.1.0/tests/derive/test_scope_files.py +109 -0
  317. connections_export-0.1.0/tests/derive/test_scope_rich_content_page.py +105 -0
  318. connections_export-0.1.0/tests/derive/test_scoped_derive.py +91 -0
  319. connections_export-0.1.0/tests/derive/test_select_scope.py +112 -0
  320. connections_export-0.1.0/tests/derive/test_self_describing.py +148 -0
  321. connections_export-0.1.0/tests/derive/test_traverse.py +38 -0
  322. connections_export-0.1.0/tests/derive/test_update_pages_are_read_back.py +92 -0
  323. connections_export-0.1.0/tests/derive/test_zip_parity.py +63 -0
  324. connections_export-0.1.0/tests/derive/test_zip_without_feeds_record.py +88 -0
  325. connections_export-0.1.0/tests/fakeserver/__init__.py +0 -0
  326. connections_export-0.1.0/tests/fakeserver/conftest.py +20 -0
  327. connections_export-0.1.0/tests/fakeserver/test_app.py +229 -0
  328. connections_export-0.1.0/tests/fakeserver/test_atom.py +208 -0
  329. connections_export-0.1.0/tests/fakeserver/test_blogs_forums.py +699 -0
  330. connections_export-0.1.0/tests/fakeserver/test_body_html.py +44 -0
  331. connections_export-0.1.0/tests/fakeserver/test_capability_integration.py +115 -0
  332. connections_export-0.1.0/tests/fakeserver/test_faults.py +125 -0
  333. connections_export-0.1.0/tests/fakeserver/test_model.py +172 -0
  334. connections_export-0.1.0/tests/fakeserver/test_navjson.py +89 -0
  335. connections_export-0.1.0/tests/fakeserver/test_one_person_one_id.py +182 -0
  336. connections_export-0.1.0/tests/fakeserver/test_prototype.py +153 -0
  337. connections_export-0.1.0/tests/fakeserver/test_since_routes.py +131 -0
  338. connections_export-0.1.0/tests/fakeserver/test_subcommunities_route.py +65 -0
  339. connections_export-0.1.0/tests/fakeserver/test_synth.py +231 -0
  340. connections_export-0.1.0/tests/fakeserver/test_visible_wave.py +118 -0
  341. connections_export-0.1.0/tests/fakeserver/test_waves.py +82 -0
  342. connections_export-0.1.0/tests/gui/__init__.py +0 -0
  343. connections_export-0.1.0/tests/gui/_served_assets.py +48 -0
  344. connections_export-0.1.0/tests/gui/test_a_run_ends_once.py +98 -0
  345. connections_export-0.1.0/tests/gui/test_app.py +323 -0
  346. connections_export-0.1.0/tests/gui/test_archive_ledger.py +101 -0
  347. connections_export-0.1.0/tests/gui/test_archive_naming.py +135 -0
  348. connections_export-0.1.0/tests/gui/test_archive_only_is_reachable.py +201 -0
  349. connections_export-0.1.0/tests/gui/test_archive_only_mode.py +157 -0
  350. connections_export-0.1.0/tests/gui/test_archive_rename.py +92 -0
  351. connections_export-0.1.0/tests/gui/test_archives.py +122 -0
  352. connections_export-0.1.0/tests/gui/test_archives_base_config.py +60 -0
  353. connections_export-0.1.0/tests/gui/test_archives_drop_zone.py +57 -0
  354. connections_export-0.1.0/tests/gui/test_archives_location.py +95 -0
  355. connections_export-0.1.0/tests/gui/test_author_filter_wiring.py +71 -0
  356. connections_export-0.1.0/tests/gui/test_author_picker_merges.py +126 -0
  357. connections_export-0.1.0/tests/gui/test_author_picker_scrolls.py +59 -0
  358. connections_export-0.1.0/tests/gui/test_capture_collision.py +80 -0
  359. connections_export-0.1.0/tests/gui/test_cli_default_launch.py +123 -0
  360. connections_export-0.1.0/tests/gui/test_cli_echo.py +145 -0
  361. connections_export-0.1.0/tests/gui/test_cli_serve.py +123 -0
  362. connections_export-0.1.0/tests/gui/test_console_real_update.py +102 -0
  363. connections_export-0.1.0/tests/gui/test_demo.py +167 -0
  364. connections_export-0.1.0/tests/gui/test_demo_chip_ingest.py +248 -0
  365. connections_export-0.1.0/tests/gui/test_demo_community.py +222 -0
  366. connections_export-0.1.0/tests/gui/test_demo_discovery_parity.py +117 -0
  367. connections_export-0.1.0/tests/gui/test_demo_files.py +96 -0
  368. connections_export-0.1.0/tests/gui/test_demo_rich_content.py +76 -0
  369. connections_export-0.1.0/tests/gui/test_demo_scoping.py +85 -0
  370. connections_export-0.1.0/tests/gui/test_demo_update.py +73 -0
  371. connections_export-0.1.0/tests/gui/test_demo_urls.py +181 -0
  372. connections_export-0.1.0/tests/gui/test_drawer_content_after_a_run_finishes.py +51 -0
  373. connections_export-0.1.0/tests/gui/test_drop_an_archive.py +51 -0
  374. connections_export-0.1.0/tests/gui/test_drop_url_vs_archive.py +44 -0
  375. connections_export-0.1.0/tests/gui/test_event_serialization.py +63 -0
  376. connections_export-0.1.0/tests/gui/test_events.py +242 -0
  377. connections_export-0.1.0/tests/gui/test_feed_info.py +178 -0
  378. connections_export-0.1.0/tests/gui/test_file_link_rendering_static.py +29 -0
  379. connections_export-0.1.0/tests/gui/test_files_surfaces.py +115 -0
  380. connections_export-0.1.0/tests/gui/test_identity_chip.py +109 -0
  381. connections_export-0.1.0/tests/gui/test_in_export_links_reach_every_component.py +52 -0
  382. connections_export-0.1.0/tests/gui/test_ledger_endpoint.py +124 -0
  383. connections_export-0.1.0/tests/gui/test_ledger_static.py +144 -0
  384. connections_export-0.1.0/tests/gui/test_licenses_page.py +167 -0
  385. connections_export-0.1.0/tests/gui/test_live_endpoints.py +101 -0
  386. connections_export-0.1.0/tests/gui/test_live_ingest_parity_static.py +202 -0
  387. connections_export-0.1.0/tests/gui/test_live_model.py +141 -0
  388. connections_export-0.1.0/tests/gui/test_live_reader_static.py +70 -0
  389. connections_export-0.1.0/tests/gui/test_live_tree_groups_by_community.py +52 -0
  390. connections_export-0.1.0/tests/gui/test_manual.py +108 -0
  391. connections_export-0.1.0/tests/gui/test_manual_documents_the_cli.py +52 -0
  392. connections_export-0.1.0/tests/gui/test_manual_interchange_spec.py +147 -0
  393. connections_export-0.1.0/tests/gui/test_manual_source.py +58 -0
  394. connections_export-0.1.0/tests/gui/test_model_endpoints.py +349 -0
  395. connections_export-0.1.0/tests/gui/test_model_source.py +243 -0
  396. connections_export-0.1.0/tests/gui/test_multi_app_setup.py +120 -0
  397. connections_export-0.1.0/tests/gui/test_multi_community_capture.py +99 -0
  398. connections_export-0.1.0/tests/gui/test_multi_community_picker_static.py +97 -0
  399. connections_export-0.1.0/tests/gui/test_multi_community_request.py +73 -0
  400. connections_export-0.1.0/tests/gui/test_open_archive_api.py +352 -0
  401. connections_export-0.1.0/tests/gui/test_open_external_archive.py +203 -0
  402. connections_export-0.1.0/tests/gui/test_overview_actions.py +55 -0
  403. connections_export-0.1.0/tests/gui/test_paste_does_not_duplicate.py +133 -0
  404. connections_export-0.1.0/tests/gui/test_pdf_appearance.py +136 -0
  405. connections_export-0.1.0/tests/gui/test_pdf_endpoint.py +243 -0
  406. connections_export-0.1.0/tests/gui/test_reader_blogs_forums_static.py +218 -0
  407. connections_export-0.1.0/tests/gui/test_reader_community_grouping_static.py +57 -0
  408. connections_export-0.1.0/tests/gui/test_reader_files_static.py +95 -0
  409. connections_export-0.1.0/tests/gui/test_reader_nav_stays_reachable.py +162 -0
  410. connections_export-0.1.0/tests/gui/test_reader_pure_js.py +445 -0
  411. connections_export-0.1.0/tests/gui/test_reader_rich_content_static.py +152 -0
  412. connections_export-0.1.0/tests/gui/test_reader_static.py +262 -0
  413. connections_export-0.1.0/tests/gui/test_recent_archives_cap.py +66 -0
  414. connections_export-0.1.0/tests/gui/test_rich_content_preview_endpoint.py +92 -0
  415. connections_export-0.1.0/tests/gui/test_saved_delay_reaches_the_run.py +111 -0
  416. connections_export-0.1.0/tests/gui/test_security.py +161 -0
  417. connections_export-0.1.0/tests/gui/test_select_screen_while_running.py +58 -0
  418. connections_export-0.1.0/tests/gui/test_settings_panel.py +152 -0
  419. connections_export-0.1.0/tests/gui/test_settings_persist.py +79 -0
  420. connections_export-0.1.0/tests/gui/test_setup_screen.py +211 -0
  421. connections_export-0.1.0/tests/gui/test_shell_static.py +963 -0
  422. connections_export-0.1.0/tests/gui/test_static_assets.py +56 -0
  423. connections_export-0.1.0/tests/gui/test_subcommunities_endpoint.py +63 -0
  424. connections_export-0.1.0/tests/gui/test_superseded_archives.py +94 -0
  425. connections_export-0.1.0/tests/gui/test_the_demo_is_signed_in.py +133 -0
  426. connections_export-0.1.0/tests/gui/test_throughput_is_honest.py +68 -0
  427. connections_export-0.1.0/tests/gui/test_update_counts_are_honest.py +89 -0
  428. connections_export-0.1.0/tests/gui/test_update_never_becomes_a_demo.py +168 -0
  429. connections_export-0.1.0/tests/gui/test_update_stays_inside_the_archive.py +96 -0
  430. connections_export-0.1.0/tests/gui/test_wiki_url.py +173 -0
  431. connections_export-0.1.0/tests/gui/test_zip_archives_listing.py +83 -0
  432. connections_export-0.1.0/tests/gui/test_zip_is_read_only.py +143 -0
  433. connections_export-0.1.0/tests/gui/test_zip_model_source.py +96 -0
  434. connections_export-0.1.0/tests/http/__init__.py +0 -0
  435. connections_export-0.1.0/tests/http/conftest.py +39 -0
  436. connections_export-0.1.0/tests/http/test_auth.py +96 -0
  437. connections_export-0.1.0/tests/http/test_capability_integration.py +63 -0
  438. connections_export-0.1.0/tests/http/test_cookies.py +24 -0
  439. connections_export-0.1.0/tests/http/test_fetch.py +46 -0
  440. connections_export-0.1.0/tests/http/test_results.py +105 -0
  441. connections_export-0.1.0/tests/http/test_retry.py +212 -0
  442. connections_export-0.1.0/tests/http/test_throttle.py +85 -0
  443. connections_export-0.1.0/tests/http/test_tls.py +45 -0
  444. connections_export-0.1.0/tests/ingest/__init__.py +0 -0
  445. connections_export-0.1.0/tests/ingest/test_obsidian.py +241 -0
  446. connections_export-0.1.0/tests/interchange/__init__.py +0 -0
  447. connections_export-0.1.0/tests/interchange/test_blogs_forums_package.py +105 -0
  448. connections_export-0.1.0/tests/interchange/test_end_to_end.py +141 -0
  449. connections_export-0.1.0/tests/interchange/test_filenames.py +185 -0
  450. connections_export-0.1.0/tests/interchange/test_files_package.py +133 -0
  451. connections_export-0.1.0/tests/interchange/test_hcl_agnostic.py +122 -0
  452. connections_export-0.1.0/tests/interchange/test_it_names_where_it_came_from.py +134 -0
  453. connections_export-0.1.0/tests/interchange/test_manifest.py +266 -0
  454. connections_export-0.1.0/tests/interchange/test_manifest_counts_all_apps.py +103 -0
  455. connections_export-0.1.0/tests/interchange/test_package.py +286 -0
  456. connections_export-0.1.0/tests/interchange/test_package_declares_what_it_contains.py +104 -0
  457. connections_export-0.1.0/tests/pdf/__init__.py +0 -0
  458. connections_export-0.1.0/tests/pdf/test_blogs_forums.py +244 -0
  459. connections_export-0.1.0/tests/pdf/test_browser.py +65 -0
  460. connections_export-0.1.0/tests/pdf/test_browser_fidelity.py +97 -0
  461. connections_export-0.1.0/tests/pdf/test_browser_launch.py +73 -0
  462. connections_export-0.1.0/tests/pdf/test_browser_quiet.py +36 -0
  463. connections_export-0.1.0/tests/pdf/test_cli_pdf.py +58 -0
  464. connections_export-0.1.0/tests/pdf/test_end_to_end.py +86 -0
  465. connections_export-0.1.0/tests/pdf/test_files_render.py +139 -0
  466. connections_export-0.1.0/tests/pdf/test_html.py +527 -0
  467. connections_export-0.1.0/tests/pdf/test_links_showcase.py +47 -0
  468. connections_export-0.1.0/tests/pdf/test_live.py +29 -0
  469. connections_export-0.1.0/tests/pdf/test_live_merge.py +106 -0
  470. connections_export-0.1.0/tests/pdf/test_marks.py +90 -0
  471. connections_export-0.1.0/tests/pdf/test_marks_logo.py +147 -0
  472. connections_export-0.1.0/tests/pdf/test_marks_surface.py +191 -0
  473. connections_export-0.1.0/tests/pdf/test_paged.py +93 -0
  474. connections_export-0.1.0/tests/pdf/test_rich_content_render.py +103 -0
  475. connections_export-0.1.0/tests/pdf/test_style_surface.py +65 -0
  476. connections_export-0.1.0/tests/pdf/test_template.py +356 -0
  477. connections_export-0.1.0/tests/probes/__init__.py +0 -0
  478. connections_export-0.1.0/tests/probes/test_files_since.py +180 -0
  479. connections_export-0.1.0/tests/sbom/__init__.py +0 -0
  480. connections_export-0.1.0/tests/sbom/test_a_package_with_no_code_needs_no_licence.py +71 -0
  481. connections_export-0.1.0/tests/sbom/test_license_gaps.py +130 -0
  482. connections_export-0.1.0/tests/sbom/test_sbom.py +252 -0
  483. connections_export-0.1.0/tests/sbom/test_the_executable_carries_its_documents.py +80 -0
  484. connections_export-0.1.0/tests/sbom/test_vendored_assets.py +145 -0
  485. connections_export-0.1.0/tests/test_apps_registry.py +117 -0
  486. connections_export-0.1.0/tests/test_identity.py +38 -0
  487. connections_export-0.1.0/tests/test_import_every_module.py +53 -0
  488. connections_export-0.1.0/tests/test_it_compiles_without_warnings.py +42 -0
  489. connections_export-0.1.0/tests/test_quick.py +107 -0
  490. connections_export-0.1.0/tests/test_readme_links.py +89 -0
  491. connections_export-0.1.0/tests/test_the_suite_leaves_the_user_alone.py +49 -0
  492. connections_export-0.1.0/tools/build_binary.py +346 -0
  493. connections_export-0.1.0/tools/write_license_bundle.py +58 -0
  494. connections_export-0.1.0/uv.lock +1079 -0
@@ -0,0 +1,144 @@
1
+ name: Binaries
2
+
3
+ # Single-file executables for people who will not install Python, attached to
4
+ # the GitHub Release on `v*`.
5
+ #
6
+ # When they are built depends on what it costs. Four platform builds -- two of
7
+ # them macOS -- is the most expensive thing here, and on a private repository
8
+ # macOS bills at 10x, so there they run on a tag or on request only. On a
9
+ # public repository standard runners are free, so they run on every push to
10
+ # main as well: a packaging fault found on a push is found before a tag exists,
11
+ # and a tag is a bad place to discover one.
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ tags: ["v*"]
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ build:
23
+ # Free on a public repo, so every push; metered on a private one, so tags
24
+ # and explicit requests only.
25
+ if: >-
26
+ github.event.repository.private == false
27
+ || startsWith(github.ref, 'refs/tags/v')
28
+ || github.event_name == 'workflow_dispatch'
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ include:
33
+ - os: ubuntu-latest
34
+ target: linux-x86_64
35
+ - os: macos-latest
36
+ target: macos-arm64
37
+ # Intel macOS. NOT `macos-13` -- GitHub retired those runners, and a
38
+ # retired label does not fail, it queues forever: the first two runs
39
+ # here sat unassigned for over an hour while the other three
40
+ # platforms had long since finished, and the run never reported.
41
+ - os: macos-15-intel
42
+ target: macos-x86_64
43
+ - os: windows-latest
44
+ target: windows-x86_64
45
+ runs-on: ${{ matrix.os }}
46
+ # A PyInstaller build is minutes, not tens of minutes. The cap turns a hung
47
+ # or unservable cell into a failure someone can see.
48
+ timeout-minutes: 30
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: astral-sh/setup-uv@v5
52
+ with:
53
+ # PyInstaller embeds the interpreter it builds against, so the
54
+ # executable IS this Python version. Pinned rather than matrixed:
55
+ # one binary per platform, not one per Python.
56
+ python-version: "3.13"
57
+ enable-cache: true
58
+
59
+ # `requests-kerberos` pulls `krb5`, which builds against system Kerberos
60
+ # headers that the Linux runners do not ship -- `--all-extras` therefore
61
+ # fails at resolve time on Linux alone. Same wall a Linux user installing
62
+ # `connections-export[sspi]` hits, so the fix belongs in the docs too.
63
+ - name: Kerberos headers (Linux only, for the sspi extra)
64
+ if: runner.os == 'Linux'
65
+ run: sudo apt-get update -qq && sudo apt-get install -y -qq libkrb5-dev
66
+
67
+ # `--all-extras` is what puts markdownify in the environment, and so in
68
+ # the bundle: the Obsidian ingester imports it lazily, which means a
69
+ # bundle built without it still starts and still passes `--help`.
70
+ # build_binary.py asserts the module is present rather than trusting it.
71
+ - name: Build executable
72
+ run: uv run --all-extras --python 3.13 python tools/build_binary.py --dist dist
73
+
74
+ # The point of the executable is that a user with no Python and no
75
+ # install can run it, so the smoke test uses the binary ALONE -- no uv,
76
+ # no source, no helper script. `--demo` renders the synthetic deployment
77
+ # that ships inside it; PDF rendering is the one thing that reaches
78
+ # outside, for a Chromium-based browser. On Windows that is the
79
+ # preinstalled Edge, which is the path most users will take.
80
+ - name: Smoke-test the executable, standalone
81
+ shell: bash
82
+ run: |
83
+ set -euo pipefail
84
+ exe="dist/connections-export"
85
+ [ "${{ runner.os }}" = "Windows" ] && exe="dist/connections-export.exe"
86
+ "$exe" --help > /dev/null
87
+ "$exe" crawl --demo --output-dir demo-archive
88
+ "$exe" pdf --demo --output smoke.pdf
89
+ # A PDF header and a plausible size: the demo set renders ~100 pages,
90
+ # so anything tiny means it rendered nothing and still exited 0.
91
+ head -c 5 smoke.pdf | grep -q '%PDF' || { echo "not a PDF"; exit 1; }
92
+ size=$(wc -c < smoke.pdf)
93
+ echo "smoke.pdf: $size bytes on ${{ matrix.target }}"
94
+ [ "$size" -gt 100000 ] || { echo "PDF suspiciously small"; exit 1; }
95
+ rm -rf demo-archive smoke.pdf
96
+
97
+ - name: Name the artifact after the version and platform
98
+ shell: bash
99
+ run: |
100
+ # `uv run --no-project`, not bare `python`: setup-uv provisions a
101
+ # managed interpreter without guaranteeing `python` on PATH, and
102
+ # --no-project skips a pointless re-sync just to read a TOML key.
103
+ version="$(uv run --no-project python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
104
+ src="dist/connections-export"; ext=""
105
+ if [ "${{ runner.os }}" = "Windows" ]; then src="dist/connections-export.exe"; ext=".exe"; fi
106
+ mv "$src" "dist/connections-export-${version}-${{ matrix.target }}${ext}"
107
+ ls -la dist/
108
+
109
+ - uses: actions/upload-artifact@v4
110
+ with:
111
+ name: connections-export-${{ matrix.target }}
112
+ path: dist/connections-export-*
113
+ if-no-files-found: error
114
+ # Four executables are ~290 MB a run, and GitHub keeps artifacts for
115
+ # 90 days by default. On a PRIVATE repo that is metered storage: seven
116
+ # runs had accumulated 2 GB against a 500 MB allowance. These are
117
+ # disposable -- rebuildable from any tag -- and on a `v*` tag the
118
+ # release assets are the copy that lasts.
119
+ retention-days: 7
120
+
121
+ attach-to-release:
122
+ if: startsWith(github.ref, 'refs/tags/v')
123
+ needs: build
124
+ runs-on: ubuntu-latest
125
+ permissions:
126
+ contents: write
127
+ steps:
128
+ - uses: actions/download-artifact@v4
129
+ with:
130
+ path: dist/
131
+ merge-multiple: true
132
+ # `gh release upload` fails if no Release object exists, and release.yml
133
+ # only publishes to PyPI -- pushing a tag does not create one. Create it
134
+ # if absent, then upload; `--clobber` makes a re-run idempotent.
135
+ - name: Attach executables to the release
136
+ env:
137
+ GH_TOKEN: ${{ github.token }}
138
+ run: |
139
+ gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 \
140
+ || gh release create "$GITHUB_REF_NAME" \
141
+ --repo "$GITHUB_REPOSITORY" \
142
+ --title "$GITHUB_REF_NAME" \
143
+ --generate-notes
144
+ gh release upload "$GITHUB_REF_NAME" dist/* --clobber --repo "$GITHUB_REPOSITORY"
@@ -0,0 +1,82 @@
1
+ name: CI
2
+
3
+ # How much of the matrix runs depends on whether the repository is private,
4
+ # because that is what decides whether it costs anything.
5
+ #
6
+ # public -> everything, on every push. GitHub-hosted standard runners are
7
+ # free for public repositories, so there is no reason to prove
8
+ # less than the whole matrix.
9
+ # private -> Linux only on a push; the full 3 OS x 3 Python matrix on a tag
10
+ # or on request. Minutes are metered there, and metered unevenly:
11
+ # macOS bills at 10x and Windows at 2x, so a nine-cell matrix on
12
+ # every push costs roughly 350 metered minutes and exhausts a
13
+ # modest allowance in a day.
14
+ #
15
+ # Linux alone catches most of it -- a dependency without wheels for a new
16
+ # Python fails at resolve time on any OS -- but the claim that `requires-python`
17
+ # is honoured can only be proved by running there.
18
+ on:
19
+ push:
20
+ branches: [main]
21
+ tags: ["v*"]
22
+ pull_request:
23
+ workflow_dispatch:
24
+
25
+ jobs:
26
+ test:
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ python: ["3.12", "3.13", "3.14"]
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: astral-sh/setup-uv@v5
35
+ with:
36
+ python-version: ${{ matrix.python }}
37
+ enable-cache: true
38
+ # `requests-kerberos` pulls `krb5`, which builds against system Kerberos
39
+ # headers that the Linux runners do not ship -- `--all-extras` therefore
40
+ # fails at resolve time on Linux alone. Same wall a Linux user installing
41
+ # `connections-export[sspi]` hits, so the fix belongs in the docs too.
42
+ - name: Kerberos headers (Linux only, for the sspi extra)
43
+ if: runner.os == 'Linux'
44
+ run: sudo apt-get update -qq && sudo apt-get install -y -qq libkrb5-dev
45
+
46
+ # No Playwright browsers installed on purpose: the suite runs offline
47
+ # against the in-process fake server; the ~2 browser-PDF tests skip.
48
+ - name: Lint
49
+ run: |
50
+ uv run --all-extras ruff check .
51
+ uv run --all-extras ruff format --check .
52
+ - name: Tests
53
+ run: uv run --all-extras pytest
54
+
55
+ # The expensive half: only before a release, or when asked. macOS bills at
56
+ # 10x and Windows at 2x, so this job costs more than everything else in the
57
+ # repo combined -- which is exactly why it does not run on every push.
58
+ cross-platform:
59
+ # Free on a public repo, so always; metered on a private one, so only
60
+ # when it is worth paying for.
61
+ if: >-
62
+ github.event.repository.private == false
63
+ || startsWith(github.ref, 'refs/tags/v')
64
+ || github.event_name == 'workflow_dispatch'
65
+ strategy:
66
+ fail-fast: false
67
+ matrix:
68
+ os: [macos-latest, windows-latest]
69
+ python: ["3.12", "3.13", "3.14"]
70
+ runs-on: ${{ matrix.os }}
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: astral-sh/setup-uv@v5
74
+ with:
75
+ python-version: ${{ matrix.python }}
76
+ enable-cache: true
77
+ - name: Lint
78
+ run: |
79
+ uv run --all-extras ruff check .
80
+ uv run --all-extras ruff format --check .
81
+ - name: Tests
82
+ run: uv run --all-extras pytest
@@ -0,0 +1,181 @@
1
+ name: Packages
2
+
3
+ # Wheel + sdist on every push to main, so staging proves the distributions the
4
+ # same way it proves the executables -- and `twine check` renders the README the
5
+ # way PyPI will, which is the one packaging fault a local install cannot show.
6
+ #
7
+ # The TestPyPI upload below is deliberately DORMANT: it never runs on a push,
8
+ # only when someone dispatches it by hand with publish=true. Before it can work,
9
+ # two things must exist (neither is created by this file):
10
+ # * a TestPyPI Trusted Publisher for this owner/repo naming `packages.yml`
11
+ # and the `testpypi` environment
12
+ # * a repository Environment named `testpypi`
13
+ on:
14
+ push:
15
+ branches: [main]
16
+ tags: ["v*"]
17
+ workflow_dispatch:
18
+ inputs:
19
+ publish:
20
+ description: "Upload the built distributions to TestPyPI"
21
+ type: boolean
22
+ default: false
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ jobs:
28
+ build:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: astral-sh/setup-uv@v5
33
+ with:
34
+ python-version: "3.13"
35
+ enable-cache: true
36
+
37
+ - name: Build wheel and sdist
38
+ run: uv build --out-dir dist
39
+
40
+ # A package index renders the README as the project description. A
41
+ # malformed one is rejected at upload, or silently renders wrong, and a
42
+ # relative link that resolves in a checkout may not resolve there.
43
+ - name: Validate metadata and description rendering
44
+ run: uvx twine check --strict dist/*
45
+
46
+ - name: Report what was built
47
+ run: ls -l dist/
48
+
49
+ - uses: actions/upload-artifact@v4
50
+ with:
51
+ name: distributions
52
+ path: dist/
53
+ if-no-files-found: error
54
+ # Small (~2 MB) but uploaded on every push; no reason to keep 90 days
55
+ # of them on metered storage.
56
+ retention-days: 7
57
+
58
+ # What only a real install can show. The tests run against the source tree,
59
+ # where the working copy has answered every question the same way for so long
60
+ # that some of them stopped being asked: bytecode is cached, the repository is
61
+ # on `sys.path`, and a data file is found because `docs/` happens to be next
62
+ # door. A fresh venv with nothing but the wheel in it has none of that.
63
+ #
64
+ # Runs on every push, before the dormant upload below, so a fault of this kind
65
+ # is found while the version number is still free to reuse.
66
+ smoke:
67
+ needs: build
68
+ runs-on: ubuntu-latest
69
+ steps:
70
+ - uses: astral-sh/setup-uv@v5
71
+ with:
72
+ python-version: "3.13"
73
+ - uses: actions/download-artifact@v4
74
+ with:
75
+ name: distributions
76
+ path: dist/
77
+
78
+ - name: Install the built wheel into an empty environment
79
+ run: |
80
+ uv venv /tmp/smoke
81
+ VIRTUAL_ENV=/tmp/smoke uv pip install dist/*.whl
82
+
83
+ # Deliberately not from the repository root: Python puts the working
84
+ # directory on `sys.path`, so a check run there imports the checkout and
85
+ # the wheel is never touched.
86
+ - name: It runs, and says nothing it was not asked to say
87
+ working-directory: /tmp
88
+ run: |
89
+ set -euo pipefail
90
+ /tmp/smoke/bin/connections-export --help >/tmp/out.txt 2>/tmp/err.txt
91
+ echo "--- stdout ---"; cat /tmp/out.txt
92
+ if [ -s /tmp/err.txt ]; then
93
+ echo "::error::the console script wrote to stderr; a user sees this"
94
+ cat /tmp/err.txt
95
+ exit 1
96
+ fi
97
+ if grep -qi stub /tmp/out.txt; then
98
+ echo "::error::a stub command is advertised in the public help"
99
+ exit 1
100
+ fi
101
+
102
+ # A SyntaxWarning is raised once, when the `.pyc` is written -- long ago
103
+ # in a working tree, and on a new install either during it or on stderr of
104
+ # every command until a cache warms. Compiling on purpose catches both.
105
+ - name: Every installed module compiles without warnings
106
+ working-directory: /tmp
107
+ run: |
108
+ SITE=$(/tmp/smoke/bin/python -P -c \
109
+ "import connections_export,pathlib;print(pathlib.Path(connections_export.__file__).parent)")
110
+ /tmp/smoke/bin/python -W error::SyntaxWarning -m compileall -q -f -x vendor "$SITE"
111
+
112
+ # The documents the application READS, not merely ships beside: the
113
+ # manual is the console's Manual tab, and the spec is copied into every
114
+ # package written.
115
+ - name: Its own documents are inside it, and readable
116
+ working-directory: /tmp
117
+ run: |
118
+ /tmp/smoke/bin/python -P - <<'PY'
119
+ import pathlib
120
+ import connections_export
121
+ from connections_export.gui.manual import MANUAL_DOC_PATH, manual_html
122
+ from connections_export.interchange.package import SPEC_DOC_PATH
123
+
124
+ root = pathlib.Path(connections_export.__file__).parent
125
+ assert MANUAL_DOC_PATH.is_file(), f"no manual at {MANUAL_DOC_PATH}"
126
+ assert SPEC_DOC_PATH.is_file(), f"no interchange spec at {SPEC_DOC_PATH}"
127
+ assert (root / "architecture.md").is_file(), "no architecture document"
128
+ html = manual_html()
129
+ assert html.count("<h2") >= 10, f"the manual rendered {html.count('<h2')} sections"
130
+ print(f"manual renders: {len(html)} chars, {html.count('<h2')} sections")
131
+ PY
132
+
133
+ # The console serves its own page, with the manual already in it -- and
134
+ # refuses a caller that is not local. The console reads archives and can
135
+ # start a capture with the user's own credentials, so it answers to this
136
+ # machine and nothing else; that guard is worth proving in the artefact
137
+ # that ships rather than only in the tests.
138
+ - name: The console starts, serves a complete page, and stays local
139
+ working-directory: /tmp
140
+ run: |
141
+ /tmp/smoke/bin/python -P - <<'PY'
142
+ import asyncio
143
+ import httpx
144
+ from connections_export.gui.app import make_app
145
+
146
+ async def get(base):
147
+ transport = httpx.ASGITransport(app=make_app(demo=True))
148
+ async with httpx.AsyncClient(transport=transport, base_url=base) as c:
149
+ return await c.get("/")
150
+
151
+ r = asyncio.run(get("http://127.0.0.1"))
152
+ assert r.status_code == 200, r.status_code
153
+ assert 'id="man-intro"' in r.text, "the manual is not in the served page"
154
+ assert "<!--MANUAL-->" not in r.text, "the manual placeholder was never filled"
155
+ print(f"console serves {len(r.text)} chars with the manual in place")
156
+
157
+ elsewhere = asyncio.run(get("http://elsewhere.example"))
158
+ assert elsewhere.status_code == 403, (
159
+ f"a non-local caller got {elsewhere.status_code}, not 403"
160
+ )
161
+ print("a non-local caller is refused")
162
+ PY
163
+
164
+ publish-testpypi:
165
+ # Dormant until dispatched by hand. Nothing on a push can reach this job.
166
+ if: github.event_name == 'workflow_dispatch' && inputs.publish
167
+ needs: [build, smoke]
168
+ runs-on: ubuntu-latest
169
+ environment:
170
+ name: testpypi
171
+ url: https://test.pypi.org/p/connections-export
172
+ permissions:
173
+ id-token: write
174
+ steps:
175
+ - uses: actions/download-artifact@v4
176
+ with:
177
+ name: distributions
178
+ path: dist/
179
+ - uses: pypa/gh-action-pypi-publish@release/v1
180
+ with:
181
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions: {}
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Confirm tag matches package version
19
+ run: |
20
+ version="$(uv run --no-project python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
21
+ test "v${version}" = "$GITHUB_REF_NAME"
22
+ - name: Build distributions
23
+ run: uv build
24
+ - uses: actions/upload-artifact@v4
25
+ with:
26
+ name: python-package-distributions
27
+ path: dist/
28
+
29
+ publish-to-pypi:
30
+ needs: build
31
+ runs-on: ubuntu-latest
32
+ environment:
33
+ name: pypi
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: python-package-distributions
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+
11
+ # Node
12
+ node_modules/
13
+ *.log
14
+
15
+ # Crawl output holds exported content and session tokens -- never commit.
16
+ # Anchored to the repository root so it does not match the
17
+ # connections_export/archive source package or tests/archive.
18
+ /archive/
19
+ *.ltpa
20
+ .env
21
+ .env.*
22
+
23
+ # Your own config: copy connections-export.example.toml and fill it in. It
24
+ # names your deployment, so it is never committed.
25
+ connections-export.toml
26
+
27
+ # hcl-serve's default run-archive location when run from the repo root
28
+ # (CONNECTIONS_EXPORT_ARCHIVES_DIR unset) — dev/testing archives, never commit.
29
+ /connections-export-archives/
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Jörg Türmer
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.