bantamkit 0.35.0__tar.gz → 0.35.2__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.
- {bantamkit-0.35.0 → bantamkit-0.35.2}/.gitignore +2 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/PKG-INFO +4 -4
- {bantamkit-0.35.0 → bantamkit-0.35.2}/README.md +2 -2
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/__init__.py +1 -1
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/mcpserver.py +13 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/selfupdate.py +89 -0
- bantamkit-0.35.2/src/bantamkit/updatecheck.py +340 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_selfupdate.py +257 -1
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_status_surface.py +109 -1
- bantamkit-0.35.2/tests/test_updatecheck.py +541 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/contracts/default.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/manifest.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/HISTORY.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/README.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/fixtures/.gitkeep +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/fixtures/catalog.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/perturbations/task-completion.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/.gitkeep +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/extract-contact.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/extract-invoice.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/extract-order.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/extract-schedule.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/extract-versions.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-db-port.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-deploy.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-oncall.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/recall-owner.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/shop-compare.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/evals/tasks/shop-total.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/pricing/default.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/profiles/default.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/profiles/patient.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/rubrics/.gitkeep +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/rubrics/code-quality.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/rubrics/grounded-completion.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/rubrics/task-completion.yaml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/schemas/shiftwork-checkpoint.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/skills/.gitkeep +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/skills/file-graph.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/skills/memory.md +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/.gitkeep +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/bantamkit_read.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/bantamkit_status.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/build_identity.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/document_list.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/document_read.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/file_graph.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/memory_compact.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/memory_dream.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/memory_recall.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/memory_save.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/repo_map.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/shiftwork_clock_in.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/shiftwork_clock_out.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/shiftwork_plan.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/shiftwork_status.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/skill_audit.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/token_ledger.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/validate_json.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/_assets/tools/work_plan.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/hatch_build.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/pyproject.toml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/agent.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/assets.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/budget.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/client.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/contract.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/criticreplay.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/critique.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/docmanifest.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/docread.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/evalrun.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/eventlog.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/filegraph.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/hostinstall.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/loopguard.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/mcpreport.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/__init__.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/__main__.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/component.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/divergence.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/dream.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/layers.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/memory/store.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/pdfread.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/pricing.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/profile.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/repomap.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/shiftwork.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/skillaudit.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/statusline.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/structured.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/textutil.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/tokenledger.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/src/bantamkit/workplan.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/cli_exit_status_probe.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/conftest.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/bad-crc.docx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/charref-4301-digits.html +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/charset-table.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/compression-method-9.docx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/corrupt-deflate.docx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/encrypted-member.docx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/encrypted-mimetype.odt +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/internal-dtd-entity.docx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/rfc2231-charset.eml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/rfc822-nested-twice.eml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/docread/x-uuencode.eml +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/f8404ab-perturbation-baseline.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/platform-assumption-baseline.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/data/served-tool-surface.json +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/docread_fixtures.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/perturbation_baseline_harness.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/rbp16_effect_probe.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/rbp18_payload_probe.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_adapter.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_agent.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_amendguard.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_bantamkit_gitignore.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_bantamkit_read_tool.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_budget.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_build_identity.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_client.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_compaction_corpus_survey.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_conformance.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_conformance_harness_resilience.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_conformance_suite_table_gate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_contract_fanout.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_criticreplay.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_critique.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_doc_commands_gate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_docread.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_docread_ceilings.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_document_manifest_parity.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_document_setup.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_document_tasks.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_document_tools.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_encoding_gate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_evalrun.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_eventlog.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_field_program_gates.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_field_programs.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_filegraph.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_hostinstall.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_install_shape.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_ladder_statistics.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_launcher_which.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_layers.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_loopguard.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_mcp_endpoint.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_mcpdrift.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_mcpreport.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_mcpserver.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory_compact_tool.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory_component.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory_divergence.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory_dream.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory_layers.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_memory_store_tripwire.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_mutmatrix.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_newline_gate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_packaging.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_pdfread.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_pinharness_ledger.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_platform_assumption_gate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_pricing.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_repo_map_tool.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_repomap.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_served_tool_count_records.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_shiftwork.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_skillaudit.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_statusline.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_structured.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_thread_exception_gate.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_tokenledger.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_tool_manifest.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_version_agreement.py +0 -0
- {bantamkit-0.35.0 → bantamkit-0.35.2}/tests/test_workplan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bantamkit
|
|
3
|
-
Version: 0.35.
|
|
3
|
+
Version: 0.35.2
|
|
4
4
|
Summary: Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop, plus a Python library that lifts small-model agents. Install once, run offline.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Ink01101011/bantamkit
|
|
6
6
|
Project-URL: Repository, https://github.com/Ink01101011/bantamkit
|
|
@@ -97,9 +97,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
|
|
|
97
97
|
one platform only), copy `wheels/` across, and install from it:
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
|
-
python -m pip download "bantamkit[mcp]==0.35.
|
|
100
|
+
python -m pip download "bantamkit[mcp]==0.35.2" -d wheels
|
|
101
101
|
python -m venv <env>
|
|
102
|
-
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.
|
|
102
|
+
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.2"
|
|
103
103
|
<env>/bin/bantamkit-mcp --install cursor
|
|
104
104
|
```
|
|
105
105
|
|
|
@@ -69,9 +69,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
|
|
|
69
69
|
one platform only), copy `wheels/` across, and install from it:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
python -m pip download "bantamkit[mcp]==0.35.
|
|
72
|
+
python -m pip download "bantamkit[mcp]==0.35.2" -d wheels
|
|
73
73
|
python -m venv <env>
|
|
74
|
-
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.
|
|
74
|
+
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.2"
|
|
75
75
|
<env>/bin/bantamkit-mcp --install cursor
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -29,4 +29,4 @@ from bantamkit.structured import StructuredOutputError, extract_json, structured
|
|
|
29
29
|
# file as its dynamic version source, so the wheel's metadata and the string the MCP
|
|
30
30
|
# server advertises are the same committed bytes, and neither is a function of when
|
|
31
31
|
# someone last ran `pip`.
|
|
32
|
-
__version__ = "0.35.
|
|
32
|
+
__version__ = "0.35.2"
|
|
@@ -30,6 +30,7 @@ from bantamkit import (
|
|
|
30
30
|
shiftwork,
|
|
31
31
|
skillaudit,
|
|
32
32
|
tokenledger,
|
|
33
|
+
updatecheck,
|
|
33
34
|
workplan,
|
|
34
35
|
)
|
|
35
36
|
from bantamkit.assets import AssetNotFound, assets_root, load_skill, load_tool_asset
|
|
@@ -965,6 +966,18 @@ def status_report(
|
|
|
965
966
|
+ ("unreadable" if size is None else str(size))
|
|
966
967
|
+ f" of {budget} bytes",
|
|
967
968
|
f"event log: {'on' if log.enabled else 'off'}",
|
|
969
|
+
# THE UPDATE LINE IS NOT A CONDITION, and that is the ruling of 2026-09-19 rather
|
|
970
|
+
# than an omission. A newer version existing is not a fault: the server is serving
|
|
971
|
+
# correctly, and `degraded_notice`'s docstring above is the operator's own reason —
|
|
972
|
+
# a footer on every result is noise, and noise trains a reader to skip it. So this
|
|
973
|
+
# never flips line 1, never enters `degraded_conditions`, and never rides another
|
|
974
|
+
# tool's reply. It also means the record is opened ONLY here, when someone asked for
|
|
975
|
+
# a report, and not on the per-call path whose cost `docs/status.md` documents.
|
|
976
|
+
#
|
|
977
|
+
# `updatecheck` opens one file and asks nobody anything — AS-7(3) is intact, and
|
|
978
|
+
# `test_selfupdate.py::test_only_the_update_flag_reaches_this_module_from_the_server`
|
|
979
|
+
# still holds, because the name this reaches is `updatecheck` and not `selfupdate`.
|
|
980
|
+
updatecheck.update_line(str(identity["version"])),
|
|
968
981
|
]
|
|
969
982
|
if conditions:
|
|
970
983
|
lines.append(f"{_plural(len(conditions), 'problem')}:")
|
|
@@ -55,11 +55,14 @@ a store or a transport exists, the same shape `--assets-root` and `--install` us
|
|
|
55
55
|
from __future__ import annotations
|
|
56
56
|
|
|
57
57
|
import json
|
|
58
|
+
import os
|
|
58
59
|
import shlex
|
|
59
60
|
import subprocess
|
|
60
61
|
import sys
|
|
62
|
+
import tempfile
|
|
61
63
|
from collections.abc import Callable
|
|
62
64
|
from dataclasses import dataclass
|
|
65
|
+
from datetime import UTC, datetime
|
|
63
66
|
|
|
64
67
|
from bantamkit import __version__
|
|
65
68
|
|
|
@@ -359,6 +362,83 @@ def compare_versions(installed: str, latest: str) -> int:
|
|
|
359
362
|
return 1 if left > right else -1
|
|
360
363
|
|
|
361
364
|
|
|
365
|
+
#: The `checked_at` stamp's shape, spelled rather than defaulted. `datetime.isoformat()`
|
|
366
|
+
#: renders `+00:00` and `Date.prototype.toISOString` renders `.000Z`, so a record written by
|
|
367
|
+
#: the two runtimes would differ in bytes neither reader cares about. Both sides render this
|
|
368
|
+
#: one shape instead, and `runtime-ts` strips its milliseconds to reach it.
|
|
369
|
+
STAMP = "%Y-%m-%dT%H:%M:%SZ"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def record_update(latest: str, now: str | None = None) -> bool:
|
|
373
|
+
"""Put the version this flag just fetched into the record `bantamkit_status` reads.
|
|
374
|
+
|
|
375
|
+
NO SECOND NETWORK CALL AND NO NEW FAILURE MODE, which is the whole reason the writer is
|
|
376
|
+
here rather than anywhere a reader could reach. `update` already holds `latest` — it was
|
|
377
|
+
parsed out of the answer the operator's own `--update` asked for — so this writes what is
|
|
378
|
+
in hand. `updatecheck` reads that file and NEVER writes it; the split is structural and
|
|
379
|
+
both halves are gated (`test_updatecheck.py::test_the_source_writes_nothing_and_reaches_
|
|
380
|
+
no_network`), so a future reader cannot grow a write by accident.
|
|
381
|
+
|
|
382
|
+
`updatecheck` IS IMPORTED INSIDE THE BODY, and not for style. `updatecheck` imports
|
|
383
|
+
`PROGRAM` and `compare_versions` from THIS module at its top level, so a module-level
|
|
384
|
+
import here would be a cycle that fails at interpreter start. The function-local import
|
|
385
|
+
is the direction that works, and `_names_reached_by` in `test_selfupdate.py` sees it —
|
|
386
|
+
`bantamkit.updatecheck` is on that gate's allowlist by name.
|
|
387
|
+
|
|
388
|
+
IT CREATES NO DIRECTORY. `<homedir>/.bantamkit` is made by an install, and a `--update`
|
|
389
|
+
run on a machine that has never had one writes nothing rather than deciding where this
|
|
390
|
+
toolbox's home directory should be. A cwd-relative `.bantamkit` is a MEMORY STORE (J54-3)
|
|
391
|
+
and the path here comes from `updatecheck.record_path()`, which hangs off `_home()`.
|
|
392
|
+
|
|
393
|
+
THE OTHER RUNTIME'S KEY IS LEFT EXACTLY AS FOUND. npm and PyPI are two registries that
|
|
394
|
+
can disagree at one version number — job56 shipped a day where they did — so this fills
|
|
395
|
+
`updatecheck.KEY` and copies the rest of the record through untouched. An UNREADABLE
|
|
396
|
+
record is replaced rather than merged: there is nothing in it to preserve.
|
|
397
|
+
|
|
398
|
+
TEMP FILE AND RENAME, so a reader never sees half a record: `os.replace` is atomic on
|
|
399
|
+
POSIX and on Windows, and the temp file is made in the SAME directory so the rename is
|
|
400
|
+
never across a filesystem.
|
|
401
|
+
|
|
402
|
+
RETURNS whether it wrote, and RAISES FOR NOTHING. A failed write must not turn a
|
|
403
|
+
successful `--update` into a failure: the operator's install was updated either way, and
|
|
404
|
+
the worst case is a status line that still says `never checked`.
|
|
405
|
+
"""
|
|
406
|
+
# The MODULE is named in the import so the gate's allowlist can name it too: a
|
|
407
|
+
# `from bantamkit import updatecheck` records only `bantamkit`, which would let any
|
|
408
|
+
# sibling module in under the same allowance.
|
|
409
|
+
from bantamkit.updatecheck import KEY, SOURCE_RECORD, load_record, record_path
|
|
410
|
+
|
|
411
|
+
path = record_path()
|
|
412
|
+
directory = path.parent
|
|
413
|
+
if not directory.is_dir():
|
|
414
|
+
return False
|
|
415
|
+
source, existing = load_record(path)
|
|
416
|
+
payload: dict[str, object] = dict(existing) if source == SOURCE_RECORD and existing else {}
|
|
417
|
+
payload["checked_at"] = now or datetime.now(UTC).strftime(STAMP)
|
|
418
|
+
payload[KEY] = {"distribution": DISTRIBUTION, "latest": latest}
|
|
419
|
+
try:
|
|
420
|
+
body = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
|
421
|
+
except (TypeError, ValueError):
|
|
422
|
+
return False
|
|
423
|
+
try:
|
|
424
|
+
handle, temporary = tempfile.mkstemp(
|
|
425
|
+
dir=str(directory), prefix=".update-check-", suffix=".json"
|
|
426
|
+
)
|
|
427
|
+
except OSError:
|
|
428
|
+
return False
|
|
429
|
+
try:
|
|
430
|
+
with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
|
|
431
|
+
stream.write(body)
|
|
432
|
+
os.replace(temporary, path)
|
|
433
|
+
except OSError:
|
|
434
|
+
try:
|
|
435
|
+
os.unlink(temporary)
|
|
436
|
+
except OSError:
|
|
437
|
+
pass
|
|
438
|
+
return False
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
|
|
362
442
|
def update(
|
|
363
443
|
installed: str,
|
|
364
444
|
origin: Origin,
|
|
@@ -389,6 +469,15 @@ def update(
|
|
|
389
469
|
raise UpdateRefused(UNREACHABLE.format(reason=exc)) from None
|
|
390
470
|
|
|
391
471
|
latest = latest_from_index_payload(body)
|
|
472
|
+
|
|
473
|
+
# THE RECORD IS WRITTEN HERE AND NOT IN ONE OF THE ARMS BELOW, because what it records is
|
|
474
|
+
# "the index said X on this date" — which is true the moment the fetch returned, whatever
|
|
475
|
+
# this flag then decides to do about it. An operator whose shape has no route gets the
|
|
476
|
+
# refusal AND a `bantamkit_status` that now knows the number; one who is already current
|
|
477
|
+
# gets a line that says so with a date. The return value is deliberately dropped: a record
|
|
478
|
+
# that could not be written is not a reason to fail an update that worked.
|
|
479
|
+
record_update(latest)
|
|
480
|
+
|
|
392
481
|
header = COMPARISON.format(program=PROGRAM, installed=installed, latest=latest)
|
|
393
482
|
order = compare_versions(installed, latest)
|
|
394
483
|
if order == 0:
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""The update record on disk, and the five-state line `bantamkit_status` prints from it.
|
|
2
|
+
|
|
3
|
+
NOTHING HERE REACHES THE NETWORK, AND THAT IS THE WHOLE DESIGN. `selfupdate.py:29-31` keeps
|
|
4
|
+
AS-7's third reason alive after the flag itself was granted: the network lives "only on this
|
|
5
|
+
flag's own path, never on `bantamkit_status`, never at startup". Measured 2026-09-19 on this
|
|
6
|
+
machine, one registry GET costs 0.14-0.46 s on a good network and 10.0 s
|
|
7
|
+
(`selfupdate.DEFAULT_TIMEOUT_SECONDS`) on a captive one, against a 0.09 s cold stdio boot —
|
|
8
|
+
1.5x to 100x the entire server start, once per session per registered endpoint. So this
|
|
9
|
+
module opens ONE file and asks nobody anything. A writer outside both runtimes (the hook,
|
|
10
|
+
and `--update` out of the answer it already fetched) puts the number there; this module only
|
|
11
|
+
reads it.
|
|
12
|
+
|
|
13
|
+
WHAT THIS IS NOT. A newer version existing is NOT a degraded condition — ruled 2026-09-19 on
|
|
14
|
+
top of `mcpserver.py:906`'s "a footer on every result is noise". The server is serving
|
|
15
|
+
correctly; what is true is that a newer one exists. So nothing here is a `Condition`, nothing
|
|
16
|
+
here flips `Degraded`, and nothing here appears in `degraded_notice`.
|
|
17
|
+
|
|
18
|
+
NO TTL LIVES IN THIS READER. The comparison is between the version that is RUNNING and the
|
|
19
|
+
version the record last saw, so a stale record cannot manufacture a false "you are stale": if
|
|
20
|
+
the operator updated since, running >= recorded and the line goes quiet by itself. Freshness
|
|
21
|
+
is the writer's problem alone, which is what keeps every state here observed rather than
|
|
22
|
+
inferred, with no timer anywhere in a runtime.
|
|
23
|
+
|
|
24
|
+
## The record
|
|
25
|
+
|
|
26
|
+
<homedir>/.bantamkit/update-check.json
|
|
27
|
+
|
|
28
|
+
{"checked_at": "2026-09-19T21:04:11Z",
|
|
29
|
+
"npm": {"package": "bantamkit-mcp", "latest": "0.36.0"},
|
|
30
|
+
"pypi": {"distribution": "bantamkit", "latest": "0.36.0"}}
|
|
31
|
+
|
|
32
|
+
Both registries, because they are two registries. THIS runtime reads `pypi.latest`; the Node
|
|
33
|
+
runtime reads `npm.latest`. That split is the `docs/porting.md` divergence row — the same
|
|
34
|
+
genuinely-different-object as `--update`'s URL row, not a new kind of one.
|
|
35
|
+
|
|
36
|
+
THE PATH RESOLVES AGAINST `Path.home()` AND NOTHING ELSE. A `.bantamkit` directory relative
|
|
37
|
+
to a cwd is a MEMORY STORE, and creating one by accident is a defect class this repo has
|
|
38
|
+
already paid for (J54-3, `hostinstall.ts:362`). Nothing in this module writes, creates, or
|
|
39
|
+
mkdirs anything — not the file, not the directory — so a missing record is a state with its
|
|
40
|
+
own sentence rather than an error or a reason to write.
|
|
41
|
+
|
|
42
|
+
## THE SENTENCES ARE THE PRODUCT
|
|
43
|
+
|
|
44
|
+
The five below are named constants and are copied into `runtime-ts` BYTE FOR BYTE, the way
|
|
45
|
+
J46-30 copied `selfupdate`'s. Like those, they name the COMMAND `PROGRAM` — `bantamkit-mcp`,
|
|
46
|
+
the one word that is true on npm and on PyPI both — and never a package name, because the
|
|
47
|
+
PyPI distribution is `bantamkit` and the npm package is `bantamkit-mcp` and a sentence naming
|
|
48
|
+
either one could not be identical on both sides.
|
|
49
|
+
|
|
50
|
+
`{date}` is the `YYYY-MM-DD` PREFIX of `checked_at`, never a locale rendering: a rendered
|
|
51
|
+
date would make the two runtimes disagree on a machine set to another locale, and the
|
|
52
|
+
`current` state names a date because the record may be months old and "current" without one
|
|
53
|
+
would be a claim the record cannot support.
|
|
54
|
+
|
|
55
|
+
"then reconnect the host" is not politeness. It is measured reason 1 in `selfupdate.py:13-20`
|
|
56
|
+
— a running server keeps serving the code it loaded at startup — and an update line that did
|
|
57
|
+
not say so would be the confusion AS-7 predicted.
|
|
58
|
+
|
|
59
|
+
## What counts as unreadable
|
|
60
|
+
|
|
61
|
+
EVERY shape this reader cannot act on, and it NEVER raises for any of them: bytes that are
|
|
62
|
+
not UTF-8, text that is not JSON, JSON that is not an object, no key for this runtime, a
|
|
63
|
+
`latest` that is absent or is not a version, and a missing or unparseable `checked_at`. A
|
|
64
|
+
leading UTF-8 BOM is NOT one of them — see `load_record`: both runtimes accept it, which is
|
|
65
|
+
what `tools/conformance/suites/updatecheck.mjs`'s BOM arms prove and why it is not a
|
|
66
|
+
`docs/porting.md` divergence row. The
|
|
67
|
+
`checked_at` rule is unconditional rather than applied only to the `current` sentence: a
|
|
68
|
+
record that cannot say when it was written is not a record, and one rule is one thing for the
|
|
69
|
+
port to reproduce instead of two.
|
|
70
|
+
|
|
71
|
+
Both checks are deliberately SHAPE checks and not parsers — a `YYYY-MM-DD` prefix, and a
|
|
72
|
+
dotted version whose first component is digits — because a `datetime` parse and a PEP 440
|
|
73
|
+
parse are each a second, larger thing for `runtime-ts` to reproduce exactly, in service of
|
|
74
|
+
strings neither writer here can produce. Same reasoning as `selfupdate._version_key`'s.
|
|
75
|
+
|
|
76
|
+
Version ordering is `selfupdate.compare_versions`, imported rather than reimplemented. There
|
|
77
|
+
is no second comparator in this codebase and this module does not add one.
|
|
78
|
+
|
|
79
|
+
Layer 5 (Composition): this module reads a file off the operator's home directory and exists
|
|
80
|
+
to be called from `mcpserver.bantamkit_status`. It imports no `mcp` and reaches no network.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
from __future__ import annotations
|
|
84
|
+
|
|
85
|
+
import json
|
|
86
|
+
import re
|
|
87
|
+
from dataclasses import dataclass
|
|
88
|
+
from pathlib import Path
|
|
89
|
+
from typing import Any
|
|
90
|
+
|
|
91
|
+
from bantamkit.selfupdate import PROGRAM, compare_versions
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"PROGRAM",
|
|
95
|
+
"KEY",
|
|
96
|
+
"RECORD_DIR",
|
|
97
|
+
"RECORD_NAME",
|
|
98
|
+
"UPDATE_NEVER",
|
|
99
|
+
"UPDATE_AVAILABLE",
|
|
100
|
+
"UPDATE_CURRENT",
|
|
101
|
+
"UPDATE_AHEAD",
|
|
102
|
+
"UPDATE_UNREADABLE",
|
|
103
|
+
"STATE_NEVER",
|
|
104
|
+
"STATE_AVAILABLE",
|
|
105
|
+
"STATE_CURRENT",
|
|
106
|
+
"STATE_AHEAD",
|
|
107
|
+
"STATE_UNREADABLE",
|
|
108
|
+
"STATES",
|
|
109
|
+
"SOURCE_ABSENT",
|
|
110
|
+
"SOURCE_UNREADABLE",
|
|
111
|
+
"SOURCE_RECORD",
|
|
112
|
+
"UpdateStatus",
|
|
113
|
+
"record_path",
|
|
114
|
+
"load_record",
|
|
115
|
+
"read_record",
|
|
116
|
+
"decide",
|
|
117
|
+
"update_status",
|
|
118
|
+
"update_line",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
#: The key of the record THIS runtime reads. Divergent by construction: `runtime-ts` reads
|
|
122
|
+
#: `npm`. Registered as a `docs/porting.md` divergence row by J57-5.
|
|
123
|
+
KEY = "pypi"
|
|
124
|
+
|
|
125
|
+
#: The two path components, spelled once. `.bantamkit` under a HOME is this toolbox's own
|
|
126
|
+
#: directory; `.bantamkit` under a cwd is a memory store, and this module never builds one.
|
|
127
|
+
RECORD_DIR = ".bantamkit"
|
|
128
|
+
RECORD_NAME = "update-check.json"
|
|
129
|
+
|
|
130
|
+
# --- the sentences ------------------------------------------------------------------
|
|
131
|
+
# Copied into `runtime-ts` byte for byte by J57-2. Changing one is a change to both runtimes
|
|
132
|
+
# and to the conformance suite, never to this file alone.
|
|
133
|
+
|
|
134
|
+
#: No record at all. The state of a machine that has never been checked and of one that has
|
|
135
|
+
#: been offline since it was installed — deliberately the same state, because they are the
|
|
136
|
+
#: same fact about what is known.
|
|
137
|
+
UPDATE_NEVER = "update: never checked."
|
|
138
|
+
|
|
139
|
+
#: The whole point of the feature. `{latest}` and `{installed}` are BOTH here so the reader
|
|
140
|
+
#: can falsify the claim, the same reason `selfupdate.COMPARISON` carries both.
|
|
141
|
+
UPDATE_AVAILABLE = (
|
|
142
|
+
"update: {program} {installed} is running; the package index has {latest} — run "
|
|
143
|
+
"`{program} --update`, then reconnect the host."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
#: Names the date because the record may be months old.
|
|
147
|
+
UPDATE_CURRENT = "update: {program} {installed} is current as of {date}."
|
|
148
|
+
|
|
149
|
+
#: A real state, not a curiosity: a checkout build, or a release not yet published, lands
|
|
150
|
+
#: here — the same pair `selfupdate.AHEAD` exists for.
|
|
151
|
+
UPDATE_AHEAD = (
|
|
152
|
+
"update: {program} {installed} is ahead of the package index, which has {latest}."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
#: One sentence for every malformed shape. It does not name WHICH shape: the operator cannot
|
|
156
|
+
#: act differently on any of them, and a per-shape sentence would be N more strings to port.
|
|
157
|
+
UPDATE_UNREADABLE = "update: the update record could not be read."
|
|
158
|
+
|
|
159
|
+
# --- the states -----------------------------------------------------------------------
|
|
160
|
+
# The names are what the non-ruled conformance companion compares: given one record and one
|
|
161
|
+
# running version, both runtimes must land in the SAME state, whichever key each one read.
|
|
162
|
+
|
|
163
|
+
STATE_NEVER = "never"
|
|
164
|
+
STATE_AVAILABLE = "available"
|
|
165
|
+
STATE_CURRENT = "current"
|
|
166
|
+
STATE_AHEAD = "ahead"
|
|
167
|
+
STATE_UNREADABLE = "unreadable"
|
|
168
|
+
STATES = (STATE_NEVER, STATE_AVAILABLE, STATE_CURRENT, STATE_AHEAD, STATE_UNREADABLE)
|
|
169
|
+
|
|
170
|
+
# --- what the loader found --------------------------------------------------------------
|
|
171
|
+
# Three outcomes and not two, because "there is no file" and "there is a file I cannot read"
|
|
172
|
+
# are different sentences. Collapsing them would print `never checked` over a corrupted
|
|
173
|
+
# record, which is the one wrong thing a reader of this file can do.
|
|
174
|
+
|
|
175
|
+
SOURCE_ABSENT = "absent"
|
|
176
|
+
SOURCE_UNREADABLE = "unreadable"
|
|
177
|
+
SOURCE_RECORD = "record"
|
|
178
|
+
|
|
179
|
+
#: `0.36.0`, and `0.31.0rc1` too — a first component of digits and dotted parts after it.
|
|
180
|
+
#: NOT a PEP 440 parse: see the module docstring. `v1.2.3` and `nightly` are not versions
|
|
181
|
+
#: here, and neither registry serves either.
|
|
182
|
+
_VERSION = re.compile(r"^[0-9]+(?:\.[0-9A-Za-z_+-]+)*$")
|
|
183
|
+
|
|
184
|
+
#: The `YYYY-MM-DD` prefix, and only the prefix. Whatever follows it is the writer's
|
|
185
|
+
#: business: this module never renders a date, it slices one.
|
|
186
|
+
_DATE_PREFIX = re.compile(r"^([0-9]{4}-[0-9]{2}-[0-9]{2})")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class UpdateStatus:
|
|
191
|
+
"""Which of the five states, and the one line that says so.
|
|
192
|
+
|
|
193
|
+
Both halves are returned because the conformance gate needs the STATE (the bit that must
|
|
194
|
+
not differ between runtimes) while `bantamkit_status` needs the LINE (the bit an operator
|
|
195
|
+
reads). Deriving either from the other would be a second decision somewhere.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
state: str
|
|
199
|
+
line: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _home() -> Path:
|
|
203
|
+
"""`Path.home()`, behind a name so a test can point it somewhere nobody lives.
|
|
204
|
+
|
|
205
|
+
The same seam `hostinstall._home` is, for the same reason: every path in this module is
|
|
206
|
+
derived from this one call, so redirecting it is total.
|
|
207
|
+
"""
|
|
208
|
+
return Path.home()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def record_path() -> Path:
|
|
212
|
+
"""`<homedir>/.bantamkit/update-check.json`. Never relative to a cwd — see the docstring.
|
|
213
|
+
|
|
214
|
+
Returns a path whether or not anything is at it. Nothing here creates either component.
|
|
215
|
+
"""
|
|
216
|
+
return _home() / RECORD_DIR / RECORD_NAME
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def load_record(path: Path | None = None) -> tuple[str, dict[str, Any] | None]:
|
|
220
|
+
"""`(SOURCE_*, record)`. Opens one file, creates nothing, and never raises.
|
|
221
|
+
|
|
222
|
+
`ENOENT` and `ENOTDIR` are ABSENT — in both, nothing is at the path, and a `.bantamkit`
|
|
223
|
+
that is a regular file leaves the operator in exactly the position of one who has never
|
|
224
|
+
been checked. Every other `OSError` (a directory where the record should be, a mode that
|
|
225
|
+
cannot be read) is UNREADABLE, because something IS there and this reader cannot use it.
|
|
226
|
+
Undecodable bytes and bad JSON are UNREADABLE for the same reason.
|
|
227
|
+
|
|
228
|
+
`utf-8-sig` AND NOT `utf-8`, AND THAT IS A PARITY FIX, NOT A PREFERENCE. `json.loads`
|
|
229
|
+
refuses a leading BOM by name (`Unexpected UTF-8 BOM (decode using utf-8-sig)`), while the
|
|
230
|
+
port's `new TextDecoder('utf-8', {fatal: true})` strips one before `JSON.parse` ever sees
|
|
231
|
+
it — so the same file was `unreadable` here and `available` there until J57-5b. PowerShell's
|
|
232
|
+
`Set-Content` and `Out-File` write UTF-8 WITH a BOM by default, so a Windows operator who
|
|
233
|
+
opens this record and saves it again produces exactly those bytes; a record a reader can
|
|
234
|
+
plainly act on is not "a shape this reader cannot act on". `utf-8-sig` strips a LEADING BOM
|
|
235
|
+
and is `utf-8` in every other respect: bytes that are not UTF-8 still raise here and are
|
|
236
|
+
still UNREADABLE.
|
|
237
|
+
"""
|
|
238
|
+
target = record_path() if path is None else Path(path)
|
|
239
|
+
try:
|
|
240
|
+
text = target.read_text(encoding="utf-8-sig")
|
|
241
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
242
|
+
return (SOURCE_ABSENT, None)
|
|
243
|
+
except (OSError, ValueError):
|
|
244
|
+
# `UnicodeDecodeError` is a `ValueError`; `IsADirectoryError` and `PermissionError`
|
|
245
|
+
# are `OSError`s. None of them may escape: every failure here is a state.
|
|
246
|
+
return (SOURCE_UNREADABLE, None)
|
|
247
|
+
try:
|
|
248
|
+
payload = json.loads(text)
|
|
249
|
+
except ValueError:
|
|
250
|
+
return (SOURCE_UNREADABLE, None)
|
|
251
|
+
if not isinstance(payload, dict):
|
|
252
|
+
# A list or a bare number is well-formed JSON and is not a record.
|
|
253
|
+
return (SOURCE_UNREADABLE, None)
|
|
254
|
+
return (SOURCE_RECORD, payload)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def read_record(path: Path | None = None) -> dict[str, Any] | None:
|
|
258
|
+
"""The parsed record, or `None` when there is not one. Creates nothing, never raises.
|
|
259
|
+
|
|
260
|
+
`None` deliberately does NOT say which of absent-or-unreadable it was — a caller that
|
|
261
|
+
needs the difference calls `load_record`, which is the one this module's own decision
|
|
262
|
+
uses. This is the convenience surface for a caller that only wants the object.
|
|
263
|
+
"""
|
|
264
|
+
return load_record(path)[1]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _latest_in(record: dict[str, Any], key: str) -> str | None:
|
|
268
|
+
"""`record[key]["latest"]` when it is a version string, else `None`. No exceptions."""
|
|
269
|
+
entry = record.get(key)
|
|
270
|
+
if not isinstance(entry, dict):
|
|
271
|
+
return None
|
|
272
|
+
latest = entry.get("latest")
|
|
273
|
+
if not isinstance(latest, str):
|
|
274
|
+
return None
|
|
275
|
+
latest = latest.strip()
|
|
276
|
+
return latest if _VERSION.match(latest) else None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _checked_date(record: dict[str, Any]) -> str | None:
|
|
280
|
+
"""The `YYYY-MM-DD` prefix of `checked_at`, or `None`. Sliced, never rendered."""
|
|
281
|
+
checked_at = record.get("checked_at")
|
|
282
|
+
if not isinstance(checked_at, str):
|
|
283
|
+
return None
|
|
284
|
+
found = _DATE_PREFIX.match(checked_at.strip())
|
|
285
|
+
return found.group(1) if found else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def decide(
|
|
289
|
+
installed: str,
|
|
290
|
+
source: str,
|
|
291
|
+
record: dict[str, Any] | None,
|
|
292
|
+
key: str = KEY,
|
|
293
|
+
) -> UpdateStatus:
|
|
294
|
+
"""The five-state decision, pure: no filesystem, no clock, no network, no TTL.
|
|
295
|
+
|
|
296
|
+
Split from the reading so the conformance harness and the tests can construct a state
|
|
297
|
+
directly and so the only thing that touches a disk is `load_record`.
|
|
298
|
+
"""
|
|
299
|
+
if source == SOURCE_ABSENT:
|
|
300
|
+
return UpdateStatus(STATE_NEVER, UPDATE_NEVER)
|
|
301
|
+
if source != SOURCE_RECORD or not isinstance(record, dict):
|
|
302
|
+
return UpdateStatus(STATE_UNREADABLE, UPDATE_UNREADABLE)
|
|
303
|
+
date = _checked_date(record)
|
|
304
|
+
latest = _latest_in(record, key)
|
|
305
|
+
if date is None or latest is None:
|
|
306
|
+
return UpdateStatus(STATE_UNREADABLE, UPDATE_UNREADABLE)
|
|
307
|
+
order = compare_versions(installed, latest)
|
|
308
|
+
if order < 0:
|
|
309
|
+
return UpdateStatus(
|
|
310
|
+
STATE_AVAILABLE,
|
|
311
|
+
UPDATE_AVAILABLE.format(program=PROGRAM, installed=installed, latest=latest),
|
|
312
|
+
)
|
|
313
|
+
if order == 0:
|
|
314
|
+
return UpdateStatus(
|
|
315
|
+
STATE_CURRENT,
|
|
316
|
+
UPDATE_CURRENT.format(program=PROGRAM, installed=installed, date=date),
|
|
317
|
+
)
|
|
318
|
+
return UpdateStatus(
|
|
319
|
+
STATE_AHEAD,
|
|
320
|
+
UPDATE_AHEAD.format(program=PROGRAM, installed=installed, latest=latest),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def update_status(
|
|
325
|
+
installed: str,
|
|
326
|
+
key: str = KEY,
|
|
327
|
+
path: Path | None = None,
|
|
328
|
+
) -> UpdateStatus:
|
|
329
|
+
"""Read the record and decide. The whole reader, in one call, for `bantamkit_status`."""
|
|
330
|
+
source, record = load_record(path)
|
|
331
|
+
return decide(installed, source, record, key)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def update_line(
|
|
335
|
+
installed: str,
|
|
336
|
+
key: str = KEY,
|
|
337
|
+
path: Path | None = None,
|
|
338
|
+
) -> str:
|
|
339
|
+
"""Exactly one of the five sentences. Always a line: there is no silent state here."""
|
|
340
|
+
return update_status(installed, key, path).line
|