unicode-logic-kit 0.31.0__py3-none-any.whl
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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
r"""Docker lifecycle + server discovery for a local HETS instance.
|
|
2
|
+
|
|
3
|
+
This module owns everything about *getting to a running HETS REST server* —
|
|
4
|
+
:mod:`~unicode_logic_kit.hets.client` owns everything about *talking to one once
|
|
5
|
+
you have its URL*. That split is deliberate: :class:`HetsClient` never shells
|
|
6
|
+
out or touches Docker, so it works identically against a container this kit
|
|
7
|
+
started, a container someone else started, or a bare-metal HETS install behind
|
|
8
|
+
a reverse proxy — anything answering the REST API on some ``http://host:port``.
|
|
9
|
+
|
|
10
|
+
Why Docker at all
|
|
11
|
+
------------------
|
|
12
|
+
HETS (the Heterogeneous Tool Set, https://github.com/spechub/Hets) is a large
|
|
13
|
+
Haskell program with a GPL-family license and a build that is realistically
|
|
14
|
+
only reproducible via the ``spechub2/hets`` Docker image (a GHC toolchain,
|
|
15
|
+
dozens of pinned library versions, and the bundled provers). Two ways exist to
|
|
16
|
+
use it from Python:
|
|
17
|
+
|
|
18
|
+
1. **In-process bindings** (the ``hets`` PyPI/``spechub`` Python API some
|
|
19
|
+
installs expose) — link this process directly against a GPL Haskell
|
|
20
|
+
library, AND it is only buildable/available on Linux with a working GHC
|
|
21
|
+
toolchain. Neither holds for this kit's primary dev platform (Windows) or
|
|
22
|
+
its license posture (MIT).
|
|
23
|
+
2. **REST against a Docker container** — the route this module takes. HETS
|
|
24
|
+
already ships an HTTP server (``hets-server``) inside the official image;
|
|
25
|
+
this kit only ever speaks HTTP to it. That is a real process/network
|
|
26
|
+
boundary, not a linking relationship — this kit's own code never imports,
|
|
27
|
+
links, or bundles any GPL component, and the container can be swapped for
|
|
28
|
+
any other HETS deployment (a shared server, a CI sidecar) without touching
|
|
29
|
+
a line of Python here. :class:`HetsContainer` is a convenience for
|
|
30
|
+
*local* use (spin one up, talk to it, tear it down); nothing about the
|
|
31
|
+
client depends on this kit having started the container it talks to.
|
|
32
|
+
|
|
33
|
+
Container image quirks (spechub2/hets:latest, HETS 0.108.0 — verified live)
|
|
34
|
+
-----------------------------------------------------------------------------
|
|
35
|
+
Not every prover this image advertises via ``GET /provers/<iri>`` actually
|
|
36
|
+
works when invoked via ``POST /prove/<iri>``:
|
|
37
|
+
|
|
38
|
+
* ``eprover`` — the wrapper script in this image is broken: its
|
|
39
|
+
``prover_output`` ends with ``/bin/sh: 1: SZS: not found`` and the goal
|
|
40
|
+
stays ``"Open\n"`` no matter the input. Do not use it.
|
|
41
|
+
* ``Vampire`` — returns ``"Open\n"`` with empty ``prover_output`` for every
|
|
42
|
+
goal tried; effectively non-functional in this image.
|
|
43
|
+
* ``SPASS`` (via the ``CASL2TPTP_FOF`` translation), ``darwin``, and
|
|
44
|
+
``darwin-non-fd`` (the finite-model DISPROVER — the one that actually
|
|
45
|
+
returns ``"Disproved\n"`` for a non-theorem, since plain ``darwin`` is a
|
|
46
|
+
model finder tuned for SATISFIABLE/proof, not for certifying
|
|
47
|
+
unprovability) all work and are what :mod:`~unicode_logic_kit.hets.client`'s
|
|
48
|
+
docstring examples and this kit's live tests use.
|
|
49
|
+
|
|
50
|
+
**``"Open\n"`` means UNKNOWN, never REFUTED.** A prover reporting ``Open`` has
|
|
51
|
+
not found a proof within its budget; it has not certified the goal false.
|
|
52
|
+
Silently treating a broken/timed-out prover's ``Open`` as a disproof would be
|
|
53
|
+
exactly the "silent downgrade" failure mode this kit's ``atp.protocol``
|
|
54
|
+
module is built to prevent (see its module docstring) — callers of
|
|
55
|
+
:mod:`~unicode_logic_kit.hets.client` must keep that distinction themselves;
|
|
56
|
+
this module and the client only ever pass the reasoner's own ``result``
|
|
57
|
+
string through unmodified.
|
|
58
|
+
|
|
59
|
+
OWL 2 / description-logic support (C43 Phase-0 spike, verified live
|
|
60
|
+
2026-09-18) — a genuine, working DL reasoner IS present, but only one
|
|
61
|
+
--------------------------------------------------------------------------
|
|
62
|
+
This image DOES parse OWL 2 (Manchester ``.omn`` and Functional-Style
|
|
63
|
+
``.ofn`` alike — an uploaded file's ``GET /dg/<iri>?format=json`` reports
|
|
64
|
+
``"logic": "OWL"`` for the resulting node, same as any CASL node). Its
|
|
65
|
+
``GET /provers/<iri>?format=json`` for an OWL-typed file lists ``Fact,
|
|
66
|
+
eprover, darwin, darwin-non-fd, Vampire, MathServeBroker, SPASS, EProver,
|
|
67
|
+
Darwin`` — of these, only ``Fact`` is a genuine DL reasoner: it is
|
|
68
|
+
`FaCT++ <http://owl.cs.manchester.ac.uk/tools/fact/>`_ 1.6.3
|
|
69
|
+
(live-confirmed inside the image: ``/usr/lib/hets/hets-owl-tools/lib/
|
|
70
|
+
uk.ac.manchester.cs.owl.factplusplus-P5.0-v1.6.3.1.jar`` plus its native
|
|
71
|
+
``libFaCTPlusPlusJNI.so``), **LGPL-2.1-licensed** (confirmed from FaCT++'s
|
|
72
|
+
own project page and the FSF Free Software Directory) — no AGPL callout
|
|
73
|
+
needed here, unlike the hypothetical Pellet-backed route the roadmap item
|
|
74
|
+
that added this section anticipated: neither ``Pellet`` nor ``HermiT`` is
|
|
75
|
+
offered by this image or its REST API at all (``POST /consistency-check``
|
|
76
|
+
with ``reasoner: "Pellet"`` answers ``"*** Error:\nuser error (no cons
|
|
77
|
+
checker found)"``).
|
|
78
|
+
|
|
79
|
+
Two more OWL-specific quirks, both load-bearing for
|
|
80
|
+
:mod:`unicode_logic_kit.hets.owl_backend` (the module that actually calls
|
|
81
|
+
``Fact``, and the fuller write-up of all of this — see its own module
|
|
82
|
+
docstring's "Phase 0 spike findings" for the exact REST calls/responses):
|
|
83
|
+
|
|
84
|
+
* **Leaving ``reasoner`` unset is broken for OWL input.** Unlike the CASL/FOL
|
|
85
|
+
route above (where an unset reasoner is fine and lets Hets choose),
|
|
86
|
+
omitting ``reasoner`` for an OWL file makes Hets pick an
|
|
87
|
+
``OWL22CASL:CASL2SoftFOL``-family translation that can crash outright —
|
|
88
|
+
live-observed on an ontology as simple as one class asserted a subclass of
|
|
89
|
+
``owl:Nothing``: ``*** Error: SuleCFOL2SoftFOL.transPREDSYMB: unknown
|
|
90
|
+
pred: Qual_pred_name owl_uNothing ...``. Always pass ``reasoner="Fact"``
|
|
91
|
+
explicitly for OWL input.
|
|
92
|
+
* **``"Timeout\n"`` is a third, genuine result value** (alongside
|
|
93
|
+
``"Consistent\n"``/``"Inconsistent\n"``) for ``POST /consistency-check``
|
|
94
|
+
on an OWL node — live-confirmed by forcing ``timeLimit: 0``. It must be
|
|
95
|
+
treated the same way ``"Open\n"`` is treated above: UNKNOWN, never
|
|
96
|
+
silently folded into either verdict.
|
|
97
|
+
|
|
98
|
+
Lifecycle
|
|
99
|
+
---------
|
|
100
|
+
:class:`HetsContainer` runs, health-polls, and stops one
|
|
101
|
+
``spechub2/hets:latest`` container. :func:`discover_hets_url` is the entry
|
|
102
|
+
point most callers want: it never silently proceeds without a real server —
|
|
103
|
+
an env var pointing nowhere, a missing ``docker`` binary, a stopped Docker
|
|
104
|
+
daemon, or an unpullable image all raise :class:`BackendUnavailable` with a
|
|
105
|
+
concrete next step, mirroring
|
|
106
|
+
:mod:`unicode_logic_kit.hol.isabelle_runner`'s "never guess, always say how to
|
|
107
|
+
fix it" discovery contract. :func:`hets_available` is the cheap, non-raising
|
|
108
|
+
predicate that test files gate on (the ``isabelle_available()`` role for this
|
|
109
|
+
backend).
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
from __future__ import annotations
|
|
113
|
+
|
|
114
|
+
import os
|
|
115
|
+
import re
|
|
116
|
+
import shutil
|
|
117
|
+
import subprocess
|
|
118
|
+
import time
|
|
119
|
+
import urllib.error
|
|
120
|
+
import urllib.request
|
|
121
|
+
import uuid
|
|
122
|
+
from typing import Optional, Tuple
|
|
123
|
+
|
|
124
|
+
from ..atp.protocol import BackendUnavailable
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"HETS_IMAGE", "HetsContainer", "discover_hets_url", "hets_available",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# Digest-pinned to the exact build this module's docstring already claims to
|
|
131
|
+
# have verified (HETS 0.108.0, 2026-08-12) — a bare ``:latest`` tag can move
|
|
132
|
+
# out from under this kit without warning, silently swapping in an image
|
|
133
|
+
# whose reasoner-availability quirks (see the module docstring's "Container
|
|
134
|
+
# image quirks" section) were never re-checked.
|
|
135
|
+
#
|
|
136
|
+
# How this digest was verified to BE that build (re-run this whenever the
|
|
137
|
+
# image is deliberately upgraded — see the "re-pinning" note below):
|
|
138
|
+
#
|
|
139
|
+
# 1. Two INDEPENDENT digest sources agreed on the manifest-list digest below
|
|
140
|
+
# for the ``spechub2/hets:latest`` tag, live on 2026-09-17:
|
|
141
|
+
# - Docker Hub's public tag API:
|
|
142
|
+
# ``GET https://hub.docker.com/v2/repositories/spechub2/hets/tags/latest``
|
|
143
|
+
# -> top-level ``"digest"``.
|
|
144
|
+
# - The Docker Registry v2 HTTP API itself (bypassing Docker Hub's own
|
|
145
|
+
# web API entirely): an anonymous pull token from
|
|
146
|
+
# ``https://auth.docker.io/token?service=registry.docker.io&scope=repository:spechub2/hets:pull``,
|
|
147
|
+
# then ``GET https://registry-1.docker.io/v2/spechub2/hets/manifests/latest``
|
|
148
|
+
# with an ``Accept: application/vnd.oci.image.index.v1+json`` header
|
|
149
|
+
# -> the ``Docker-Content-Digest`` response header. This is the same
|
|
150
|
+
# digest ``docker pull``/``docker manifest inspect`` would resolve
|
|
151
|
+
# ``:latest`` to.
|
|
152
|
+
# 2. `docker pull spechub2/hets@<this digest>` (Docker Desktop, live) then
|
|
153
|
+
# `docker run -d --rm -p 18000:8000 spechub2/hets@<this digest>` and
|
|
154
|
+
# `curl http://localhost:18000/version` answered
|
|
155
|
+
# ``"The Heterogeneous Tool Set, version 0.108.0"`` verbatim — the exact
|
|
156
|
+
# version this module's docstring names, confirmed by the ``/version``
|
|
157
|
+
# banner itself, not just by tag metadata.
|
|
158
|
+
#
|
|
159
|
+
# Re-pinning for a deliberate upgrade: `docker pull spechub2/hets:latest`,
|
|
160
|
+
# then `docker inspect --format='{{index .RepoDigests 0}}' spechub2/hets:latest`
|
|
161
|
+
# (or repeat step 1 above against the registry API without a local pull),
|
|
162
|
+
# THEN repeat step 2 to confirm the new digest's own ``/version`` banner and
|
|
163
|
+
# re-verify the "Container image quirks" section below still holds before
|
|
164
|
+
# updating this constant — a digest bump with no re-verification would
|
|
165
|
+
# reintroduce exactly the silent-drift risk this pin exists to close.
|
|
166
|
+
HETS_IMAGE = "spechub2/hets@sha256:406dcf34fb2486a99829a57583a2b247163ed99d1f9ace2d66ebedab9d47528a"
|
|
167
|
+
|
|
168
|
+
_DEFAULT_PORT = 8000
|
|
169
|
+
_VERSION_PATH = "/version"
|
|
170
|
+
_HEALTH_TIMEOUT = 2.0 # per-probe socket timeout, not the overall budget
|
|
171
|
+
_POLL_INTERVAL = 0.5
|
|
172
|
+
|
|
173
|
+
# docker CLI stderr signatures. Deliberately permissive (a few alternatives
|
|
174
|
+
# per scenario) since the exact wording differs across Docker Desktop /
|
|
175
|
+
# Engine versions and Windows named-pipe vs. Linux unix-socket transports —
|
|
176
|
+
# a missed pattern degrades to the generic "docker run failed" branch, which
|
|
177
|
+
# is still a loud BackendUnavailable, just a less specific one.
|
|
178
|
+
_DAEMON_DOWN_RE = re.compile(
|
|
179
|
+
r"cannot connect to the docker daemon|"
|
|
180
|
+
r"daemon is not running|"
|
|
181
|
+
r"error during connect|"
|
|
182
|
+
r"pipe/dockerdesktoplinuxengine|"
|
|
183
|
+
r"dial (?:unix|tcp).*(?:no such file|connection refused|actively refused)",
|
|
184
|
+
re.I,
|
|
185
|
+
)
|
|
186
|
+
_IMAGE_MISSING_RE = re.compile(
|
|
187
|
+
r"pull access denied|"
|
|
188
|
+
r"repository does not exist|"
|
|
189
|
+
r"manifest (?:unknown|for .* not found)|"
|
|
190
|
+
r"no such host|"
|
|
191
|
+
r"failed to resolve reference|"
|
|
192
|
+
r"error response from daemon: get ",
|
|
193
|
+
re.I,
|
|
194
|
+
)
|
|
195
|
+
_PORT_BUSY_RE = re.compile(
|
|
196
|
+
r"port is already allocated|"
|
|
197
|
+
r"bind for [\d.:]+ failed|"
|
|
198
|
+
r"address already in use",
|
|
199
|
+
re.I,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _probe_health(url: str, timeout: float = _HEALTH_TIMEOUT) -> bool:
|
|
204
|
+
"""``True`` iff ``GET <url>/version`` answers HTTP 200 within ``timeout``.
|
|
205
|
+
|
|
206
|
+
The single primitive both :func:`discover_hets_url` and
|
|
207
|
+
:class:`HetsContainer` poll on. Any failure (connection refused, DNS,
|
|
208
|
+
timeout, non-200) is treated as "not healthy yet" — this is a liveness
|
|
209
|
+
probe, not a diagnostic, so it swallows everything and returns ``bool``.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
with urllib.request.urlopen(
|
|
213
|
+
url.rstrip("/") + _VERSION_PATH, timeout=timeout
|
|
214
|
+
) as resp:
|
|
215
|
+
return 200 <= resp.status < 300
|
|
216
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class HetsContainer:
|
|
221
|
+
"""Context manager around one ``docker run`` of :data:`HETS_IMAGE`.
|
|
222
|
+
|
|
223
|
+
Usage::
|
|
224
|
+
|
|
225
|
+
with HetsContainer(port=8001) as hc:
|
|
226
|
+
client = HetsClient(hc.url)
|
|
227
|
+
...
|
|
228
|
+
# container stopped on exit, even if the body raised
|
|
229
|
+
|
|
230
|
+
``start()`` and ``__enter__`` are equivalent (``__enter__`` just calls
|
|
231
|
+
``start()`` and returns ``self``) — both are provided so a caller that
|
|
232
|
+
wants to hold onto the object before entering a ``with`` block (e.g. to
|
|
233
|
+
pass it around) can call ``start()`` directly.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, *, port: int = _DEFAULT_PORT, name: Optional[str] = None,
|
|
237
|
+
health_timeout: float = 60.0):
|
|
238
|
+
self.port = port
|
|
239
|
+
self.name = name or f"ufk-hets-{uuid.uuid4().hex[:8]}"
|
|
240
|
+
self.health_timeout = health_timeout
|
|
241
|
+
self.url = f"http://localhost:{port}"
|
|
242
|
+
self._started = False
|
|
243
|
+
|
|
244
|
+
def start(self) -> "HetsContainer":
|
|
245
|
+
"""Run the container and block until ``GET /version`` answers.
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
BackendUnavailable: no ``docker`` binary on PATH; the Docker
|
|
249
|
+
daemon is not running; the image is absent locally and could
|
|
250
|
+
not be pulled; ``docker run`` failed for any other reason;
|
|
251
|
+
or the container started but never became healthy within
|
|
252
|
+
``health_timeout`` seconds (the container is stopped first,
|
|
253
|
+
so a failed start never leaks a running container).
|
|
254
|
+
"""
|
|
255
|
+
docker_bin = shutil.which("docker")
|
|
256
|
+
if docker_bin is None:
|
|
257
|
+
raise BackendUnavailable(
|
|
258
|
+
"hets: no `docker` binary found on PATH. Install Docker Desktop "
|
|
259
|
+
"(https://www.docker.com/products/docker-desktop/) and ensure "
|
|
260
|
+
"`docker` is on PATH, or set $UFK_HETS_URL to a running HETS "
|
|
261
|
+
"server instead of asking this kit to manage a container."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
cmd = [docker_bin, "run", "-d", "--rm", "-p", f"{self.port}:8000",
|
|
265
|
+
"--name", self.name, HETS_IMAGE]
|
|
266
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
267
|
+
if proc.returncode != 0:
|
|
268
|
+
stderr = (proc.stderr or "").strip()
|
|
269
|
+
if _DAEMON_DOWN_RE.search(stderr):
|
|
270
|
+
raise BackendUnavailable(
|
|
271
|
+
"hets: the Docker daemon is not running. Start Docker "
|
|
272
|
+
"Desktop (or `sudo systemctl start docker` on Linux), or "
|
|
273
|
+
"set $UFK_HETS_URL to a running HETS server.\n"
|
|
274
|
+
f"docker stderr: {stderr}"
|
|
275
|
+
)
|
|
276
|
+
if _IMAGE_MISSING_RE.search(stderr):
|
|
277
|
+
raise BackendUnavailable(
|
|
278
|
+
f"hets: image {HETS_IMAGE!r} is not present locally and "
|
|
279
|
+
f"could not be pulled automatically. Run "
|
|
280
|
+
f"`docker pull {HETS_IMAGE}` (needs network access), or "
|
|
281
|
+
"set $UFK_HETS_URL to a running HETS server.\n"
|
|
282
|
+
f"docker stderr: {stderr}"
|
|
283
|
+
)
|
|
284
|
+
if _PORT_BUSY_RE.search(stderr):
|
|
285
|
+
# The single most likely real failure with a fixed default
|
|
286
|
+
# port (review-flagged): something — often a PREVIOUS hets
|
|
287
|
+
# container — already owns it.
|
|
288
|
+
raise BackendUnavailable(
|
|
289
|
+
f"hets: port {self.port} is already in use — likely an "
|
|
290
|
+
"already-running HETS container (then just use it: "
|
|
291
|
+
f"discover_hets_url() finds http://localhost:{self.port}) "
|
|
292
|
+
f"or another service (then pick HetsContainer(port=...) "
|
|
293
|
+
"differently).\n"
|
|
294
|
+
f"docker stderr: {stderr}"
|
|
295
|
+
)
|
|
296
|
+
raise BackendUnavailable(
|
|
297
|
+
f"hets: `docker run` failed (exit {proc.returncode}). Check "
|
|
298
|
+
f"`docker info` for daemon health, or set $UFK_HETS_URL to a "
|
|
299
|
+
f"running HETS server instead.\ndocker stderr: {stderr}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self._started = True
|
|
303
|
+
deadline = time.monotonic() + self.health_timeout
|
|
304
|
+
while time.monotonic() < deadline:
|
|
305
|
+
if _probe_health(self.url):
|
|
306
|
+
return self
|
|
307
|
+
time.sleep(_POLL_INTERVAL)
|
|
308
|
+
|
|
309
|
+
# Started but never answered — do not leak a running-but-useless
|
|
310
|
+
# container onto the caller's machine.
|
|
311
|
+
self.stop()
|
|
312
|
+
raise BackendUnavailable(
|
|
313
|
+
f"hets: container {self.name!r} (image {HETS_IMAGE}) was started "
|
|
314
|
+
f"but did not answer GET {self.url}{_VERSION_PATH} within "
|
|
315
|
+
f"{self.health_timeout}s. Inspect it with `docker logs {self.name}` "
|
|
316
|
+
"before it was stopped, or raise health_timeout."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def stop(self) -> None:
|
|
320
|
+
"""Stop the container (no-op if never started or already stopped).
|
|
321
|
+
|
|
322
|
+
Uses ``docker stop`` rather than ``docker kill`` — the container was
|
|
323
|
+
started with ``--rm``, so a stop also removes it. Failures here are
|
|
324
|
+
swallowed (best-effort cleanup): raising out of ``stop()``/``__exit__``
|
|
325
|
+
would mask whatever exception the ``with`` body itself raised.
|
|
326
|
+
"""
|
|
327
|
+
if not self._started:
|
|
328
|
+
return
|
|
329
|
+
docker_bin = shutil.which("docker") or "docker"
|
|
330
|
+
subprocess.run([docker_bin, "stop", self.name], capture_output=True, text=True)
|
|
331
|
+
self._started = False
|
|
332
|
+
|
|
333
|
+
def __enter__(self) -> "HetsContainer":
|
|
334
|
+
return self.start()
|
|
335
|
+
|
|
336
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
337
|
+
self.stop()
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def discover_hets_url(
|
|
342
|
+
*, start_container: bool = False, port: int = _DEFAULT_PORT,
|
|
343
|
+
health_timeout: float = 60.0,
|
|
344
|
+
) -> Tuple[str, Optional[HetsContainer]]:
|
|
345
|
+
"""Find a reachable HETS server URL, in order of precedence.
|
|
346
|
+
|
|
347
|
+
1. ``$UFK_HETS_URL`` — if set to a NON-EMPTY value, it is health-checked
|
|
348
|
+
(never trusted blind); an unhealthy value raises
|
|
349
|
+
:class:`BackendUnavailable` naming the broken env var rather than
|
|
350
|
+
silently falling through to the next option (a caller who
|
|
351
|
+
deliberately pointed at a specific server wants to know THAT server
|
|
352
|
+
is down, not get a different one back). An EMPTY string is treated
|
|
353
|
+
exactly like an unset variable — deliberate, so
|
|
354
|
+
``UFK_HETS_URL=$SOME_UNSET_CI_VAR`` degrades to normal discovery
|
|
355
|
+
instead of a guaranteed error about the empty URL.
|
|
356
|
+
2. ``http://localhost:<port>`` — checked whether or not this kit started
|
|
357
|
+
it (covers a container someone already has running, or a non-Docker
|
|
358
|
+
local install).
|
|
359
|
+
3. If ``start_container=True``: start a fresh :class:`HetsContainer` on
|
|
360
|
+
``port`` and return its URL.
|
|
361
|
+
4. Otherwise: raise :class:`BackendUnavailable` listing every option
|
|
362
|
+
tried and how to fix each one. Never silently returns a URL nobody
|
|
363
|
+
verified answers.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
``(url, container)`` — ``container`` is the started
|
|
367
|
+
:class:`HetsContainer` when this call started one (case 3, so the
|
|
368
|
+
caller can ``container.stop()`` when done), else ``None`` (cases 1-2:
|
|
369
|
+
an already-running server this call does not own and must not stop).
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
BackendUnavailable: nothing reachable, per the precedence above.
|
|
373
|
+
"""
|
|
374
|
+
env_url = os.environ.get("UFK_HETS_URL")
|
|
375
|
+
if env_url:
|
|
376
|
+
if _probe_health(env_url):
|
|
377
|
+
return env_url, None
|
|
378
|
+
raise BackendUnavailable(
|
|
379
|
+
f"hets: $UFK_HETS_URL={env_url!r} is set but did not answer "
|
|
380
|
+
f"GET {env_url.rstrip('/')}{_VERSION_PATH} — point it at a live "
|
|
381
|
+
"HETS server, or unset it to fall back to localhost/auto-start."
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
local_url = f"http://localhost:{port}"
|
|
385
|
+
if _probe_health(local_url):
|
|
386
|
+
return local_url, None
|
|
387
|
+
|
|
388
|
+
if start_container:
|
|
389
|
+
container = HetsContainer(port=port, health_timeout=health_timeout).start()
|
|
390
|
+
return container.url, container
|
|
391
|
+
|
|
392
|
+
raise BackendUnavailable(
|
|
393
|
+
"hets: no server discovered. Tried, in order:\n"
|
|
394
|
+
" 1. $UFK_HETS_URL — not set\n"
|
|
395
|
+
f" 2. {local_url}{_VERSION_PATH} — no response\n"
|
|
396
|
+
"Fix one of:\n"
|
|
397
|
+
" - set $UFK_HETS_URL to a running HETS server\n"
|
|
398
|
+
f" - start one yourself: docker run -d --rm -p {port}:8000 {HETS_IMAGE}\n"
|
|
399
|
+
" - call discover_hets_url(start_container=True) to have this kit "
|
|
400
|
+
"start and own a container (requires Docker Desktop / a running "
|
|
401
|
+
"daemon; install from https://www.docker.com/products/docker-desktop/)"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def hets_available(*, port: int = _DEFAULT_PORT) -> bool:
|
|
406
|
+
"""Cheap, non-raising predicate: is a HETS server reachable right now?
|
|
407
|
+
|
|
408
|
+
Mirrors :func:`unicode_logic_kit.hol.isabelle_runner.isabelle_available` —
|
|
409
|
+
live test files gate (``skipif``) on THIS, never on a bare env-var
|
|
410
|
+
presence check, so the skip is accurate whether the server is reached
|
|
411
|
+
via ``$UFK_HETS_URL`` or an already-running localhost container. Never
|
|
412
|
+
starts a container (calls :func:`discover_hets_url` with
|
|
413
|
+
``start_container=False``), so probing a test's availability never has
|
|
414
|
+
the side effect of spinning up Docker.
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
discover_hets_url(start_container=False, port=port)
|
|
418
|
+
return True
|
|
419
|
+
except BackendUnavailable:
|
|
420
|
+
return False
|