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.
- indonesia_civic_stack-0.1.0/.cursorrules +15 -0
- indonesia_civic_stack-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +66 -0
- indonesia_civic_stack-0.1.0/.github/ISSUE_TEMPLATE/degraded_module.yml +58 -0
- indonesia_civic_stack-0.1.0/.github/ISSUE_TEMPLATE/module_addition.yml +81 -0
- indonesia_civic_stack-0.1.0/.github/copilot-instructions.md +23 -0
- indonesia_civic_stack-0.1.0/.github/pull_request_template.md +38 -0
- indonesia_civic_stack-0.1.0/.github/workflows/ci.yml +99 -0
- indonesia_civic_stack-0.1.0/.gitignore +5 -0
- indonesia_civic_stack-0.1.0/.mcp.json +12 -0
- indonesia_civic_stack-0.1.0/AGENTS.md +166 -0
- indonesia_civic_stack-0.1.0/CLAUDE.md +67 -0
- indonesia_civic_stack-0.1.0/CONTRIBUTING.md +127 -0
- indonesia_civic_stack-0.1.0/LICENSE +21 -0
- indonesia_civic_stack-0.1.0/LICENSES.md +26 -0
- indonesia_civic_stack-0.1.0/PKG-INFO +870 -0
- indonesia_civic_stack-0.1.0/PROMPTS.md +202 -0
- indonesia_civic_stack-0.1.0/README.md +821 -0
- indonesia_civic_stack-0.1.0/SHAPES.MD +317 -0
- indonesia_civic_stack-0.1.0/SKILL.md +98 -0
- indonesia_civic_stack-0.1.0/SPRINT2-PREP.md +102 -0
- indonesia_civic_stack-0.1.0/app.py +102 -0
- indonesia_civic_stack-0.1.0/civic_stack/__init__.py +41 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/Dockerfile +27 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/README.md +173 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/app.py +18 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/browser.py +134 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/normalizer.py +269 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/router.py +102 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/scraper.py +165 -0
- indonesia_civic_stack-0.1.0/civic_stack/ahu/server.py +116 -0
- indonesia_civic_stack-0.1.0/civic_stack/app.py +102 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/README.md +190 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/__init__.py +35 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/app.py +15 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/normalizer.py +122 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/router.py +57 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/scraper.py +274 -0
- indonesia_civic_stack-0.1.0/civic_stack/bmkg/server.py +71 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/Dockerfile +25 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/README.md +111 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/__init__.py +18 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/app.py +18 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/browser.py +104 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/normalizer.py +231 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/router.py +76 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/scraper.py +192 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpjph/server.py +91 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/Dockerfile +22 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/LICENSE +21 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/README.md +141 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/app.py +18 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/normalizer.py +170 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/router.py +84 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/scraper.py +143 -0
- indonesia_civic_stack-0.1.0/civic_stack/bpom/server.py +97 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/README.md +165 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/__init__.py +19 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/app.py +15 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/normalizer.py +90 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/router.py +56 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/scraper.py +217 -0
- indonesia_civic_stack-0.1.0/civic_stack/bps/server.py +62 -0
- indonesia_civic_stack-0.1.0/civic_stack/cli.py +70 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/README.md +169 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/app.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/normalizer.py +101 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/router.py +66 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/scraper.py +181 -0
- indonesia_civic_stack-0.1.0/civic_stack/kpu/server.py +104 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/Dockerfile +12 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/README.md +162 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/__init__.py +23 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/app.py +15 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/normalizer.py +125 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/router.py +47 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/scraper.py +325 -0
- indonesia_civic_stack-0.1.0/civic_stack/lhkpn/server.py +68 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/README.md +187 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/__init__.py +18 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/app.py +15 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/normalizer.py +96 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/router.py +41 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/scraper.py +209 -0
- indonesia_civic_stack-0.1.0/civic_stack/lpse/server.py +66 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/README.md +145 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/app.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/normalizer.py +106 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/router.py +87 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/scraper.py +209 -0
- indonesia_civic_stack-0.1.0/civic_stack/ojk/server.py +110 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/Dockerfile +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/README.md +138 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/__init__.py +5 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/app.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/normalizer.py +161 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/router.py +57 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/scraper.py +121 -0
- indonesia_civic_stack-0.1.0/civic_stack/oss_nib/server.py +82 -0
- indonesia_civic_stack-0.1.0/civic_stack/server.py +494 -0
- indonesia_civic_stack-0.1.0/civic_stack/shared/__init__.py +5 -0
- indonesia_civic_stack-0.1.0/civic_stack/shared/http.py +265 -0
- indonesia_civic_stack-0.1.0/civic_stack/shared/mcp.py +115 -0
- indonesia_civic_stack-0.1.0/civic_stack/shared/schema.py +110 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/README.md +175 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/app.py +18 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/normalizer.py +84 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/router.py +40 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/scraper.py +188 -0
- indonesia_civic_stack-0.1.0/civic_stack/simbg/server.py +56 -0
- indonesia_civic_stack-0.1.0/docker-compose.yml +168 -0
- indonesia_civic_stack-0.1.0/docs/module-spec.md +124 -0
- indonesia_civic_stack-0.1.0/docs/openapi.json +902 -0
- indonesia_civic_stack-0.1.0/docs/phase2-tickets.md +252 -0
- indonesia_civic_stack-0.1.0/docs/visualiser.html +639 -0
- indonesia_civic_stack-0.1.0/examples/halalkah/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/examples/halalkah/halal_check.py +203 -0
- indonesia_civic_stack-0.1.0/examples/halalkah/migration_guide.md +144 -0
- indonesia_civic_stack-0.1.0/examples/halalkah/smoke_test.py +126 -0
- indonesia_civic_stack-0.1.0/modules/__init__.py +1 -0
- indonesia_civic_stack-0.1.0/modules/ahu/Dockerfile +27 -0
- indonesia_civic_stack-0.1.0/modules/ahu/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/ahu/README.md +173 -0
- indonesia_civic_stack-0.1.0/modules/ahu/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/modules/ahu/app.py +18 -0
- indonesia_civic_stack-0.1.0/modules/ahu/browser.py +134 -0
- indonesia_civic_stack-0.1.0/modules/ahu/normalizer.py +269 -0
- indonesia_civic_stack-0.1.0/modules/ahu/router.py +102 -0
- indonesia_civic_stack-0.1.0/modules/ahu/scraper.py +165 -0
- indonesia_civic_stack-0.1.0/modules/ahu/server.py +116 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/README.md +190 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/__init__.py +35 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/app.py +15 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/normalizer.py +122 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/router.py +57 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/scraper.py +269 -0
- indonesia_civic_stack-0.1.0/modules/bmkg/server.py +71 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/Dockerfile +25 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/README.md +111 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/__init__.py +18 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/app.py +18 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/browser.py +104 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/normalizer.py +231 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/router.py +76 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/scraper.py +192 -0
- indonesia_civic_stack-0.1.0/modules/bpjph/server.py +91 -0
- indonesia_civic_stack-0.1.0/modules/bpom/Dockerfile +22 -0
- indonesia_civic_stack-0.1.0/modules/bpom/LICENSE +21 -0
- indonesia_civic_stack-0.1.0/modules/bpom/README.md +141 -0
- indonesia_civic_stack-0.1.0/modules/bpom/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/modules/bpom/app.py +18 -0
- indonesia_civic_stack-0.1.0/modules/bpom/normalizer.py +170 -0
- indonesia_civic_stack-0.1.0/modules/bpom/router.py +84 -0
- indonesia_civic_stack-0.1.0/modules/bpom/scraper.py +143 -0
- indonesia_civic_stack-0.1.0/modules/bpom/server.py +97 -0
- indonesia_civic_stack-0.1.0/modules/bps/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/modules/bps/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/bps/README.md +165 -0
- indonesia_civic_stack-0.1.0/modules/bps/__init__.py +19 -0
- indonesia_civic_stack-0.1.0/modules/bps/app.py +15 -0
- indonesia_civic_stack-0.1.0/modules/bps/normalizer.py +90 -0
- indonesia_civic_stack-0.1.0/modules/bps/router.py +56 -0
- indonesia_civic_stack-0.1.0/modules/bps/scraper.py +204 -0
- indonesia_civic_stack-0.1.0/modules/bps/server.py +62 -0
- indonesia_civic_stack-0.1.0/modules/kpu/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/modules/kpu/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/kpu/README.md +169 -0
- indonesia_civic_stack-0.1.0/modules/kpu/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/modules/kpu/app.py +17 -0
- indonesia_civic_stack-0.1.0/modules/kpu/normalizer.py +101 -0
- indonesia_civic_stack-0.1.0/modules/kpu/router.py +66 -0
- indonesia_civic_stack-0.1.0/modules/kpu/scraper.py +181 -0
- indonesia_civic_stack-0.1.0/modules/kpu/server.py +104 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/Dockerfile +12 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/README.md +162 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/__init__.py +23 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/app.py +15 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/normalizer.py +125 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/router.py +47 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/scraper.py +320 -0
- indonesia_civic_stack-0.1.0/modules/lhkpn/server.py +68 -0
- indonesia_civic_stack-0.1.0/modules/lpse/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/modules/lpse/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/lpse/README.md +187 -0
- indonesia_civic_stack-0.1.0/modules/lpse/__init__.py +18 -0
- indonesia_civic_stack-0.1.0/modules/lpse/app.py +15 -0
- indonesia_civic_stack-0.1.0/modules/lpse/normalizer.py +96 -0
- indonesia_civic_stack-0.1.0/modules/lpse/router.py +41 -0
- indonesia_civic_stack-0.1.0/modules/lpse/scraper.py +209 -0
- indonesia_civic_stack-0.1.0/modules/lpse/server.py +66 -0
- indonesia_civic_stack-0.1.0/modules/ojk/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/modules/ojk/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/ojk/README.md +145 -0
- indonesia_civic_stack-0.1.0/modules/ojk/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/modules/ojk/app.py +17 -0
- indonesia_civic_stack-0.1.0/modules/ojk/normalizer.py +106 -0
- indonesia_civic_stack-0.1.0/modules/ojk/router.py +87 -0
- indonesia_civic_stack-0.1.0/modules/ojk/scraper.py +209 -0
- indonesia_civic_stack-0.1.0/modules/ojk/server.py +110 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/Dockerfile +17 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/README.md +138 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/__init__.py +5 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/app.py +17 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/normalizer.py +161 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/router.py +57 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/scraper.py +121 -0
- indonesia_civic_stack-0.1.0/modules/oss_nib/server.py +82 -0
- indonesia_civic_stack-0.1.0/modules/simbg/Dockerfile +10 -0
- indonesia_civic_stack-0.1.0/modules/simbg/LICENSE +127 -0
- indonesia_civic_stack-0.1.0/modules/simbg/README.md +175 -0
- indonesia_civic_stack-0.1.0/modules/simbg/__init__.py +17 -0
- indonesia_civic_stack-0.1.0/modules/simbg/app.py +18 -0
- indonesia_civic_stack-0.1.0/modules/simbg/normalizer.py +84 -0
- indonesia_civic_stack-0.1.0/modules/simbg/router.py +40 -0
- indonesia_civic_stack-0.1.0/modules/simbg/scraper.py +188 -0
- indonesia_civic_stack-0.1.0/modules/simbg/server.py +56 -0
- indonesia_civic_stack-0.1.0/proxy/README.md +67 -0
- indonesia_civic_stack-0.1.0/proxy/worker.js +145 -0
- indonesia_civic_stack-0.1.0/proxy/wrangler.toml +3 -0
- indonesia_civic_stack-0.1.0/pyproject.toml +105 -0
- indonesia_civic_stack-0.1.0/scripts/export_openapi.py +29 -0
- indonesia_civic_stack-0.1.0/scripts/test_module.py +201 -0
- indonesia_civic_stack-0.1.0/server.py +10 -0
- indonesia_civic_stack-0.1.0/shared/__init__.py +5 -0
- indonesia_civic_stack-0.1.0/shared/http.py +250 -0
- indonesia_civic_stack-0.1.0/shared/mcp.py +115 -0
- indonesia_civic_stack-0.1.0/shared/schema.py +110 -0
- indonesia_civic_stack-0.1.0/tests/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/ahu/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/ahu/fixtures/company_found.html +46 -0
- indonesia_civic_stack-0.1.0/tests/ahu/fixtures/company_not_found.html +11 -0
- indonesia_civic_stack-0.1.0/tests/ahu/fixtures/search_results.html +43 -0
- indonesia_civic_stack-0.1.0/tests/ahu/test_ahu.py +223 -0
- indonesia_civic_stack-0.1.0/tests/bmkg/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/bmkg/cassettes/earthquake_history.yaml +44 -0
- indonesia_civic_stack-0.1.0/tests/bmkg/cassettes/earthquake_latest.yaml +38 -0
- indonesia_civic_stack-0.1.0/tests/bmkg/test_bmkg.py +133 -0
- indonesia_civic_stack-0.1.0/tests/bpjph/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/bpjph/fixtures/cert_found.html +23 -0
- indonesia_civic_stack-0.1.0/tests/bpjph/fixtures/cert_not_found.html +11 -0
- indonesia_civic_stack-0.1.0/tests/bpjph/fixtures/search_results.html +36 -0
- indonesia_civic_stack-0.1.0/tests/bpjph/test_bpjph.py +184 -0
- indonesia_civic_stack-0.1.0/tests/bpom/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/bpom/cassettes/expired.yaml +55 -0
- indonesia_civic_stack-0.1.0/tests/bpom/cassettes/found.yaml +55 -0
- indonesia_civic_stack-0.1.0/tests/bpom/cassettes/not_found.yaml +46 -0
- indonesia_civic_stack-0.1.0/tests/bpom/cassettes/search_multi.yaml +77 -0
- indonesia_civic_stack-0.1.0/tests/bpom/test_bpom.py +106 -0
- indonesia_civic_stack-0.1.0/tests/bps/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/bps/cassettes/dataset_not_found.yaml +24 -0
- indonesia_civic_stack-0.1.0/tests/bps/cassettes/dataset_search.yaml +32 -0
- indonesia_civic_stack-0.1.0/tests/bps/cassettes/indicator_timeseries.yaml +37 -0
- indonesia_civic_stack-0.1.0/tests/bps/test_bps.py +109 -0
- indonesia_civic_stack-0.1.0/tests/kpu/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/kpu/cassettes/candidate_found.yaml +37 -0
- indonesia_civic_stack-0.1.0/tests/kpu/cassettes/candidate_not_found.yaml +19 -0
- indonesia_civic_stack-0.1.0/tests/kpu/cassettes/election_results.yaml +38 -0
- indonesia_civic_stack-0.1.0/tests/kpu/cassettes/search_multi.yaml +48 -0
- indonesia_civic_stack-0.1.0/tests/kpu/test_kpu.py +69 -0
- indonesia_civic_stack-0.1.0/tests/lhkpn/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/lhkpn/cassettes/official_found.yaml +68 -0
- indonesia_civic_stack-0.1.0/tests/lhkpn/cassettes/official_not_found.yaml +26 -0
- indonesia_civic_stack-0.1.0/tests/lhkpn/cassettes/search_multi.yaml +43 -0
- indonesia_civic_stack-0.1.0/tests/lhkpn/test_lhkpn.py +138 -0
- indonesia_civic_stack-0.1.0/tests/lpse/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/lpse/cassettes/partial_results.yaml +133 -0
- indonesia_civic_stack-0.1.0/tests/lpse/cassettes/tender_search.yaml +128 -0
- indonesia_civic_stack-0.1.0/tests/lpse/cassettes/vendor_found.yaml +143 -0
- indonesia_civic_stack-0.1.0/tests/lpse/cassettes/vendor_not_found.yaml +112 -0
- indonesia_civic_stack-0.1.0/tests/lpse/test_lpse.py +125 -0
- indonesia_civic_stack-0.1.0/tests/ojk/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/ojk/cassettes/institution_found.yaml +33 -0
- indonesia_civic_stack-0.1.0/tests/ojk/cassettes/institution_not_found.yaml +36 -0
- indonesia_civic_stack-0.1.0/tests/ojk/cassettes/waspada_found.yaml +32 -0
- indonesia_civic_stack-0.1.0/tests/ojk/test_ojk.py +57 -0
- indonesia_civic_stack-0.1.0/tests/oss-nib/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/oss-nib/fixtures/nib_found.html +23 -0
- indonesia_civic_stack-0.1.0/tests/oss-nib/fixtures/nib_not_found.html +8 -0
- indonesia_civic_stack-0.1.0/tests/oss-nib/fixtures/search_results.html +27 -0
- indonesia_civic_stack-0.1.0/tests/oss-nib/test_oss_nib.py +96 -0
- indonesia_civic_stack-0.1.0/tests/simbg/__init__.py +0 -0
- indonesia_civic_stack-0.1.0/tests/simbg/cassettes/permit_found.yaml +151 -0
- indonesia_civic_stack-0.1.0/tests/simbg/cassettes/permit_not_found.yaml +109 -0
- indonesia_civic_stack-0.1.0/tests/simbg/test_simbg.py +111 -0
- 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,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
|