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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. 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