indonesia-civic-stack 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 (308) hide show
  1. indonesia_civic_stack-0.1.0/.cursorrules +15 -0
  2. indonesia_civic_stack-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +66 -0
  3. indonesia_civic_stack-0.1.0/.github/ISSUE_TEMPLATE/degraded_module.yml +58 -0
  4. indonesia_civic_stack-0.1.0/.github/ISSUE_TEMPLATE/module_addition.yml +81 -0
  5. indonesia_civic_stack-0.1.0/.github/copilot-instructions.md +23 -0
  6. indonesia_civic_stack-0.1.0/.github/pull_request_template.md +38 -0
  7. indonesia_civic_stack-0.1.0/.github/workflows/ci.yml +99 -0
  8. indonesia_civic_stack-0.1.0/.gitignore +5 -0
  9. indonesia_civic_stack-0.1.0/.mcp.json +12 -0
  10. indonesia_civic_stack-0.1.0/AGENTS.md +166 -0
  11. indonesia_civic_stack-0.1.0/CLAUDE.md +67 -0
  12. indonesia_civic_stack-0.1.0/CONTRIBUTING.md +127 -0
  13. indonesia_civic_stack-0.1.0/LICENSE +21 -0
  14. indonesia_civic_stack-0.1.0/LICENSES.md +26 -0
  15. indonesia_civic_stack-0.1.0/PKG-INFO +870 -0
  16. indonesia_civic_stack-0.1.0/PROMPTS.md +202 -0
  17. indonesia_civic_stack-0.1.0/README.md +821 -0
  18. indonesia_civic_stack-0.1.0/SHAPES.MD +317 -0
  19. indonesia_civic_stack-0.1.0/SKILL.md +98 -0
  20. indonesia_civic_stack-0.1.0/SPRINT2-PREP.md +102 -0
  21. indonesia_civic_stack-0.1.0/app.py +102 -0
  22. indonesia_civic_stack-0.1.0/civic_stack/__init__.py +41 -0
  23. indonesia_civic_stack-0.1.0/civic_stack/ahu/Dockerfile +27 -0
  24. indonesia_civic_stack-0.1.0/civic_stack/ahu/LICENSE +127 -0
  25. indonesia_civic_stack-0.1.0/civic_stack/ahu/README.md +173 -0
  26. indonesia_civic_stack-0.1.0/civic_stack/ahu/__init__.py +17 -0
  27. indonesia_civic_stack-0.1.0/civic_stack/ahu/app.py +18 -0
  28. indonesia_civic_stack-0.1.0/civic_stack/ahu/browser.py +134 -0
  29. indonesia_civic_stack-0.1.0/civic_stack/ahu/normalizer.py +269 -0
  30. indonesia_civic_stack-0.1.0/civic_stack/ahu/router.py +102 -0
  31. indonesia_civic_stack-0.1.0/civic_stack/ahu/scraper.py +165 -0
  32. indonesia_civic_stack-0.1.0/civic_stack/ahu/server.py +116 -0
  33. indonesia_civic_stack-0.1.0/civic_stack/app.py +102 -0
  34. indonesia_civic_stack-0.1.0/civic_stack/bmkg/Dockerfile +10 -0
  35. indonesia_civic_stack-0.1.0/civic_stack/bmkg/LICENSE +127 -0
  36. indonesia_civic_stack-0.1.0/civic_stack/bmkg/README.md +190 -0
  37. indonesia_civic_stack-0.1.0/civic_stack/bmkg/__init__.py +35 -0
  38. indonesia_civic_stack-0.1.0/civic_stack/bmkg/app.py +15 -0
  39. indonesia_civic_stack-0.1.0/civic_stack/bmkg/normalizer.py +122 -0
  40. indonesia_civic_stack-0.1.0/civic_stack/bmkg/router.py +57 -0
  41. indonesia_civic_stack-0.1.0/civic_stack/bmkg/scraper.py +274 -0
  42. indonesia_civic_stack-0.1.0/civic_stack/bmkg/server.py +71 -0
  43. indonesia_civic_stack-0.1.0/civic_stack/bpjph/Dockerfile +25 -0
  44. indonesia_civic_stack-0.1.0/civic_stack/bpjph/LICENSE +127 -0
  45. indonesia_civic_stack-0.1.0/civic_stack/bpjph/README.md +111 -0
  46. indonesia_civic_stack-0.1.0/civic_stack/bpjph/__init__.py +18 -0
  47. indonesia_civic_stack-0.1.0/civic_stack/bpjph/app.py +18 -0
  48. indonesia_civic_stack-0.1.0/civic_stack/bpjph/browser.py +104 -0
  49. indonesia_civic_stack-0.1.0/civic_stack/bpjph/normalizer.py +231 -0
  50. indonesia_civic_stack-0.1.0/civic_stack/bpjph/router.py +76 -0
  51. indonesia_civic_stack-0.1.0/civic_stack/bpjph/scraper.py +192 -0
  52. indonesia_civic_stack-0.1.0/civic_stack/bpjph/server.py +91 -0
  53. indonesia_civic_stack-0.1.0/civic_stack/bpom/Dockerfile +22 -0
  54. indonesia_civic_stack-0.1.0/civic_stack/bpom/LICENSE +21 -0
  55. indonesia_civic_stack-0.1.0/civic_stack/bpom/README.md +141 -0
  56. indonesia_civic_stack-0.1.0/civic_stack/bpom/__init__.py +17 -0
  57. indonesia_civic_stack-0.1.0/civic_stack/bpom/app.py +18 -0
  58. indonesia_civic_stack-0.1.0/civic_stack/bpom/normalizer.py +170 -0
  59. indonesia_civic_stack-0.1.0/civic_stack/bpom/router.py +84 -0
  60. indonesia_civic_stack-0.1.0/civic_stack/bpom/scraper.py +143 -0
  61. indonesia_civic_stack-0.1.0/civic_stack/bpom/server.py +97 -0
  62. indonesia_civic_stack-0.1.0/civic_stack/bps/Dockerfile +10 -0
  63. indonesia_civic_stack-0.1.0/civic_stack/bps/LICENSE +127 -0
  64. indonesia_civic_stack-0.1.0/civic_stack/bps/README.md +165 -0
  65. indonesia_civic_stack-0.1.0/civic_stack/bps/__init__.py +19 -0
  66. indonesia_civic_stack-0.1.0/civic_stack/bps/app.py +15 -0
  67. indonesia_civic_stack-0.1.0/civic_stack/bps/normalizer.py +90 -0
  68. indonesia_civic_stack-0.1.0/civic_stack/bps/router.py +56 -0
  69. indonesia_civic_stack-0.1.0/civic_stack/bps/scraper.py +217 -0
  70. indonesia_civic_stack-0.1.0/civic_stack/bps/server.py +62 -0
  71. indonesia_civic_stack-0.1.0/civic_stack/cli.py +70 -0
  72. indonesia_civic_stack-0.1.0/civic_stack/kpu/Dockerfile +10 -0
  73. indonesia_civic_stack-0.1.0/civic_stack/kpu/LICENSE +127 -0
  74. indonesia_civic_stack-0.1.0/civic_stack/kpu/README.md +169 -0
  75. indonesia_civic_stack-0.1.0/civic_stack/kpu/__init__.py +17 -0
  76. indonesia_civic_stack-0.1.0/civic_stack/kpu/app.py +17 -0
  77. indonesia_civic_stack-0.1.0/civic_stack/kpu/normalizer.py +101 -0
  78. indonesia_civic_stack-0.1.0/civic_stack/kpu/router.py +66 -0
  79. indonesia_civic_stack-0.1.0/civic_stack/kpu/scraper.py +181 -0
  80. indonesia_civic_stack-0.1.0/civic_stack/kpu/server.py +104 -0
  81. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/Dockerfile +12 -0
  82. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/LICENSE +127 -0
  83. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/README.md +162 -0
  84. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/__init__.py +23 -0
  85. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/app.py +15 -0
  86. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/normalizer.py +125 -0
  87. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/router.py +47 -0
  88. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/scraper.py +325 -0
  89. indonesia_civic_stack-0.1.0/civic_stack/lhkpn/server.py +68 -0
  90. indonesia_civic_stack-0.1.0/civic_stack/lpse/Dockerfile +10 -0
  91. indonesia_civic_stack-0.1.0/civic_stack/lpse/LICENSE +127 -0
  92. indonesia_civic_stack-0.1.0/civic_stack/lpse/README.md +187 -0
  93. indonesia_civic_stack-0.1.0/civic_stack/lpse/__init__.py +18 -0
  94. indonesia_civic_stack-0.1.0/civic_stack/lpse/app.py +15 -0
  95. indonesia_civic_stack-0.1.0/civic_stack/lpse/normalizer.py +96 -0
  96. indonesia_civic_stack-0.1.0/civic_stack/lpse/router.py +41 -0
  97. indonesia_civic_stack-0.1.0/civic_stack/lpse/scraper.py +209 -0
  98. indonesia_civic_stack-0.1.0/civic_stack/lpse/server.py +66 -0
  99. indonesia_civic_stack-0.1.0/civic_stack/ojk/Dockerfile +10 -0
  100. indonesia_civic_stack-0.1.0/civic_stack/ojk/LICENSE +127 -0
  101. indonesia_civic_stack-0.1.0/civic_stack/ojk/README.md +145 -0
  102. indonesia_civic_stack-0.1.0/civic_stack/ojk/__init__.py +17 -0
  103. indonesia_civic_stack-0.1.0/civic_stack/ojk/app.py +17 -0
  104. indonesia_civic_stack-0.1.0/civic_stack/ojk/normalizer.py +106 -0
  105. indonesia_civic_stack-0.1.0/civic_stack/ojk/router.py +87 -0
  106. indonesia_civic_stack-0.1.0/civic_stack/ojk/scraper.py +209 -0
  107. indonesia_civic_stack-0.1.0/civic_stack/ojk/server.py +110 -0
  108. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/Dockerfile +17 -0
  109. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/README.md +138 -0
  110. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/__init__.py +5 -0
  111. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/app.py +17 -0
  112. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/normalizer.py +161 -0
  113. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/router.py +57 -0
  114. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/scraper.py +121 -0
  115. indonesia_civic_stack-0.1.0/civic_stack/oss_nib/server.py +82 -0
  116. indonesia_civic_stack-0.1.0/civic_stack/server.py +494 -0
  117. indonesia_civic_stack-0.1.0/civic_stack/shared/__init__.py +5 -0
  118. indonesia_civic_stack-0.1.0/civic_stack/shared/http.py +265 -0
  119. indonesia_civic_stack-0.1.0/civic_stack/shared/mcp.py +115 -0
  120. indonesia_civic_stack-0.1.0/civic_stack/shared/schema.py +110 -0
  121. indonesia_civic_stack-0.1.0/civic_stack/simbg/Dockerfile +10 -0
  122. indonesia_civic_stack-0.1.0/civic_stack/simbg/LICENSE +127 -0
  123. indonesia_civic_stack-0.1.0/civic_stack/simbg/README.md +175 -0
  124. indonesia_civic_stack-0.1.0/civic_stack/simbg/__init__.py +17 -0
  125. indonesia_civic_stack-0.1.0/civic_stack/simbg/app.py +18 -0
  126. indonesia_civic_stack-0.1.0/civic_stack/simbg/normalizer.py +84 -0
  127. indonesia_civic_stack-0.1.0/civic_stack/simbg/router.py +40 -0
  128. indonesia_civic_stack-0.1.0/civic_stack/simbg/scraper.py +188 -0
  129. indonesia_civic_stack-0.1.0/civic_stack/simbg/server.py +56 -0
  130. indonesia_civic_stack-0.1.0/docker-compose.yml +168 -0
  131. indonesia_civic_stack-0.1.0/docs/module-spec.md +124 -0
  132. indonesia_civic_stack-0.1.0/docs/openapi.json +902 -0
  133. indonesia_civic_stack-0.1.0/docs/phase2-tickets.md +252 -0
  134. indonesia_civic_stack-0.1.0/docs/visualiser.html +639 -0
  135. indonesia_civic_stack-0.1.0/examples/halalkah/__init__.py +0 -0
  136. indonesia_civic_stack-0.1.0/examples/halalkah/halal_check.py +203 -0
  137. indonesia_civic_stack-0.1.0/examples/halalkah/migration_guide.md +144 -0
  138. indonesia_civic_stack-0.1.0/examples/halalkah/smoke_test.py +126 -0
  139. indonesia_civic_stack-0.1.0/modules/__init__.py +1 -0
  140. indonesia_civic_stack-0.1.0/modules/ahu/Dockerfile +27 -0
  141. indonesia_civic_stack-0.1.0/modules/ahu/LICENSE +127 -0
  142. indonesia_civic_stack-0.1.0/modules/ahu/README.md +173 -0
  143. indonesia_civic_stack-0.1.0/modules/ahu/__init__.py +17 -0
  144. indonesia_civic_stack-0.1.0/modules/ahu/app.py +18 -0
  145. indonesia_civic_stack-0.1.0/modules/ahu/browser.py +134 -0
  146. indonesia_civic_stack-0.1.0/modules/ahu/normalizer.py +269 -0
  147. indonesia_civic_stack-0.1.0/modules/ahu/router.py +102 -0
  148. indonesia_civic_stack-0.1.0/modules/ahu/scraper.py +165 -0
  149. indonesia_civic_stack-0.1.0/modules/ahu/server.py +116 -0
  150. indonesia_civic_stack-0.1.0/modules/bmkg/Dockerfile +10 -0
  151. indonesia_civic_stack-0.1.0/modules/bmkg/LICENSE +127 -0
  152. indonesia_civic_stack-0.1.0/modules/bmkg/README.md +190 -0
  153. indonesia_civic_stack-0.1.0/modules/bmkg/__init__.py +35 -0
  154. indonesia_civic_stack-0.1.0/modules/bmkg/app.py +15 -0
  155. indonesia_civic_stack-0.1.0/modules/bmkg/normalizer.py +122 -0
  156. indonesia_civic_stack-0.1.0/modules/bmkg/router.py +57 -0
  157. indonesia_civic_stack-0.1.0/modules/bmkg/scraper.py +269 -0
  158. indonesia_civic_stack-0.1.0/modules/bmkg/server.py +71 -0
  159. indonesia_civic_stack-0.1.0/modules/bpjph/Dockerfile +25 -0
  160. indonesia_civic_stack-0.1.0/modules/bpjph/LICENSE +127 -0
  161. indonesia_civic_stack-0.1.0/modules/bpjph/README.md +111 -0
  162. indonesia_civic_stack-0.1.0/modules/bpjph/__init__.py +18 -0
  163. indonesia_civic_stack-0.1.0/modules/bpjph/app.py +18 -0
  164. indonesia_civic_stack-0.1.0/modules/bpjph/browser.py +104 -0
  165. indonesia_civic_stack-0.1.0/modules/bpjph/normalizer.py +231 -0
  166. indonesia_civic_stack-0.1.0/modules/bpjph/router.py +76 -0
  167. indonesia_civic_stack-0.1.0/modules/bpjph/scraper.py +192 -0
  168. indonesia_civic_stack-0.1.0/modules/bpjph/server.py +91 -0
  169. indonesia_civic_stack-0.1.0/modules/bpom/Dockerfile +22 -0
  170. indonesia_civic_stack-0.1.0/modules/bpom/LICENSE +21 -0
  171. indonesia_civic_stack-0.1.0/modules/bpom/README.md +141 -0
  172. indonesia_civic_stack-0.1.0/modules/bpom/__init__.py +17 -0
  173. indonesia_civic_stack-0.1.0/modules/bpom/app.py +18 -0
  174. indonesia_civic_stack-0.1.0/modules/bpom/normalizer.py +170 -0
  175. indonesia_civic_stack-0.1.0/modules/bpom/router.py +84 -0
  176. indonesia_civic_stack-0.1.0/modules/bpom/scraper.py +143 -0
  177. indonesia_civic_stack-0.1.0/modules/bpom/server.py +97 -0
  178. indonesia_civic_stack-0.1.0/modules/bps/Dockerfile +10 -0
  179. indonesia_civic_stack-0.1.0/modules/bps/LICENSE +127 -0
  180. indonesia_civic_stack-0.1.0/modules/bps/README.md +165 -0
  181. indonesia_civic_stack-0.1.0/modules/bps/__init__.py +19 -0
  182. indonesia_civic_stack-0.1.0/modules/bps/app.py +15 -0
  183. indonesia_civic_stack-0.1.0/modules/bps/normalizer.py +90 -0
  184. indonesia_civic_stack-0.1.0/modules/bps/router.py +56 -0
  185. indonesia_civic_stack-0.1.0/modules/bps/scraper.py +204 -0
  186. indonesia_civic_stack-0.1.0/modules/bps/server.py +62 -0
  187. indonesia_civic_stack-0.1.0/modules/kpu/Dockerfile +10 -0
  188. indonesia_civic_stack-0.1.0/modules/kpu/LICENSE +127 -0
  189. indonesia_civic_stack-0.1.0/modules/kpu/README.md +169 -0
  190. indonesia_civic_stack-0.1.0/modules/kpu/__init__.py +17 -0
  191. indonesia_civic_stack-0.1.0/modules/kpu/app.py +17 -0
  192. indonesia_civic_stack-0.1.0/modules/kpu/normalizer.py +101 -0
  193. indonesia_civic_stack-0.1.0/modules/kpu/router.py +66 -0
  194. indonesia_civic_stack-0.1.0/modules/kpu/scraper.py +181 -0
  195. indonesia_civic_stack-0.1.0/modules/kpu/server.py +104 -0
  196. indonesia_civic_stack-0.1.0/modules/lhkpn/Dockerfile +12 -0
  197. indonesia_civic_stack-0.1.0/modules/lhkpn/LICENSE +127 -0
  198. indonesia_civic_stack-0.1.0/modules/lhkpn/README.md +162 -0
  199. indonesia_civic_stack-0.1.0/modules/lhkpn/__init__.py +23 -0
  200. indonesia_civic_stack-0.1.0/modules/lhkpn/app.py +15 -0
  201. indonesia_civic_stack-0.1.0/modules/lhkpn/normalizer.py +125 -0
  202. indonesia_civic_stack-0.1.0/modules/lhkpn/router.py +47 -0
  203. indonesia_civic_stack-0.1.0/modules/lhkpn/scraper.py +320 -0
  204. indonesia_civic_stack-0.1.0/modules/lhkpn/server.py +68 -0
  205. indonesia_civic_stack-0.1.0/modules/lpse/Dockerfile +10 -0
  206. indonesia_civic_stack-0.1.0/modules/lpse/LICENSE +127 -0
  207. indonesia_civic_stack-0.1.0/modules/lpse/README.md +187 -0
  208. indonesia_civic_stack-0.1.0/modules/lpse/__init__.py +18 -0
  209. indonesia_civic_stack-0.1.0/modules/lpse/app.py +15 -0
  210. indonesia_civic_stack-0.1.0/modules/lpse/normalizer.py +96 -0
  211. indonesia_civic_stack-0.1.0/modules/lpse/router.py +41 -0
  212. indonesia_civic_stack-0.1.0/modules/lpse/scraper.py +209 -0
  213. indonesia_civic_stack-0.1.0/modules/lpse/server.py +66 -0
  214. indonesia_civic_stack-0.1.0/modules/ojk/Dockerfile +10 -0
  215. indonesia_civic_stack-0.1.0/modules/ojk/LICENSE +127 -0
  216. indonesia_civic_stack-0.1.0/modules/ojk/README.md +145 -0
  217. indonesia_civic_stack-0.1.0/modules/ojk/__init__.py +17 -0
  218. indonesia_civic_stack-0.1.0/modules/ojk/app.py +17 -0
  219. indonesia_civic_stack-0.1.0/modules/ojk/normalizer.py +106 -0
  220. indonesia_civic_stack-0.1.0/modules/ojk/router.py +87 -0
  221. indonesia_civic_stack-0.1.0/modules/ojk/scraper.py +209 -0
  222. indonesia_civic_stack-0.1.0/modules/ojk/server.py +110 -0
  223. indonesia_civic_stack-0.1.0/modules/oss_nib/Dockerfile +17 -0
  224. indonesia_civic_stack-0.1.0/modules/oss_nib/README.md +138 -0
  225. indonesia_civic_stack-0.1.0/modules/oss_nib/__init__.py +5 -0
  226. indonesia_civic_stack-0.1.0/modules/oss_nib/app.py +17 -0
  227. indonesia_civic_stack-0.1.0/modules/oss_nib/normalizer.py +161 -0
  228. indonesia_civic_stack-0.1.0/modules/oss_nib/router.py +57 -0
  229. indonesia_civic_stack-0.1.0/modules/oss_nib/scraper.py +121 -0
  230. indonesia_civic_stack-0.1.0/modules/oss_nib/server.py +82 -0
  231. indonesia_civic_stack-0.1.0/modules/simbg/Dockerfile +10 -0
  232. indonesia_civic_stack-0.1.0/modules/simbg/LICENSE +127 -0
  233. indonesia_civic_stack-0.1.0/modules/simbg/README.md +175 -0
  234. indonesia_civic_stack-0.1.0/modules/simbg/__init__.py +17 -0
  235. indonesia_civic_stack-0.1.0/modules/simbg/app.py +18 -0
  236. indonesia_civic_stack-0.1.0/modules/simbg/normalizer.py +84 -0
  237. indonesia_civic_stack-0.1.0/modules/simbg/router.py +40 -0
  238. indonesia_civic_stack-0.1.0/modules/simbg/scraper.py +188 -0
  239. indonesia_civic_stack-0.1.0/modules/simbg/server.py +56 -0
  240. indonesia_civic_stack-0.1.0/proxy/README.md +67 -0
  241. indonesia_civic_stack-0.1.0/proxy/worker.js +145 -0
  242. indonesia_civic_stack-0.1.0/proxy/wrangler.toml +3 -0
  243. indonesia_civic_stack-0.1.0/pyproject.toml +105 -0
  244. indonesia_civic_stack-0.1.0/scripts/export_openapi.py +29 -0
  245. indonesia_civic_stack-0.1.0/scripts/test_module.py +201 -0
  246. indonesia_civic_stack-0.1.0/server.py +10 -0
  247. indonesia_civic_stack-0.1.0/shared/__init__.py +5 -0
  248. indonesia_civic_stack-0.1.0/shared/http.py +250 -0
  249. indonesia_civic_stack-0.1.0/shared/mcp.py +115 -0
  250. indonesia_civic_stack-0.1.0/shared/schema.py +110 -0
  251. indonesia_civic_stack-0.1.0/tests/__init__.py +0 -0
  252. indonesia_civic_stack-0.1.0/tests/ahu/__init__.py +0 -0
  253. indonesia_civic_stack-0.1.0/tests/ahu/fixtures/company_found.html +46 -0
  254. indonesia_civic_stack-0.1.0/tests/ahu/fixtures/company_not_found.html +11 -0
  255. indonesia_civic_stack-0.1.0/tests/ahu/fixtures/search_results.html +43 -0
  256. indonesia_civic_stack-0.1.0/tests/ahu/test_ahu.py +223 -0
  257. indonesia_civic_stack-0.1.0/tests/bmkg/__init__.py +0 -0
  258. indonesia_civic_stack-0.1.0/tests/bmkg/cassettes/earthquake_history.yaml +44 -0
  259. indonesia_civic_stack-0.1.0/tests/bmkg/cassettes/earthquake_latest.yaml +38 -0
  260. indonesia_civic_stack-0.1.0/tests/bmkg/test_bmkg.py +133 -0
  261. indonesia_civic_stack-0.1.0/tests/bpjph/__init__.py +0 -0
  262. indonesia_civic_stack-0.1.0/tests/bpjph/fixtures/cert_found.html +23 -0
  263. indonesia_civic_stack-0.1.0/tests/bpjph/fixtures/cert_not_found.html +11 -0
  264. indonesia_civic_stack-0.1.0/tests/bpjph/fixtures/search_results.html +36 -0
  265. indonesia_civic_stack-0.1.0/tests/bpjph/test_bpjph.py +184 -0
  266. indonesia_civic_stack-0.1.0/tests/bpom/__init__.py +0 -0
  267. indonesia_civic_stack-0.1.0/tests/bpom/cassettes/expired.yaml +55 -0
  268. indonesia_civic_stack-0.1.0/tests/bpom/cassettes/found.yaml +55 -0
  269. indonesia_civic_stack-0.1.0/tests/bpom/cassettes/not_found.yaml +46 -0
  270. indonesia_civic_stack-0.1.0/tests/bpom/cassettes/search_multi.yaml +77 -0
  271. indonesia_civic_stack-0.1.0/tests/bpom/test_bpom.py +106 -0
  272. indonesia_civic_stack-0.1.0/tests/bps/__init__.py +0 -0
  273. indonesia_civic_stack-0.1.0/tests/bps/cassettes/dataset_not_found.yaml +24 -0
  274. indonesia_civic_stack-0.1.0/tests/bps/cassettes/dataset_search.yaml +32 -0
  275. indonesia_civic_stack-0.1.0/tests/bps/cassettes/indicator_timeseries.yaml +37 -0
  276. indonesia_civic_stack-0.1.0/tests/bps/test_bps.py +109 -0
  277. indonesia_civic_stack-0.1.0/tests/kpu/__init__.py +0 -0
  278. indonesia_civic_stack-0.1.0/tests/kpu/cassettes/candidate_found.yaml +37 -0
  279. indonesia_civic_stack-0.1.0/tests/kpu/cassettes/candidate_not_found.yaml +19 -0
  280. indonesia_civic_stack-0.1.0/tests/kpu/cassettes/election_results.yaml +38 -0
  281. indonesia_civic_stack-0.1.0/tests/kpu/cassettes/search_multi.yaml +48 -0
  282. indonesia_civic_stack-0.1.0/tests/kpu/test_kpu.py +69 -0
  283. indonesia_civic_stack-0.1.0/tests/lhkpn/__init__.py +0 -0
  284. indonesia_civic_stack-0.1.0/tests/lhkpn/cassettes/official_found.yaml +68 -0
  285. indonesia_civic_stack-0.1.0/tests/lhkpn/cassettes/official_not_found.yaml +26 -0
  286. indonesia_civic_stack-0.1.0/tests/lhkpn/cassettes/search_multi.yaml +43 -0
  287. indonesia_civic_stack-0.1.0/tests/lhkpn/test_lhkpn.py +138 -0
  288. indonesia_civic_stack-0.1.0/tests/lpse/__init__.py +0 -0
  289. indonesia_civic_stack-0.1.0/tests/lpse/cassettes/partial_results.yaml +133 -0
  290. indonesia_civic_stack-0.1.0/tests/lpse/cassettes/tender_search.yaml +128 -0
  291. indonesia_civic_stack-0.1.0/tests/lpse/cassettes/vendor_found.yaml +143 -0
  292. indonesia_civic_stack-0.1.0/tests/lpse/cassettes/vendor_not_found.yaml +112 -0
  293. indonesia_civic_stack-0.1.0/tests/lpse/test_lpse.py +125 -0
  294. indonesia_civic_stack-0.1.0/tests/ojk/__init__.py +0 -0
  295. indonesia_civic_stack-0.1.0/tests/ojk/cassettes/institution_found.yaml +33 -0
  296. indonesia_civic_stack-0.1.0/tests/ojk/cassettes/institution_not_found.yaml +36 -0
  297. indonesia_civic_stack-0.1.0/tests/ojk/cassettes/waspada_found.yaml +32 -0
  298. indonesia_civic_stack-0.1.0/tests/ojk/test_ojk.py +57 -0
  299. indonesia_civic_stack-0.1.0/tests/oss-nib/__init__.py +0 -0
  300. indonesia_civic_stack-0.1.0/tests/oss-nib/fixtures/nib_found.html +23 -0
  301. indonesia_civic_stack-0.1.0/tests/oss-nib/fixtures/nib_not_found.html +8 -0
  302. indonesia_civic_stack-0.1.0/tests/oss-nib/fixtures/search_results.html +27 -0
  303. indonesia_civic_stack-0.1.0/tests/oss-nib/test_oss_nib.py +96 -0
  304. indonesia_civic_stack-0.1.0/tests/simbg/__init__.py +0 -0
  305. indonesia_civic_stack-0.1.0/tests/simbg/cassettes/permit_found.yaml +151 -0
  306. indonesia_civic_stack-0.1.0/tests/simbg/cassettes/permit_not_found.yaml +109 -0
  307. indonesia_civic_stack-0.1.0/tests/simbg/test_simbg.py +111 -0
  308. indonesia_civic_stack-0.1.0/tests/test_shared_schema.py +75 -0
@@ -0,0 +1,15 @@
1
+ This is indonesia-civic-stack — a Python SDK for Indonesian government data.
2
+
3
+ Architecture: shared/ (core) + civic_stack/ (11 gov portal wrappers) + proxy/ (CF Worker).
4
+ All async Python 3.11+, Pydantic v2, httpx, FastMCP for MCP servers.
5
+
6
+ Rules:
7
+ - Use civic_client() from civic_stack.shared.http, never raw httpx.AsyncClient
8
+ - Return CivicStackResponse from civic_stack.shared.schema, never raw dicts
9
+ - On errors: return error_response(), never raise in search()
10
+ - All functions take proxy_url: str | None = None
11
+ - Rate limit with RateLimiter from civic_stack.shared.http
12
+ - Tests use VCR cassettes, never live portal calls
13
+ - Line length 100, ruff formatting
14
+
15
+ Read AGENTS.md for full patterns and CONTRIBUTING.md for module contract.
@@ -0,0 +1,66 @@
1
+ name: Bug report
2
+ description: A module is returning wrong data, erroring unexpectedly, or behaving incorrectly.
3
+ labels: ["bug"]
4
+ body:
5
+ - type: dropdown
6
+ id: module
7
+ attributes:
8
+ label: Module
9
+ description: Which module is affected?
10
+ options:
11
+ - bpom
12
+ - bpjph
13
+ - ahu
14
+ - ojk
15
+ - oss-nib
16
+ - lpse
17
+ - kpu
18
+ - lhkpn
19
+ - bps
20
+ - bmkg
21
+ - simbg
22
+ - shared (schema / http / mcp)
23
+ - other
24
+ validations:
25
+ required: true
26
+
27
+ - type: textarea
28
+ id: description
29
+ attributes:
30
+ label: What happened?
31
+ description: Describe the bug. Include the query you ran, the response you got, and what you expected.
32
+ placeholder: |
33
+ I called `fetch("BPOM MD 123456789012")` and got status=ERROR.
34
+ Expected status=ACTIVE. The portal shows the product as active.
35
+ validations:
36
+ required: true
37
+
38
+ - type: textarea
39
+ id: reproduce
40
+ attributes:
41
+ label: Steps to reproduce
42
+ placeholder: |
43
+ 1. `pip install indonesia-civic-stack`
44
+ 2. `await fetch("BPOM MD 123456789012")`
45
+ 3. Response: {...}
46
+ validations:
47
+ required: true
48
+
49
+ - type: textarea
50
+ id: environment
51
+ attributes:
52
+ label: Environment
53
+ placeholder: |
54
+ - civic-stack version: 0.1.0
55
+ - Python: 3.11.9
56
+ - OS: Ubuntu 22.04
57
+ - proxy_url: yes/no
58
+ validations:
59
+ required: false
60
+
61
+ - type: checkboxes
62
+ id: portal_check
63
+ attributes:
64
+ label: Portal verification
65
+ options:
66
+ - label: I checked the live portal manually and the data is there / the portal is reachable
@@ -0,0 +1,58 @@
1
+ name: Degraded module report
2
+ description: A module has stopped working because the portal changed. Use this to flag it for community fix.
3
+ labels: ["degraded"]
4
+ body:
5
+ - type: dropdown
6
+ id: module
7
+ attributes:
8
+ label: Affected module
9
+ options:
10
+ - bpom
11
+ - bpjph
12
+ - ahu
13
+ - ojk
14
+ - oss-nib
15
+ - lpse
16
+ - kpu
17
+ - lhkpn
18
+ - bps
19
+ - bmkg
20
+ - simbg
21
+ validations:
22
+ required: true
23
+
24
+ - type: textarea
25
+ id: symptom
26
+ attributes:
27
+ label: Symptom
28
+ description: What is failing? Which test or query broke?
29
+ placeholder: |
30
+ fetch("BPOM MD 123456789012") now returns status=ERROR.
31
+ CI cassette `found.yaml` replays fine but live portal returns 404.
32
+ validations:
33
+ required: true
34
+
35
+ - type: textarea
36
+ id: portal_change
37
+ attributes:
38
+ label: What changed on the portal?
39
+ description: If known — new URL, new HTML structure, added JS rendering, etc.
40
+ placeholder: "Portal moved from PHP to React SPA. Table selectors are now dynamic class names."
41
+ validations:
42
+ required: false
43
+
44
+ - type: input
45
+ id: first_seen
46
+ attributes:
47
+ label: When did this break?
48
+ placeholder: "e.g. 2026-03-10 (noticed in weekly CI run)"
49
+ validations:
50
+ required: false
51
+
52
+ - type: checkboxes
53
+ id: fix_interest
54
+ attributes:
55
+ label: Fix interest
56
+ options:
57
+ - label: I am willing to investigate and open a fix PR
58
+ - label: I can provide updated cassettes / fixtures once the portal change is understood
@@ -0,0 +1,81 @@
1
+ name: New module proposal
2
+ description: Propose adding a new Indonesian government data source as a civic-stack module.
3
+ labels: ["module-proposal"]
4
+ body:
5
+ - type: input
6
+ id: module_name
7
+ attributes:
8
+ label: Proposed module name
9
+ placeholder: "e.g. ojk, kpu, lhkpn"
10
+ validations:
11
+ required: true
12
+
13
+ - type: input
14
+ id: source_url
15
+ attributes:
16
+ label: Source portal URL
17
+ placeholder: "e.g. https://ojk.go.id"
18
+ validations:
19
+ required: true
20
+
21
+ - type: input
22
+ id: operator
23
+ attributes:
24
+ label: Portal operator (government agency)
25
+ placeholder: "e.g. Otoritas Jasa Keuangan (OJK)"
26
+ validations:
27
+ required: true
28
+
29
+ - type: dropdown
30
+ id: scrape_method
31
+ attributes:
32
+ label: Scrape method
33
+ options:
34
+ - httpx + BeautifulSoup (static HTML)
35
+ - Playwright (JS-rendered)
36
+ - REST API wrapper
37
+ - PDF extraction (pdfplumber / Claude Vision)
38
+ - Mixed
39
+ validations:
40
+ required: true
41
+
42
+ - type: dropdown
43
+ id: auth_required
44
+ attributes:
45
+ label: Authentication required?
46
+ options:
47
+ - "No — public search, no login"
48
+ - "Partial — public tier available, login for full data"
49
+ - "Yes — login required"
50
+ validations:
51
+ required: true
52
+
53
+ - type: textarea
54
+ id: data_fields
55
+ attributes:
56
+ label: Key data fields the module would expose
57
+ placeholder: |
58
+ - Institution name
59
+ - License number
60
+ - License status (active/revoked)
61
+ - Regulated products
62
+ validations:
63
+ required: true
64
+
65
+ - type: textarea
66
+ id: use_cases
67
+ attributes:
68
+ label: What Kah products or use cases does this enable?
69
+ placeholder: "e.g. legalkah.id financial license verification, AmanKah unlicensed fintech detection"
70
+ validations:
71
+ required: true
72
+
73
+ - type: checkboxes
74
+ id: commitment
75
+ attributes:
76
+ label: Contributor commitment
77
+ description: Check all that apply.
78
+ options:
79
+ - label: I am willing to implement this module (full contract — fetch, search, MCP, VCR fixtures, README)
80
+ - label: I can verify the portal works and document rate limits / block behaviour
81
+ - label: I can maintain this module for at least 6 months after merging
@@ -0,0 +1,23 @@
1
+ # Copilot Instructions
2
+
3
+ This is a Python SDK for Indonesian government data portals.
4
+
5
+ ## Key rules:
6
+ - All functions are async. Use `async def` and `await`.
7
+ - Always use `civic_client()` from `shared.http` — never raw httpx.
8
+ - Every function must accept `proxy_url: str | None = None` parameter.
9
+ - Return `CivicStackResponse` from `shared.schema` — never raw dicts.
10
+ - On errors, return `error_response()` — never raise in `search()`.
11
+ - Rate limit with `RateLimiter` from `shared.http`.
12
+ - Tests use VCR cassettes — never hit live government portals.
13
+ - Python 3.11+, Pydantic v2, ruff for formatting, line length 100.
14
+
15
+ ## File structure per module:
16
+ - `scraper.py` — fetch() and search() functions
17
+ - `normalizer.py` — raw HTML/JSON → dict transformations
18
+ - `router.py` — FastAPI routes
19
+ - `server.py` — MCP server using CivicStackMCPBase
20
+ - `app.py` — FastAPI app
21
+ - `README.md` — module docs
22
+
23
+ See `AGENTS.md` for full architecture guide.
@@ -0,0 +1,38 @@
1
+ # PR Checklist
2
+
3
+ ## Type
4
+ - [ ] New module
5
+ - [ ] Bug fix (module scraper / normalizer)
6
+ - [ ] Fix for degraded module (reference issue: #___)
7
+ - [ ] Shared layer change (schema / http / mcp)
8
+ - [ ] Docs / tests only
9
+
10
+ ## Module contract (required for new modules — skip for non-module PRs)
11
+
12
+ - [ ] `fetch(query, *, debug, proxy_url) -> CivicStackResponse` implemented
13
+ - [ ] `search(keyword, filters, *, proxy_url) -> list[CivicStackResponse]` implemented
14
+ - [ ] MCP server class inheriting `CivicStackMCPBase` with `_register_tools()`
15
+ - [ ] At least 3 VCR cassettes or HTML fixtures (`found`, `not_found`, `error`)
16
+ - [ ] `modules/<name>/README.md` filled from `docs/module-spec.md` template
17
+ - [ ] `modules/<name>/LICENSE` file added (MIT or Apache-2.0 per `LICENSES.md`)
18
+ - [ ] `LICENSES.md` updated with new module entry
19
+
20
+ ## Quality
21
+
22
+ - [ ] `ruff check .` passes
23
+ - [ ] `ruff format --check .` passes
24
+ - [ ] `mypy shared/ modules/<name>/` passes
25
+ - [ ] `pytest tests/<name>/` passes
26
+ - [ ] No live portal calls in any test (VCR `record_mode="none"` or monkeypatched Playwright)
27
+
28
+ ## Description
29
+
30
+ <!-- What does this PR do? Why is it needed? -->
31
+
32
+ ## Test evidence
33
+
34
+ <!-- Paste a `pytest -v tests/<name>/` run or link to CI run -->
35
+
36
+ ## Portal notes
37
+
38
+ <!-- Any quirks discovered, rate limits tested, known block behaviour -->
@@ -0,0 +1,99 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, "claude/**", "feat/**", "fix/**"]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: "0 2 * * 1"
10
+
11
+ env:
12
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
13
+
14
+ jobs:
15
+ lint:
16
+ name: Lint & type-check
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ cache: pip
24
+ - run: pip install -e ".[dev,mcp]"
25
+ - run: ruff check civic_stack/ tests/
26
+ - run: ruff format --check civic_stack/ tests/
27
+ - run: mypy civic_stack/shared/
28
+
29
+ test:
30
+ name: Tests
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.12"
37
+ cache: pip
38
+ - run: pip install -e ".[dev]"
39
+ - name: Run tests (VCR replay, skip browser modules)
40
+ run: |
41
+ python -m pytest tests/ -v --tb=short \
42
+ --ignore=tests/ahu \
43
+ --ignore=tests/bpjph \
44
+ --ignore=tests/oss-nib
45
+ env:
46
+ VCR_RECORD_MODE: none
47
+
48
+ test-browser:
49
+ name: Tests (browser modules)
50
+ runs-on: ubuntu-latest
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+ - uses: actions/setup-python@v5
54
+ with:
55
+ python-version: "3.12"
56
+ cache: pip
57
+ - run: pip install -e ".[dev,browser]"
58
+ - run: playwright install chromium --with-deps
59
+ - name: Run browser module tests
60
+ run: |
61
+ python -m pytest tests/ahu tests/bpjph tests/oss-nib -v --tb=short
62
+ env:
63
+ VCR_RECORD_MODE: none
64
+ continue-on-error: true # Browser tests may fail due to geo-blocking
65
+
66
+ openapi:
67
+ name: Export OpenAPI spec
68
+ runs-on: ubuntu-latest
69
+ needs: [lint, test]
70
+ if: github.ref == 'refs/heads/main'
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: actions/setup-python@v5
74
+ with:
75
+ python-version: "3.12"
76
+ cache: pip
77
+ - run: pip install -e ".[dev,api]"
78
+ - run: python scripts/export_openapi.py
79
+ - uses: actions/upload-artifact@v4
80
+ with:
81
+ name: openapi-spec
82
+ path: docs/openapi.json
83
+
84
+ live-portal-check:
85
+ name: Live portal regression check
86
+ runs-on: ubuntu-latest
87
+ if: github.event_name == 'schedule'
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ - uses: actions/setup-python@v5
91
+ with:
92
+ python-version: "3.12"
93
+ cache: pip
94
+ - run: pip install -e ".[dev]"
95
+ - name: Run live integration tests
96
+ run: python scripts/test_module.py --live
97
+ env:
98
+ PROXY_URL: ${{ secrets.PROXY_URL }}
99
+ continue-on-error: true
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ *.egg-info/
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "civic-stack": {
4
+ "command": ".venv/bin/python",
5
+ "args": ["-m", "civic_stack.server"],
6
+ "env": {
7
+ "PROXY_URL": "",
8
+ "BPS_API_KEY": ""
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,166 @@
1
+ # AGENTS.md — AI Agent Development Guide
2
+
3
+ > This file helps AI coding agents (Claude Code, Codex, Cursor, etc.) work effectively in this repo.
4
+
5
+ ## Project Overview
6
+
7
+ **indonesia-civic-stack** is a Python SDK + MCP server + REST API that wraps 11 Indonesian government data portals into a unified interface. Every module returns `CivicStackResponse`.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ indonesia-civic-stack/
13
+ ├── shared/ # Core shared code (DO NOT break these)
14
+ │ ├── schema.py # CivicStackResponse — the universal envelope
15
+ │ ├── http.py # civic_client(), fetch_with_retry(), proxy support
16
+ │ └── mcp.py # CivicStackMCPBase — base class for MCP servers
17
+ ├── modules/ # One directory per government portal
18
+ │ ├── bpom/ # Example module (Food & Drug registry)
19
+ │ │ ├── scraper.py # fetch() + search() — core logic
20
+ │ │ ├── normalizer.py # Raw HTML/JSON → structured dict
21
+ │ │ ├── router.py # FastAPI routes
22
+ │ │ ├── server.py # MCP server (FastMCP)
23
+ │ │ ├── app.py # FastAPI app entry point
24
+ │ │ ├── Dockerfile
25
+ │ │ └── README.md
26
+ │ └── ... (11 modules total)
27
+ ├── proxy/ # CF Worker proxy for geo-blocked portals
28
+ ├── tests/ # VCR-based tests (no live portal calls in CI)
29
+ └── app.py # Unified FastAPI app (all modules)
30
+ ```
31
+
32
+ ## Key Patterns
33
+
34
+ ### Every module MUST follow this contract:
35
+
36
+ ```python
37
+ # civic_stack/<name>/scraper.py
38
+ async def fetch(query: str, *, proxy_url: str | None = None) -> CivicStackResponse:
39
+ """Single-record lookup by ID."""
40
+
41
+ async def search(keyword: str, *, proxy_url: str | None = None) -> list[CivicStackResponse]:
42
+ """Multi-result keyword search. Returns [] on not-found, never raises."""
43
+ ```
44
+
45
+ ### HTTP client — always use civic_client():
46
+
47
+ ```python
48
+ from civic_stack.shared.http import civic_client, fetch_with_retry
49
+
50
+ async with civic_client(proxy_url=proxy_url) as client:
51
+ response = await fetch_with_retry(client, "GET", url, rate_limiter=_limiter)
52
+ ```
53
+
54
+ `civic_client()` auto-reads `PROXY_URL` from environment. Never create raw `httpx.AsyncClient`.
55
+
56
+ ### MCP servers — two valid patterns:
57
+
58
+ ```python
59
+ # Pattern 1: explicit init
60
+ class BpomMCPServer(CivicStackMCPBase):
61
+ def __init__(self):
62
+ super().__init__("bpom")
63
+ def _register_tools(self): ...
64
+
65
+ # Pattern 2: class attribute
66
+ class BmkgMCPServer(CivicStackMCPBase):
67
+ module_name = "bmkg"
68
+ def _register_tools(self): ...
69
+ ```
70
+
71
+ ### Error handling — return envelopes, don't raise:
72
+
73
+ ```python
74
+ from civic_stack.shared.schema import error_response, not_found_response
75
+
76
+ # When portal is unreachable:
77
+ return error_response("bpom", url, detail="Portal returned 404")
78
+
79
+ # When no results found:
80
+ return not_found_response("bpom", url)
81
+ ```
82
+
83
+ ## Critical Rules
84
+
85
+ 1. **Never break `shared/`** — all 11 modules depend on schema.py, http.py, mcp.py
86
+ 2. **Never hit live portals in tests** — use VCR cassettes (`tests/<module>/cassettes/`)
87
+ 3. **Always return CivicStackResponse** — never return raw dicts or raise on not-found
88
+ 4. **Rate limiting is mandatory** — every scraper must use `RateLimiter` from civic_stack.shared.http
89
+ 5. **Proxy support is mandatory** — every function takes `proxy_url` kwarg and passes to `civic_client()`
90
+ 6. **No data persistence** — modules fetch and return, no databases, no file writes
91
+
92
+ ## Running Tests
93
+
94
+ ```bash
95
+ pytest -v # All tests (VCR replay, no live calls)
96
+ pytest tests/bpom/ -v # Single module
97
+ ruff check . # Lint
98
+ mypy shared/ # Type check
99
+ ```
100
+
101
+ ## Adding a New Module
102
+
103
+ 1. Copy `civic_stack/bpom/` as template
104
+ 2. Implement `scraper.py` (fetch + search)
105
+ 3. Add `normalizer.py`, `router.py`, `server.py`, `app.py`
106
+ 4. Record VCR cassettes: `pytest --vcr-record=new_episodes`
107
+ 5. Add module to README table and `docker-compose.yml`
108
+ 6. See `CONTRIBUTING.md` for full checklist
109
+
110
+ ## Known Gotchas
111
+
112
+ - **Geo-blocking**: Most .go.id portals block non-Indonesian IPs. Set `PROXY_URL` env var.
113
+ - **Portal URL churn**: Indonesian gov portals change URLs without notice. Check issue tracker.
114
+ - **Browser modules**: bpjph, ahu, oss_nib need Playwright. ahu also needs Camoufox.
115
+ - **BPS needs API key**: Set `BPS_API_KEY` env var (free registration).
116
+ - **LHKPN is degraded**: Portal moved behind auth. Module returns errors.
117
+ - **CF Worker proxy has limits**: Can't proxy to CF-protected origins (403/522).
118
+
119
+ ## Environment Variables
120
+
121
+ | Variable | Required | Description |
122
+ |----------|----------|-------------|
123
+ | `PROXY_URL` | For non-ID deploys | Proxy URL (auto-detects *.workers.dev as rewrite mode) |
124
+ | `PROXY_MODE` | No | Override: `connect` or `rewrite` |
125
+ | `BPS_API_KEY` | For BPS module | Free key from webapi.bps.go.id |
126
+ | `CIVIC_API_KEY` | For REST API auth | Set to enable API key checking |
127
+ | `CIVIC_RATE_LIMIT` | No | Requests per minute (default: 60) |
128
+
129
+ ## Common Tasks (Copy-Paste Recipes)
130
+
131
+ ### "Add a new search parameter to an existing module"
132
+ 1. Read `civic_stack/<name>/scraper.py` — find the `search()` function
133
+ 2. Add the new parameter to the function signature (with default `None`)
134
+ 3. Pass it to `fetch_with_retry()` via `params=` dict
135
+ 4. Update `civic_stack/<name>/server.py` — add parameter to MCP tool
136
+ 5. Update `civic_stack/<name>/router.py` — add query param to FastAPI endpoint
137
+ 6. Record new VCR cassette: `pytest tests/<name>/ --vcr-record=new_episodes`
138
+
139
+ ### "Fix a broken portal URL"
140
+ 1. Check the portal manually: `curl -sI "https://portal.go.id/old-path"`
141
+ 2. Find the new URL (check portal homepage, inspect network tab)
142
+ 3. Update the URL constant at top of `civic_stack/<name>/scraper.py`
143
+ 4. Re-record VCR cassettes
144
+ 5. Update `civic_stack/<name>/README.md` with the URL change
145
+ 6. Check if other modules reference the same portal
146
+
147
+ ### "Add proxy support to a new function"
148
+ 1. Add `proxy_url: str | None = None` to function signature
149
+ 2. Pass to `civic_client(proxy_url=proxy_url)` — that's it
150
+ 3. `civic_client()` auto-reads `PROXY_URL` from env when `proxy_url=None`
151
+
152
+ ### "Debug a failing scraper"
153
+ 1. Check if it's a geo-block: `curl -sI "https://portal.go.id" | head -5`
154
+ 2. Check if URL changed: compare with `civic_stack/<name>/README.md`
155
+ 3. Check portal HTML structure: `curl -s "https://portal.go.id" | python -m bs4`
156
+ 4. Run with debug: `await fetch(query, debug=True)` — check `.raw` field in response
157
+ 5. Check GitHub issues for known portal changes
158
+
159
+ ## File Conventions
160
+
161
+ - `scraper.py` — all HTTP logic lives here
162
+ - `normalizer.py` — transforms raw HTML/JSON to structured dicts
163
+ - `router.py` — FastAPI routes (thin, delegates to scraper)
164
+ - `server.py` — MCP server (thin, delegates to scraper)
165
+ - Module constants (URLs, rate limits) go at top of scraper.py
166
+ - Use `MODULE = "module_name"` constant in every scraper
@@ -0,0 +1,67 @@
1
+ # CLAUDE.md — Instructions for Claude Code
2
+
3
+ ## MCP Integration
4
+
5
+ This repo includes `.mcp.json` — Claude Code auto-discovers 40 MCP tools on startup.
6
+ The unified server (`server.py`) registers all tools from all 11 modules in one process.
7
+
8
+ ## Quick Context
9
+
10
+ This is a Python SDK for Indonesian government data. 11 modules, each wrapping a gov portal.
11
+ Shared layer in `shared/`. Tests use VCR (no live calls). All async, Pydantic v2, Python 3.11+.
12
+
13
+ ## Before Writing Code
14
+
15
+ 1. Read `AGENTS.md` for architecture and patterns
16
+ 2. Read `CONTRIBUTING.md` for the module contract
17
+ 3. Check the module's `README.md` for portal-specific quirks
18
+ 4. Check GitHub issues for known portal changes
19
+
20
+ ## Commands
21
+
22
+ ```bash
23
+ # Test
24
+ pytest -v
25
+ pytest tests/<module>/ -v
26
+
27
+ # Lint + format
28
+ ruff check . && ruff format --check .
29
+
30
+ # Type check
31
+ mypy shared/ civic_stack/
32
+
33
+ # Run unified MCP server (40 tools)
34
+ python server.py
35
+
36
+ # Run single module MCP server
37
+ python -m civic_stack.bpom.server
38
+
39
+ # Run single module REST API
40
+ uvicorn modules.bpom.app:app --port 8001
41
+
42
+ # Run unified REST API
43
+ uvicorn app:app --port 8000
44
+
45
+ # Build & deploy proxy
46
+ cd proxy && npx wrangler deploy
47
+
48
+ # Verify MCP tools load
49
+ python -c "import asyncio; from server import mcp; print(asyncio.run(mcp.list_tools()))"
50
+ ```
51
+
52
+ ## Do Not
53
+
54
+ - Create raw `httpx.AsyncClient` — use `civic_client()`
55
+ - Raise exceptions from `search()` — return `error_response()` envelope
56
+ - Hit live portals in tests — record VCR cassettes
57
+ - Modify `shared/schema.py` without checking all 11 modules
58
+ - Remove proxy_url parameter from any function signature
59
+ - Use `print()` — use `logger` from `logging`
60
+
61
+ ## Style
62
+
63
+ - Line length: 100
64
+ - Async-first: `async def`, `httpx.AsyncClient`
65
+ - Type hints on all public functions
66
+ - Pydantic v2 models
67
+ - `ruff` for linting and formatting