manualbeagle 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 (237) hide show
  1. manualbeagle-0.1.0/.dockerignore +9 -0
  2. manualbeagle-0.1.0/.editorconfig +26 -0
  3. manualbeagle-0.1.0/.gitattributes +3 -0
  4. manualbeagle-0.1.0/.github/CODEOWNERS +2 -0
  5. manualbeagle-0.1.0/.github/FUNDING.yml +2 -0
  6. manualbeagle-0.1.0/.github/ISSUE_TEMPLATE/broken_adapter.yml +38 -0
  7. manualbeagle-0.1.0/.github/ISSUE_TEMPLATE/bug.yml +43 -0
  8. manualbeagle-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  9. manualbeagle-0.1.0/.github/ISSUE_TEMPLATE/feature.yml +30 -0
  10. manualbeagle-0.1.0/.github/ISSUE_TEMPLATE/new_brand.yml +48 -0
  11. manualbeagle-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +37 -0
  12. manualbeagle-0.1.0/.github/dependabot.yml +14 -0
  13. manualbeagle-0.1.0/.github/workflows/ci.yml +34 -0
  14. manualbeagle-0.1.0/.github/workflows/live-adapters.yml +138 -0
  15. manualbeagle-0.1.0/.github/workflows/release.yml +193 -0
  16. manualbeagle-0.1.0/.gitignore +21 -0
  17. manualbeagle-0.1.0/.pre-commit-config.yaml +7 -0
  18. manualbeagle-0.1.0/AGENTS.md +6 -0
  19. manualbeagle-0.1.0/CHANGELOG.md +156 -0
  20. manualbeagle-0.1.0/CLAUDE.md +99 -0
  21. manualbeagle-0.1.0/CODE_OF_CONDUCT.md +133 -0
  22. manualbeagle-0.1.0/CONTRIBUTING.md +382 -0
  23. manualbeagle-0.1.0/Dockerfile +49 -0
  24. manualbeagle-0.1.0/LICENSE +202 -0
  25. manualbeagle-0.1.0/NOTICE +4 -0
  26. manualbeagle-0.1.0/PKG-INFO +207 -0
  27. manualbeagle-0.1.0/README.md +150 -0
  28. manualbeagle-0.1.0/SECURITY.md +64 -0
  29. manualbeagle-0.1.0/docs/adapters.md +381 -0
  30. manualbeagle-0.1.0/docs/architecture.md +229 -0
  31. manualbeagle-0.1.0/docs/brands.md +72 -0
  32. manualbeagle-0.1.0/docs/docker.md +102 -0
  33. manualbeagle-0.1.0/docs/integrations/folder.md +77 -0
  34. manualbeagle-0.1.0/docs/integrations/homeassistant.md +141 -0
  35. manualbeagle-0.1.0/docs/integrations/homebox.md +231 -0
  36. manualbeagle-0.1.0/docs/integrations/mcp.md +72 -0
  37. manualbeagle-0.1.0/docs/integrations/paperless.md +110 -0
  38. manualbeagle-0.1.0/docs/privacy.md +127 -0
  39. manualbeagle-0.1.0/docs/releasing.md +94 -0
  40. manualbeagle-0.1.0/docs/web.md +349 -0
  41. manualbeagle-0.1.0/pyproject.toml +100 -0
  42. manualbeagle-0.1.0/src/manualbeagle/__init__.py +3 -0
  43. manualbeagle-0.1.0/src/manualbeagle/__main__.py +5 -0
  44. manualbeagle-0.1.0/src/manualbeagle/adapters/__init__.py +81 -0
  45. manualbeagle-0.1.0/src/manualbeagle/adapters/base.py +227 -0
  46. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/alorair.yaml +30 -0
  47. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/amcrest.yaml +21 -0
  48. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/apc.yaml +14 -0
  49. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/brother.yaml +20 -0
  50. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/canon.yaml +19 -0
  51. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/chamberlain.yaml +18 -0
  52. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/chargepoint.yaml +18 -0
  53. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/cyberpower.yaml +17 -0
  54. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/dewalt.yaml +42 -0
  55. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/ecoflow.yaml +21 -0
  56. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/epson.yaml +18 -0
  57. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/eufy.yaml +47 -0
  58. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/ge_appliances.yaml +38 -0
  59. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/harbor_freight.yaml +44 -0
  60. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/hatch.yaml +25 -0
  61. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/honda_power.yaml +17 -0
  62. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/klein_tools.yaml +18 -0
  63. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/levoit.yaml +21 -0
  64. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/lg.yaml +29 -0
  65. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/lutron.yaml +22 -0
  66. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/mitsubishi_electric.yaml +25 -0
  67. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/petkit.yaml +37 -0
  68. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/reolink.yaml +25 -0
  69. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/resideo.yaml +24 -0
  70. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/roborock.yaml +18 -0
  71. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/samsung.yaml +26 -0
  72. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/sensorpush.yaml +31 -0
  73. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/solaredge.yaml +18 -0
  74. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/span.yaml +26 -0
  75. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/toyota.yaml +30 -0
  76. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/tplink.yaml +38 -0
  77. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/tripp_lite.yaml +14 -0
  78. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/wen.yaml +21 -0
  79. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/whirlpool.yaml +19 -0
  80. manualbeagle-0.1.0/src/manualbeagle/adapters/defs/whisker.yaml +17 -0
  81. manualbeagle-0.1.0/src/manualbeagle/adapters/py/__init__.py +1 -0
  82. manualbeagle-0.1.0/src/manualbeagle/adapters/py/amcrest.py +93 -0
  83. manualbeagle-0.1.0/src/manualbeagle/adapters/py/brother.py +88 -0
  84. manualbeagle-0.1.0/src/manualbeagle/adapters/py/chamberlain.py +108 -0
  85. manualbeagle-0.1.0/src/manualbeagle/adapters/py/ecoflow.py +121 -0
  86. manualbeagle-0.1.0/src/manualbeagle/adapters/py/epson.py +87 -0
  87. manualbeagle-0.1.0/src/manualbeagle/adapters/py/hatch.py +121 -0
  88. manualbeagle-0.1.0/src/manualbeagle/adapters/py/levoit.py +74 -0
  89. manualbeagle-0.1.0/src/manualbeagle/adapters/py/lg.py +109 -0
  90. manualbeagle-0.1.0/src/manualbeagle/adapters/py/mitsubishi_electric.py +80 -0
  91. manualbeagle-0.1.0/src/manualbeagle/adapters/py/reolink.py +95 -0
  92. manualbeagle-0.1.0/src/manualbeagle/adapters/py/resideo.py +129 -0
  93. manualbeagle-0.1.0/src/manualbeagle/adapters/py/roborock.py +105 -0
  94. manualbeagle-0.1.0/src/manualbeagle/adapters/py/samsung.py +119 -0
  95. manualbeagle-0.1.0/src/manualbeagle/adapters/py/shopify.py +116 -0
  96. manualbeagle-0.1.0/src/manualbeagle/adapters/py/toyota.py +100 -0
  97. manualbeagle-0.1.0/src/manualbeagle/adapters/py/whisker.py +91 -0
  98. manualbeagle-0.1.0/src/manualbeagle/adapters/py/zendesk.py +108 -0
  99. manualbeagle-0.1.0/src/manualbeagle/adapters/yaml_adapter.py +193 -0
  100. manualbeagle-0.1.0/src/manualbeagle/ask.py +456 -0
  101. manualbeagle-0.1.0/src/manualbeagle/batch.py +213 -0
  102. manualbeagle-0.1.0/src/manualbeagle/cli.py +881 -0
  103. manualbeagle-0.1.0/src/manualbeagle/config.py +286 -0
  104. manualbeagle-0.1.0/src/manualbeagle/http.py +389 -0
  105. manualbeagle-0.1.0/src/manualbeagle/library.py +409 -0
  106. manualbeagle-0.1.0/src/manualbeagle/maintenance.py +1045 -0
  107. manualbeagle-0.1.0/src/manualbeagle/mcp_server.py +173 -0
  108. manualbeagle-0.1.0/src/manualbeagle/models.py +157 -0
  109. manualbeagle-0.1.0/src/manualbeagle/nameplate.py +371 -0
  110. manualbeagle-0.1.0/src/manualbeagle/newitem.py +242 -0
  111. manualbeagle-0.1.0/src/manualbeagle/normalize.py +113 -0
  112. manualbeagle-0.1.0/src/manualbeagle/pipeline.py +118 -0
  113. manualbeagle-0.1.0/src/manualbeagle/scoreboard.py +190 -0
  114. manualbeagle-0.1.0/src/manualbeagle/search/__init__.py +38 -0
  115. manualbeagle-0.1.0/src/manualbeagle/search/searxng.py +68 -0
  116. manualbeagle-0.1.0/src/manualbeagle/settings.py +194 -0
  117. manualbeagle-0.1.0/src/manualbeagle/sinks/__init__.py +1 -0
  118. manualbeagle-0.1.0/src/manualbeagle/sinks/folder.py +84 -0
  119. manualbeagle-0.1.0/src/manualbeagle/sinks/homebox.py +314 -0
  120. manualbeagle-0.1.0/src/manualbeagle/sinks/paperless.py +383 -0
  121. manualbeagle-0.1.0/src/manualbeagle/sinks/seed.py +42 -0
  122. manualbeagle-0.1.0/src/manualbeagle/sources/__init__.py +1 -0
  123. manualbeagle-0.1.0/src/manualbeagle/sources/homeassistant.py +432 -0
  124. manualbeagle-0.1.0/src/manualbeagle/sync.py +322 -0
  125. manualbeagle-0.1.0/src/manualbeagle/testing.py +192 -0
  126. manualbeagle-0.1.0/src/manualbeagle/verify.py +386 -0
  127. manualbeagle-0.1.0/src/manualbeagle/vision.py +208 -0
  128. manualbeagle-0.1.0/src/manualbeagle/web/__init__.py +6 -0
  129. manualbeagle-0.1.0/src/manualbeagle/web/app.py +1547 -0
  130. manualbeagle-0.1.0/src/manualbeagle/web/integrations.py +291 -0
  131. manualbeagle-0.1.0/src/manualbeagle/web/jobs.py +118 -0
  132. manualbeagle-0.1.0/src/manualbeagle/web/static/app.js +1410 -0
  133. manualbeagle-0.1.0/src/manualbeagle/web/static/icons/icon-180.png +0 -0
  134. manualbeagle-0.1.0/src/manualbeagle/web/static/icons/icon-192.png +0 -0
  135. manualbeagle-0.1.0/src/manualbeagle/web/static/icons/icon-512.png +0 -0
  136. manualbeagle-0.1.0/src/manualbeagle/web/static/icons/icon.svg +1 -0
  137. manualbeagle-0.1.0/src/manualbeagle/web/static/icons/maskable-512.png +0 -0
  138. manualbeagle-0.1.0/src/manualbeagle/web/static/index.html +201 -0
  139. manualbeagle-0.1.0/src/manualbeagle/web/static/manifest.webmanifest +21 -0
  140. manualbeagle-0.1.0/src/manualbeagle/web/static/style.css +245 -0
  141. manualbeagle-0.1.0/src/manualbeagle/web/static/sw.js +38 -0
  142. manualbeagle-0.1.0/tests/fixtures/alorair/galaxy-60.json +13 -0
  143. manualbeagle-0.1.0/tests/fixtures/alorair/sentinel-hd55.json +13 -0
  144. manualbeagle-0.1.0/tests/fixtures/amcrest/ad410.json +13 -0
  145. manualbeagle-0.1.0/tests/fixtures/amcrest/ip8m-t2499ew.json +13 -0
  146. manualbeagle-0.1.0/tests/fixtures/brother/hl-l2395dw.json +23 -0
  147. manualbeagle-0.1.0/tests/fixtures/brother/hl-l2460dw.json +23 -0
  148. manualbeagle-0.1.0/tests/fixtures/brother/pt-p750w.json +23 -0
  149. manualbeagle-0.1.0/tests/fixtures/chamberlain/b970.json +23 -0
  150. manualbeagle-0.1.0/tests/fixtures/chamberlain/b980.json +23 -0
  151. manualbeagle-0.1.0/tests/fixtures/dewalt/dcs380p1.json +13 -0
  152. manualbeagle-0.1.0/tests/fixtures/dewalt/dcs391b.json +13 -0
  153. manualbeagle-0.1.0/tests/fixtures/dewalt/dwe575sb.json +13 -0
  154. manualbeagle-0.1.0/tests/fixtures/ecoflow/delta-2.json +22 -0
  155. manualbeagle-0.1.0/tests/fixtures/ecoflow/river-2.json +22 -0
  156. manualbeagle-0.1.0/tests/fixtures/ecoflow/trail-plus-300-dc.json +14 -0
  157. manualbeagle-0.1.0/tests/fixtures/epson/et-2800.json +22 -0
  158. manualbeagle-0.1.0/tests/fixtures/epson/wf-2850.json +22 -0
  159. manualbeagle-0.1.0/tests/fixtures/epson/xp-6000.json +22 -0
  160. manualbeagle-0.1.0/tests/fixtures/eufy/robovac-x8-hybrid.json +22 -0
  161. manualbeagle-0.1.0/tests/fixtures/eufy/smart-scale-p2-pro.json +13 -0
  162. manualbeagle-0.1.0/tests/fixtures/ge_appliances/fcm7skww.json +13 -0
  163. manualbeagle-0.1.0/tests/fixtures/ge_appliances/gtw465asnww.json +13 -0
  164. manualbeagle-0.1.0/tests/fixtures/ge_appliances/jb645rkss.json +13 -0
  165. manualbeagle-0.1.0/tests/fixtures/hatch/rest-2nd-gen.json +31 -0
  166. manualbeagle-0.1.0/tests/fixtures/hatch/rest.json +31 -0
  167. manualbeagle-0.1.0/tests/fixtures/hatch/restore-2.json +31 -0
  168. manualbeagle-0.1.0/tests/fixtures/hatch/restore.json +31 -0
  169. manualbeagle-0.1.0/tests/fixtures/levoit/classic300s.json +13 -0
  170. manualbeagle-0.1.0/tests/fixtures/levoit/core-300s-p.json +13 -0
  171. manualbeagle-0.1.0/tests/fixtures/lg/32u889sa-w.json +13 -0
  172. manualbeagle-0.1.0/tests/fixtures/lg/dlex5500v.json +13 -0
  173. manualbeagle-0.1.0/tests/fixtures/lg/lw8016er.json +13 -0
  174. manualbeagle-0.1.0/tests/fixtures/lg/wm5700hva-assevus.json +13 -0
  175. manualbeagle-0.1.0/tests/fixtures/lg/wm5700hva.json +13 -0
  176. manualbeagle-0.1.0/tests/fixtures/maintenance/garage_opener.txt +11 -0
  177. manualbeagle-0.1.0/tests/fixtures/maintenance/robot_vacuum.txt +9 -0
  178. manualbeagle-0.1.0/tests/fixtures/maintenance/window_ac.txt +15 -0
  179. manualbeagle-0.1.0/tests/fixtures/mitsubishi_electric/msz-fh12na.json +24 -0
  180. manualbeagle-0.1.0/tests/fixtures/mitsubishi_electric/msz-gl09na.json +34 -0
  181. manualbeagle-0.1.0/tests/fixtures/mitsubishi_electric/muz-fh12na.json +44 -0
  182. manualbeagle-0.1.0/tests/fixtures/mitsubishi_electric/mxz-3c24na2.json +24 -0
  183. manualbeagle-0.1.0/tests/fixtures/mitsubishi_electric/pac-uswhs002-wf-2.json +24 -0
  184. manualbeagle-0.1.0/tests/fixtures/nameplate/make_plates.py +51 -0
  185. manualbeagle-0.1.0/tests/fixtures/nameplate/plate_lg.png +0 -0
  186. manualbeagle-0.1.0/tests/fixtures/nameplate/plate_whirlpool.jpg +0 -0
  187. manualbeagle-0.1.0/tests/fixtures/petkit/d4h-2.json +13 -0
  188. manualbeagle-0.1.0/tests/fixtures/petkit/yumshare-dual-hopper-2.json +13 -0
  189. manualbeagle-0.1.0/tests/fixtures/reolink/d340w.json +22 -0
  190. manualbeagle-0.1.0/tests/fixtures/reolink/e1-outdoor-pro.json +22 -0
  191. manualbeagle-0.1.0/tests/fixtures/reolink/trackflex-floodlight.json +22 -0
  192. manualbeagle-0.1.0/tests/fixtures/resideo/rcht9610wf.json +31 -0
  193. manualbeagle-0.1.0/tests/fixtures/resideo/rchtsensor.json +40 -0
  194. manualbeagle-0.1.0/tests/fixtures/resideo/rth9585wf.json +22 -0
  195. manualbeagle-0.1.0/tests/fixtures/resideo/th6220wf2006.json +22 -0
  196. manualbeagle-0.1.0/tests/fixtures/roborock/q-revo.json +58 -0
  197. manualbeagle-0.1.0/tests/fixtures/roborock/s8-maxv-ultra.json +40 -0
  198. manualbeagle-0.1.0/tests/fixtures/samsung/hw-q800a-za.json +14 -0
  199. manualbeagle-0.1.0/tests/fixtures/samsung/qn65qn85aafxza.json +14 -0
  200. manualbeagle-0.1.0/tests/fixtures/samsung/swa-9500s-za.json +14 -0
  201. manualbeagle-0.1.0/tests/fixtures/sensorpush/ht-w.json +14 -0
  202. manualbeagle-0.1.0/tests/fixtures/sensorpush/ht1.json +14 -0
  203. manualbeagle-0.1.0/tests/fixtures/span/main-32.json +13 -0
  204. manualbeagle-0.1.0/tests/fixtures/span/main-40.json +13 -0
  205. manualbeagle-0.1.0/tests/fixtures/toyota/bz-2026.json +14 -0
  206. manualbeagle-0.1.0/tests/fixtures/toyota/bz4x-2023.json +14 -0
  207. manualbeagle-0.1.0/tests/fixtures/toyota/camry-2019.json +14 -0
  208. manualbeagle-0.1.0/tests/fixtures/toyota/rav4-hybrid-2020.json +24 -0
  209. manualbeagle-0.1.0/tests/fixtures/toyota/sienna-2022.json +24 -0
  210. manualbeagle-0.1.0/tests/fixtures/tplink/ep25p4.json +14 -0
  211. manualbeagle-0.1.0/tests/fixtures/tplink/hs300.json +14 -0
  212. manualbeagle-0.1.0/tests/fixtures/tplink/kp125m.json +14 -0
  213. manualbeagle-0.1.0/tests/fixtures/wen/3410.json +13 -0
  214. manualbeagle-0.1.0/tests/fixtures/wen/56235i.json +13 -0
  215. manualbeagle-0.1.0/tests/fixtures/whisker/litter-robot-4.json +14 -0
  216. manualbeagle-0.1.0/tests/fixtures/whisker/litter-robot-5-pro.json +23 -0
  217. manualbeagle-0.1.0/tests/test_adapters.py +29 -0
  218. manualbeagle-0.1.0/tests/test_ask.py +256 -0
  219. manualbeagle-0.1.0/tests/test_cli.py +23 -0
  220. manualbeagle-0.1.0/tests/test_config.py +40 -0
  221. manualbeagle-0.1.0/tests/test_core.py +279 -0
  222. manualbeagle-0.1.0/tests/test_homeassistant.py +344 -0
  223. manualbeagle-0.1.0/tests/test_homebox.py +83 -0
  224. manualbeagle-0.1.0/tests/test_library.py +134 -0
  225. manualbeagle-0.1.0/tests/test_maintenance.py +348 -0
  226. manualbeagle-0.1.0/tests/test_mcp.py +65 -0
  227. manualbeagle-0.1.0/tests/test_nameplate.py +298 -0
  228. manualbeagle-0.1.0/tests/test_newitem.py +246 -0
  229. manualbeagle-0.1.0/tests/test_paperless.py +243 -0
  230. manualbeagle-0.1.0/tests/test_scoreboard.py +128 -0
  231. manualbeagle-0.1.0/tests/test_settings.py +157 -0
  232. manualbeagle-0.1.0/tests/test_sync.py +284 -0
  233. manualbeagle-0.1.0/tests/test_web.py +375 -0
  234. manualbeagle-0.1.0/tests/test_web_integrations.py +154 -0
  235. manualbeagle-0.1.0/tests/test_web_maintenance.py +197 -0
  236. manualbeagle-0.1.0/tests/test_web_nameplate.py +157 -0
  237. manualbeagle-0.1.0/tests/test_web_newitem.py +170 -0
@@ -0,0 +1,9 @@
1
+ # Only pyproject.toml, README.md, LICENSE, NOTICE and src/ go into the image.
2
+ *
3
+ !pyproject.toml
4
+ !README.md
5
+ !LICENSE
6
+ !NOTICE
7
+ !src/
8
+ **/__pycache__
9
+ **/*.py[cod]
@@ -0,0 +1,26 @@
1
+ # https://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ trim_trailing_whitespace = true
9
+ indent_style = space
10
+ indent_size = 4
11
+
12
+ [*.py]
13
+ max_line_length = 110
14
+
15
+ [*.{yml,yaml,toml,json,html,css,js}]
16
+ indent_size = 2
17
+
18
+ [*.md]
19
+ indent_size = 2
20
+ # a trailing double space is a line break in Markdown
21
+ trim_trailing_whitespace = false
22
+
23
+ [tests/fixtures/**]
24
+ # recorded responses are written by the test harness; leave them as they are
25
+ trim_trailing_whitespace = false
26
+ insert_final_newline = unset
@@ -0,0 +1,3 @@
1
+ * text=auto eol=lf
2
+ *.pdf binary
3
+ *.png binary
@@ -0,0 +1,2 @@
1
+ # Everything is reviewed by the maintainer.
2
+ * @sstangle73
@@ -0,0 +1,2 @@
1
+ # The Sponsor button on GitHub. Supporting the project is optional: everything works without it.
2
+ buy_me_a_coffee: stevenstorie
@@ -0,0 +1,38 @@
1
+ name: Broken adapter
2
+ description: A brand that used to work finds the wrong manual or nothing at all.
3
+ title: "Broken adapter: "
4
+ labels: ["adapter: broken"]
5
+ body:
6
+ - type: input
7
+ id: adapter
8
+ attributes:
9
+ label: Adapter id
10
+ description: From `manualbeagle adapters list` (the first column).
11
+ placeholder: lg
12
+ validations:
13
+ required: true
14
+ - type: input
15
+ id: model
16
+ attributes:
17
+ label: Model
18
+ placeholder: WM5700HVA
19
+ validations:
20
+ required: true
21
+ - type: textarea
22
+ id: output
23
+ attributes:
24
+ label: Output
25
+ description: The output of `manualbeagle find <brand> <model> --why` and, if you have a clone, `manualbeagle adapters test <id> --live -v`.
26
+ render: text
27
+ validations:
28
+ required: true
29
+ - type: textarea
30
+ id: expected
31
+ attributes:
32
+ label: What you expected
33
+ description: The right manual's link, if you found it in a browser.
34
+ - type: input
35
+ id: version
36
+ attributes:
37
+ label: Version or commit
38
+ description: "`manualbeagle --version`, or `git rev-parse --short HEAD`."
@@ -0,0 +1,43 @@
1
+ name: Bug
2
+ description: Something other than a brand adapter doesn't work as documented.
3
+ title: "Bug: "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ For a security problem, don't open an issue: see [SECURITY.md](https://github.com/manualbeagle/manualbeagle/blob/main/SECURITY.md).
10
+ Please remove API keys, private addresses and anything else private from what you paste.
11
+ - type: textarea
12
+ id: what
13
+ attributes:
14
+ label: What happened
15
+ description: The command you ran and its full output.
16
+ render: text
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: expected
21
+ attributes:
22
+ label: What you expected
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: steps
27
+ attributes:
28
+ label: How to reproduce it
29
+ description: Steps, and the relevant part of your config.toml (without secrets).
30
+ - type: input
31
+ id: version
32
+ attributes:
33
+ label: Version or commit
34
+ description: "`manualbeagle --version`, or `git rev-parse --short HEAD`."
35
+ validations:
36
+ required: true
37
+ - type: input
38
+ id: environment
39
+ attributes:
40
+ label: Operating system and Python version
41
+ placeholder: "Ubuntu 24.04, Python 3.12"
42
+ validations:
43
+ required: true
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security problem
4
+ url: https://github.com/manualbeagle/manualbeagle/security/policy
5
+ about: Report it privately, as SECURITY.md describes, not in a public issue.
@@ -0,0 +1,30 @@
1
+ name: Feature
2
+ description: Suggest something new, such as another inventory to file manuals in.
3
+ title: "Feature: "
4
+ labels: ["enhancement"]
5
+ body:
6
+ - type: textarea
7
+ id: problem
8
+ attributes:
9
+ label: What you're trying to do
10
+ description: The problem, not only the solution.
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: idea
15
+ attributes:
16
+ label: What would help
17
+ - type: textarea
18
+ id: alternatives
19
+ attributes:
20
+ label: What you've tried instead
21
+ - type: checkboxes
22
+ id: rules
23
+ attributes:
24
+ label: Project rules
25
+ description: Manual Beagle keeps a few rules whatever the feature (see CONTRIBUTING.md).
26
+ options:
27
+ - label: It works without AI (AI may only add to a rules-only path).
28
+ required: true
29
+ - label: It doesn't host, share or mirror manuals, crawl a site in bulk, or get around bot protection or logins.
30
+ required: true
@@ -0,0 +1,48 @@
1
+ name: New brand
2
+ description: Ask for (or offer to write) an adapter for a brand Manual Beagle doesn't know yet.
3
+ title: "Brand: "
4
+ labels: ["brand"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks! Check [docs/brands.md](https://github.com/manualbeagle/manualbeagle/blob/main/docs/brands.md) first: the brand may already be there under
10
+ another name. Writing the adapter yourself is usually about 15 minutes; [CONTRIBUTING.md](https://github.com/manualbeagle/manualbeagle/blob/main/CONTRIBUTING.md) walks through it.
11
+ - type: input
12
+ id: brand
13
+ attributes:
14
+ label: Brand
15
+ description: As it's written on the rating plate, and any other names it goes by.
16
+ placeholder: "AlorAir (also AlorAir Solutions)"
17
+ validations:
18
+ required: true
19
+ - type: input
20
+ id: site
21
+ attributes:
22
+ label: The maker's support or manuals page
23
+ description: Where a person finds manuals on the maker's own site.
24
+ placeholder: "https://www.example.com/support"
25
+ validations:
26
+ required: true
27
+ - type: textarea
28
+ id: models
29
+ attributes:
30
+ label: Example models
31
+ description: Two or more real model numbers, ideally ones you own, so the adapter has test cases.
32
+ placeholder: |
33
+ Galaxy 60
34
+ Sentinel HD55
35
+ validations:
36
+ required: true
37
+ - type: textarea
38
+ id: how
39
+ attributes:
40
+ label: How the site lists documents (if you know)
41
+ description: PDF links in the page's HTML, JSON embedded in the page, or an API the page calls. The browser's developer tools (Network tab) show it.
42
+ - type: checkboxes
43
+ id: rules
44
+ attributes:
45
+ label: Fetching rules
46
+ options:
47
+ - label: As far as I can tell, the site's terms don't forbid automated access, and it doesn't need a login or pass a CAPTCHA to reach the manuals (if it does, say so above; it can still get a link-only adapter).
48
+ - label: I'd like to write this adapter myself.
@@ -0,0 +1,37 @@
1
+ ## What this changes
2
+
3
+ <!-- One or two sentences. For an adapter: the brand, how it searches, and how many models pass live. -->
4
+
5
+ ## Checklist
6
+
7
+ - [ ] `ruff check src tests` and `ruff format --check src tests` pass.
8
+ - [ ] `pytest` passes (offline; no test touches the network unless it's marked `live`).
9
+ - [ ] Docs are updated for anything a user or contributor would notice (`README.md`, `CONTRIBUTING.md`,
10
+ `docs/architecture.md`, `docs/adapters.md`, `docs/integrations/`), and public code has docstrings and type
11
+ hints.
12
+ - [ ] Colour in any output is paired with a glyph or a word.
13
+ - [ ] No new dependency, or it's compatible with Apache-2.0 and the description says why it's needed.
14
+
15
+ ### For a new or changed adapter
16
+
17
+ - [ ] The adapter starts with its header: the site, how it searches, known gaps, and today's date as
18
+ "Last verified".
19
+ - [ ] At least two real test cases, with fixtures recorded by `manualbeagle adapters test <id> --record -v`.
20
+ - [ ] The fixtures are small and hold no PDF bodies.
21
+ - [ ] `docs/brands.md` regenerated with `manualbeagle adapters scoreboard --write docs/brands.md`.
22
+ - [ ] The output of `manualbeagle adapters test <id> --live -v` is below.
23
+
24
+ ### Fetching rules
25
+
26
+ - [ ] Every request goes through `ctx.http` (the polite client): one request at a time per site, the project's
27
+ User-Agent.
28
+ - [ ] Nothing crawls or lists a site in bulk: only what one lookup needs.
29
+ - [ ] Nothing gets around a CAPTCHA, a bot wall or a login (no browser automation, spoofed headers, copied
30
+ cookies or proxies).
31
+ - [ ] The site's terms don't forbid automated access.
32
+ - [ ] Nothing hosts, shares or mirrors PDFs.
33
+
34
+ ## Live test output (adapters)
35
+
36
+ ```
37
+ ```
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: github-actions
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
11
+ - package-ecosystem: docker # the Dockerfile's base image
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths-ignore: ["docs/**", "*.md"]
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ # Every version pyproject.toml's classifiers name.
24
+ python-version: ["3.12", "3.13", "3.14"]
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ cache: pip
31
+ - run: pip install -e ".[dev]"
32
+ - run: ruff check src tests
33
+ - run: ruff format --check src tests
34
+ - run: pytest -q
@@ -0,0 +1,138 @@
1
+ name: Live adapters
2
+
3
+ # Runs every adapter's test cases against the real sites, which is how we notice when a maker's site changes.
4
+ #
5
+ # - Every night once the repository is public. While it's private, Actions minutes count, so the schedule runs
6
+ # on Mondays only (the second cron line); each job's `if` picks the line that applies.
7
+ # - The run's page shows the live scoreboard (docs/brands.md with a Live column) and a report of what broke; the
8
+ # JSON results, the log and both Markdown files are kept as the run's artifact for 30 days.
9
+ # - A scheduled run keeps one issue labelled `live-adapters` current: opened when something breaks, its text
10
+ # replaced and a comment added each night it stays broken, closed when every case passes again. A manual run
11
+ # touches the issue only when asked to (the `report` input).
12
+ # - Requests follow the project's fetching rules like any lookup: one at a time per site, with a delay, and
13
+ # nothing that crawls. A site that blocks the runner shows as ✗ blocked; nothing tries to get around it.
14
+
15
+ on:
16
+ schedule:
17
+ - cron: "17 7 * * *" # nightly, public repository
18
+ - cron: "17 9 * * 1" # Mondays, private repository
19
+ workflow_dispatch:
20
+ inputs:
21
+ adapters:
22
+ description: "adapter ids, space-separated (blank: all)"
23
+ required: false
24
+ report:
25
+ description: "open, update or close the live-adapters issue"
26
+ type: boolean
27
+ default: false
28
+
29
+ permissions:
30
+ contents: read
31
+
32
+ concurrency:
33
+ group: live-adapters
34
+ cancel-in-progress: false
35
+
36
+ jobs:
37
+ live:
38
+ if: >-
39
+ github.event_name != 'schedule'
40
+ || (github.event.schedule == '17 7 * * *' && !github.event.repository.private)
41
+ || (github.event.schedule == '17 9 * * 1' && github.event.repository.private)
42
+ runs-on: ubuntu-latest
43
+ timeout-minutes: 45
44
+ outputs:
45
+ broken: ${{ steps.report.outputs.broken }}
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ - uses: actions/setup-python@v5
49
+ with:
50
+ python-version: "3.12"
51
+ cache: pip
52
+ - run: pip install -e .
53
+ - name: Run live adapter tests
54
+ # The input goes through an environment variable, never into the script itself, so a value can't inject
55
+ # shell commands. It's split on spaces on purpose: one argument per adapter id.
56
+ env:
57
+ ADAPTERS: ${{ github.event.inputs.adapters }}
58
+ run: |
59
+ # shellcheck disable=SC2086 # split on purpose
60
+ manualbeagle adapters test --live --json live.json $ADAPTERS | tee live.txt
61
+ continue-on-error: true
62
+ - name: Live scoreboard
63
+ run: manualbeagle adapters scoreboard --live live.json --write live-brands.md
64
+ - name: What broke
65
+ id: report
66
+ env:
67
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
68
+ run: |
69
+ set +e
70
+ manualbeagle adapters report live.json --run-url "$RUN_URL" --write report.md
71
+ python -c "import json; r = json.load(open('live.json', encoding='utf-8')); print('broken=' + str(sum(x['status'] in ('fail', 'error', 'blocked') for x in r)))" >> "$GITHUB_OUTPUT"
72
+ exit 0
73
+ - name: Summary
74
+ run: |
75
+ {
76
+ cat report.md
77
+ echo
78
+ echo '## Live scoreboard'
79
+ sed -n '/^| Brand /,$p' live-brands.md
80
+ } >> "$GITHUB_STEP_SUMMARY"
81
+ - uses: actions/upload-artifact@v4
82
+ if: always()
83
+ with:
84
+ name: live-adapters
85
+ path: |
86
+ live.json
87
+ live.txt
88
+ live-brands.md
89
+ report.md
90
+ retention-days: 30
91
+ if-no-files-found: warn
92
+ - name: Fail the run when a case broke
93
+ env:
94
+ BROKEN: ${{ steps.report.outputs.broken }}
95
+ run: |
96
+ if [ "${BROKEN:-1}" != "0" ]; then echo "✗ $BROKEN broken case(s): see the summary"; exit 1; fi
97
+ echo "✓ every case that ran passed"
98
+
99
+ issue:
100
+ name: Keep the live-adapters issue current
101
+ needs: live
102
+ if: >-
103
+ always() && needs.live.outputs.broken != ''
104
+ && (github.event_name == 'schedule' || inputs.report == true)
105
+ runs-on: ubuntu-latest
106
+ timeout-minutes: 5
107
+ permissions:
108
+ contents: read
109
+ issues: write
110
+ steps:
111
+ - uses: actions/download-artifact@v4
112
+ with:
113
+ name: live-adapters
114
+ - name: Open, update or close the issue
115
+ env:
116
+ GH_TOKEN: ${{ github.token }}
117
+ GH_REPO: ${{ github.repository }}
118
+ BROKEN: ${{ needs.live.outputs.broken }}
119
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
120
+ run: |
121
+ label=live-adapters
122
+ gh label create "$label" --color FBCA04 \
123
+ --description "An adapter's test cases fail against the maker's live site" --force
124
+ num=$(gh issue list --label "$label" --state open --json number --jq '.[0].number // empty')
125
+ today=$(date -u +%F)
126
+ if [ "$BROKEN" != "0" ]; then
127
+ if [ -n "$num" ]; then
128
+ gh issue edit "$num" --title "Live adapters: $BROKEN case(s) broken" --body-file report.md
129
+ gh issue comment "$num" --body "✗ $today: $BROKEN case(s) broken ([run]($RUN_URL)). The description has the current list."
130
+ else
131
+ gh issue create --title "Live adapters: $BROKEN case(s) broken" --label "$label" --body-file report.md
132
+ fi
133
+ elif [ -n "$num" ]; then
134
+ gh issue comment "$num" --body "✓ $today: every case that ran passed ([run]($RUN_URL)). Closing."
135
+ gh issue close "$num"
136
+ else
137
+ echo "✓ nothing broken and no open issue"
138
+ fi
@@ -0,0 +1,193 @@
1
+ name: Release
2
+
3
+ # Pushing a version tag (v0.1.0) builds the sdist, the wheel and the container image, checks them, and publishes
4
+ # them only when the repository variable RELEASE_ENABLED is "true" (Settings > Secrets and variables > Actions >
5
+ # Variables). Until then, and on every manual run, it builds and checks everything and publishes nothing. The
6
+ # steps to switch it on, and to make a release, are in docs/releasing.md.
7
+ #
8
+ # - PyPI: trusted publishing (OpenID Connect), so there is no API token to store. It needs a publisher on PyPI
9
+ # for this repository, this workflow file and the environment "pypi", whose required reviewer approves each
10
+ # upload.
11
+ # - Container: ghcr.io/<owner>/<repo>, for linux/amd64 and linux/arm64, tagged with the version; a final version
12
+ # (not a pre-release such as 0.2.0rc1) is also tagged <major>.<minor> and "latest".
13
+ # - GitHub release: made last, from the tag, with the version's CHANGELOG.md section as its notes and the sdist
14
+ # and wheel attached.
15
+ # - Third-party actions are pinned to commit SHAs (the version in a comment), since these jobs can publish;
16
+ # Dependabot keeps them current.
17
+
18
+ on:
19
+ push:
20
+ tags: ["v[0-9]*"]
21
+ workflow_dispatch: # a dry run: builds and checks, publishes nothing
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ concurrency:
27
+ group: release-${{ github.ref }}
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ build:
32
+ name: Build and check the packages
33
+ runs-on: ubuntu-latest
34
+ timeout-minutes: 10
35
+ outputs:
36
+ version: ${{ steps.version.outputs.version }}
37
+ prerelease: ${{ steps.version.outputs.prerelease }}
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: actions/setup-python@v5
41
+ with:
42
+ python-version: "3.12"
43
+ - run: pip install build twine
44
+ - run: python -m build
45
+ - run: twine check --strict dist/*
46
+ - name: The version, and whether it's a pre-release
47
+ id: version
48
+ run: |
49
+ version=$(python -c "import glob; print(glob.glob('dist/*.whl')[0].split('-')[1])")
50
+ echo "version=$version" >> "$GITHUB_OUTPUT"
51
+ if echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then pre=false; else pre=true; fi
52
+ echo "prerelease=$pre" >> "$GITHUB_OUTPUT"
53
+ echo "version $version, pre-release: $pre"
54
+ - name: The tag matches the package's version
55
+ if: github.ref_type == 'tag'
56
+ env:
57
+ TAG: ${{ github.ref_name }}
58
+ VERSION: ${{ steps.version.outputs.version }}
59
+ run: |
60
+ if [ "v$VERSION" != "$TAG" ]; then
61
+ echo "✗ tag $TAG doesn't match version $VERSION (src/manualbeagle/__init__.py)"; exit 1
62
+ fi
63
+ echo "✓ tag $TAG matches version $VERSION"
64
+ - name: CHANGELOG.md has a section for the version
65
+ if: github.ref_type == 'tag'
66
+ env:
67
+ VERSION: ${{ steps.version.outputs.version }}
68
+ run: |
69
+ grep -q "^## \[$VERSION\]" CHANGELOG.md || { echo "✗ no '## [$VERSION]' in CHANGELOG.md"; exit 1; }
70
+ echo "✓ CHANGELOG.md has [$VERSION]"
71
+ - name: The wheel and the sdist each install and run
72
+ run: |
73
+ for pkg in dist/*.whl dist/*.tar.gz; do
74
+ rm -rf /tmp/check && python -m venv /tmp/check
75
+ /tmp/check/bin/pip install --quiet "$pkg"
76
+ /tmp/check/bin/manualbeagle --version
77
+ /tmp/check/bin/manualbeagle adapters list > /dev/null
78
+ echo "✓ $pkg"
79
+ done
80
+ - uses: actions/upload-artifact@v4
81
+ with:
82
+ name: dist
83
+ path: dist/
84
+ if-no-files-found: error
85
+
86
+ docker:
87
+ name: Build, check and (when enabled) publish the image
88
+ needs: build
89
+ runs-on: ubuntu-latest
90
+ timeout-minutes: 45
91
+ permissions:
92
+ contents: read
93
+ packages: write
94
+ steps:
95
+ - uses: actions/checkout@v4
96
+ - name: Build for this runner
97
+ env:
98
+ VERSION: ${{ needs.build.outputs.version }}
99
+ run: docker build --build-arg "VERSION=$VERSION" -t manualbeagle:check .
100
+ - name: The image starts and answers
101
+ run: |
102
+ docker run -d --name mb -p 127.0.0.1:8765:8765 -e MANUALBEAGLE_WEB_TOKEN=check manualbeagle:check
103
+ for _ in $(seq 1 30); do curl -fs http://127.0.0.1:8765/healthz && break; sleep 1; done
104
+ curl -fs http://127.0.0.1:8765/healthz
105
+ docker exec mb manualbeagle --version
106
+ docker exec mb python -c "from manualbeagle.library import ocr_available; assert ocr_available(), 'no OCR'"
107
+ test "$(docker exec mb id -u)" = 1000
108
+ echo "✓ the image serves /healthz, has OCR and runs as uid 1000"
109
+ docker image ls manualbeagle:check
110
+ - name: Container log
111
+ if: always()
112
+ run: docker logs mb || true
113
+ - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
114
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
115
+ - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
116
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
117
+ - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
118
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
119
+ with:
120
+ registry: ghcr.io
121
+ username: ${{ github.actor }}
122
+ password: ${{ secrets.GITHUB_TOKEN }}
123
+ - name: Image tags
124
+ id: tags
125
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
126
+ env:
127
+ REPO_IMAGE: ghcr.io/${{ github.repository }}
128
+ VERSION: ${{ needs.build.outputs.version }}
129
+ PRE: ${{ needs.build.outputs.prerelease }}
130
+ run: |
131
+ image=$(echo "$REPO_IMAGE" | tr '[:upper:]' '[:lower:]')
132
+ tags="$image:$VERSION"
133
+ if [ "$PRE" = false ]; then tags="$tags,$image:${VERSION%.*},$image:latest"; fi
134
+ echo "tags=$tags" >> "$GITHUB_OUTPUT"
135
+ echo "$tags"
136
+ - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
137
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
138
+ with:
139
+ context: .
140
+ platforms: linux/amd64,linux/arm64
141
+ push: true
142
+ build-args: VERSION=${{ needs.build.outputs.version }}
143
+ tags: ${{ steps.tags.outputs.tags }}
144
+
145
+ pypi:
146
+ name: Publish to PyPI
147
+ needs: [build, docker]
148
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
149
+ runs-on: ubuntu-latest
150
+ timeout-minutes: 10
151
+ environment:
152
+ name: pypi
153
+ url: https://pypi.org/project/manualbeagle/
154
+ permissions:
155
+ id-token: write # trusted publishing
156
+ steps:
157
+ - uses: actions/download-artifact@v4
158
+ with:
159
+ name: dist
160
+ path: dist/
161
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
162
+
163
+ github-release:
164
+ name: The GitHub release
165
+ needs: [build, pypi]
166
+ if: github.ref_type == 'tag' && vars.RELEASE_ENABLED == 'true'
167
+ runs-on: ubuntu-latest
168
+ timeout-minutes: 5
169
+ permissions:
170
+ contents: write
171
+ steps:
172
+ - uses: actions/checkout@v4
173
+ - uses: actions/download-artifact@v4
174
+ with:
175
+ name: dist
176
+ path: dist/
177
+ - name: Notes from CHANGELOG.md, then the release
178
+ env:
179
+ GH_TOKEN: ${{ github.token }}
180
+ TAG: ${{ github.ref_name }}
181
+ VERSION: ${{ needs.build.outputs.version }}
182
+ PRE: ${{ needs.build.outputs.prerelease }}
183
+ run: |
184
+ awk -v v="$VERSION" '
185
+ index($0, "## [" v "]") == 1 { on = 1; next }
186
+ on && /^## \[/ { exit }
187
+ on && /^\[[^]]+\]: / { exit }
188
+ on { print }
189
+ ' CHANGELOG.md > notes.md
190
+ test -s notes.md || { echo "✗ CHANGELOG.md has no notes for $VERSION"; exit 1; }
191
+ flags=""
192
+ if [ "$PRE" = true ]; then flags="--prerelease"; fi
193
+ gh release create "$TAG" dist/* --verify-tag --title "$VERSION" --notes-file notes.md $flags
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ .coverage
12
+ htmlcov/
13
+ # local library, downloads and secrets
14
+ /library/
15
+ /cache/
16
+ *.sqlite
17
+ *.sqlite-journal
18
+ .env
19
+
20
+ # Claude Code: personal notes for this checkout
21
+ CLAUDE.local.md
@@ -0,0 +1,7 @@
1
+ # Runs the same ruff checks as CI on every commit: pip install pre-commit, then pre-commit install.
2
+ repos:
3
+ - repo: https://github.com/astral-sh/ruff-pre-commit
4
+ rev: v0.17.3
5
+ hooks:
6
+ - id: ruff-check
7
+ - id: ruff-format
@@ -0,0 +1,6 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for AI coding agents working on this repository are in [CLAUDE.md](CLAUDE.md): the layout, how to
4
+ run the tests and lint, how to add a brand adapter, and the project's rules (fetching politely and never around
5
+ bot protection, no AI required, Apache-2.0-compatible dependencies, colour always paired with a glyph or word,
6
+ and documentation as part of every change). Contributors should also read [CONTRIBUTING.md](CONTRIBUTING.md).