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,748 @@
|
|
|
1
|
+
r"""HTTP client for the HETS (Heterogeneous Tool Set) REST server.
|
|
2
|
+
|
|
3
|
+
Stdlib ``urllib`` only — no ``requests`` dependency, matching this kit's
|
|
4
|
+
external-tool adapters elsewhere (``atp.prover9_entailment`` /
|
|
5
|
+
``atp.vampire_entailment`` shell out via ``subprocess`` with no extra
|
|
6
|
+
dependency either; this one talks HTTP instead of spawning a process, but the
|
|
7
|
+
"no new hard dependency for one adapter" principle is the same).
|
|
8
|
+
:class:`HetsClient` is deliberately dumb about *how* it got its ``base_url``
|
|
9
|
+
— see :mod:`~unicode_logic_kit.hets.docker` for discovery/lifecycle.
|
|
10
|
+
|
|
11
|
+
Wire protocol (verified live against ``spechub2/hets:latest``, HETS 0.108.0,
|
|
12
|
+
2026-08-12 — treat every claim below as ground truth for THIS client, not
|
|
13
|
+
general HETS documentation, since the REST API is not versioned separately
|
|
14
|
+
from the server and has changed shape across releases before)
|
|
15
|
+
|
|
16
|
+
--------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
1. ``GET /version`` → plain text, e.g. ``"The Heterogeneous Tool Set, version
|
|
19
|
+
0.108.0"``.
|
|
20
|
+
2. Upload is a two-step handshake: ``GET /folder`` → a plain-text absolute
|
|
21
|
+
scratch-folder path (e.g. ``/tmp/hetsUserFolder_zvNMsf``); then
|
|
22
|
+
``POST /uploadFile/<folderBasename>/<filename>`` with the raw file text as
|
|
23
|
+
the request body → a plain-text STORED path (e.g.
|
|
24
|
+
``/tmp/hetsUserFolder_zvNMsf/probe.casl``). An error body starts with
|
|
25
|
+
``"*** Error"`` regardless of HTTP status.
|
|
26
|
+
3. The stored path IS the IRI for every subsequent endpoint below, but
|
|
27
|
+
URL-ENCODED IN FULL — including the ``/`` characters — e.g.
|
|
28
|
+
``/tmp/hetsUserFolder_zvNMsf/probe.casl`` becomes
|
|
29
|
+
``%2Ftmp%2FhetsUserFolder_zvNMsf%2Fprobe.casl``. :meth:`upload` returns the
|
|
30
|
+
RAW (unencoded) path; every method that takes ``iri`` encodes it
|
|
31
|
+
internally via ``urllib.parse.quote(iri, safe="")`` — ``safe=""`` matters,
|
|
32
|
+
the stdlib default ``safe="/"`` would leave the path unencoded and every
|
|
33
|
+
endpoint below would 404 on it.
|
|
34
|
+
4. ``GET /dg/<iri>?format=json`` → the parsed development graph:
|
|
35
|
+
``{"DGraph": {"filename", "libname", "dgnodes", "DGNode": [{"name",
|
|
36
|
+
"logic", "Declarations": [...], "Axioms": [...]}], ...}}``. A bad IRI
|
|
37
|
+
comes back as an error BODY (``"*** Error:\nfile does not exist: ..."``)
|
|
38
|
+
— HETS can still answer HTTP 200 for this, so the body-prefix check in
|
|
39
|
+
:meth:`_get`/:meth:`_post` is load-bearing, not defensive boilerplate.
|
|
40
|
+
5. ``GET /provers/<iri>?format=json`` → ``{"provers": [{"identifier":
|
|
41
|
+
"eprover", "name": "eprover"}, ...]}``.
|
|
42
|
+
6. ``GET /translations/<iri>`` → XML (the one endpoint that is NOT JSON):
|
|
43
|
+
``<Translations><translations><li>CASL2SoftFOL</li>
|
|
44
|
+
<li>CASL2NNF:CASL2SoftFOL</li>...</translations></Translations>``.
|
|
45
|
+
Composed comorphisms use a ``:`` separator.
|
|
46
|
+
7. ``POST /prove/<iri>`` with JSON body
|
|
47
|
+
``{"format":"json","goals":[{"node":"<NodeName>",
|
|
48
|
+
"translation":"<optional comorphism>",
|
|
49
|
+
"reasonerConfiguration":{"timeLimit":10,"reasoner":"<optional id>"}}]}``
|
|
50
|
+
→ a large, deeply-nested JSON object in which each attempted goal appears
|
|
51
|
+
as ``{"name":"Ax3","result":"Proved\n"|"Disproved\n"|"Open\n",
|
|
52
|
+
"used_prover":{"identifier":...},"used_translation":"CASL2TPTP_FOF",
|
|
53
|
+
"tactic_script":{...},"proof_tree":"","used_time":{...},
|
|
54
|
+
"used_axioms":[...],"prover_output":"..."}`` — the EXACT nesting path
|
|
55
|
+
varies (goals come from ``%implied`` axioms and get axiom-item names,
|
|
56
|
+
e.g. ``"Ax3"`` = third axiom item in the spec), so this client extracts
|
|
57
|
+
goal objects with a recursive structural walk (see
|
|
58
|
+
:func:`_extract_goal_objects`) rather than hard-coding a JSON path.
|
|
59
|
+
8. ``POST /consistency-check/<iri>`` — same request/response SHAPE as
|
|
60
|
+
``/prove``, with ``"result"`` values like ``"Consistent\n"``.
|
|
61
|
+
9. ``"Open\n"`` means UNKNOWN (budget/prover gave up), never REFUTED — see
|
|
62
|
+
:mod:`~unicode_logic_kit.hets.docker`'s module docstring for which
|
|
63
|
+
reasoners in the shipped image actually work (``SPASS``, ``darwin``,
|
|
64
|
+
``darwin-non-fd``) versus which are broken in this image and always
|
|
65
|
+
report ``Open`` (``eprover``, ``Vampire``).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
from __future__ import annotations
|
|
69
|
+
|
|
70
|
+
import json
|
|
71
|
+
import re
|
|
72
|
+
import urllib.error
|
|
73
|
+
import http.client
|
|
74
|
+
import urllib.parse
|
|
75
|
+
import urllib.request
|
|
76
|
+
from typing import Dict, List, Optional, Tuple
|
|
77
|
+
|
|
78
|
+
from ..fol.tptp_input import _HETS_LOGIC_REFERENCE
|
|
79
|
+
from .haskell_json import repair_haskell_json
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"HetsClient",
|
|
83
|
+
"HetsNoTranslationsError",
|
|
84
|
+
"HetsSublogicError",
|
|
85
|
+
"strip_hets_theory_header",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
_ERROR_PREFIX = "*** Error"
|
|
89
|
+
_EXCERPT_CHARS = 2000
|
|
90
|
+
|
|
91
|
+
#: HETS' own 422 body for a theory whose sublogic the requested comorphism
|
|
92
|
+
#: does not cover. Verified verbatim against the live server::
|
|
93
|
+
#:
|
|
94
|
+
#: *** Error:
|
|
95
|
+
#: for 'OWL22CASL;CASL2TPTP_FOF' expected sublogic 'NP-sROIQux-D|-|'
|
|
96
|
+
#: but found sublogic 'NP-sROIQ-D|Literal|dateTime|decimal|integer|string|' with signature sublogic 'ELQLRL-ALC'
|
|
97
|
+
#:
|
|
98
|
+
#: A stable, machine-readable signature — which is what makes
|
|
99
|
+
#: :class:`HetsSublogicError` branchable rather than prose a caller has to
|
|
100
|
+
#: grep. Note HETS writes the composition with ``;`` here while the URL (and
|
|
101
|
+
#: the command line) take ``:``; the exception carries HETS' spelling
|
|
102
|
+
#: verbatim rather than normalising it, because that is the string HETS'
|
|
103
|
+
#: other messages use.
|
|
104
|
+
_SUBLOGIC_RE = re.compile(
|
|
105
|
+
r"for '([^']+)' expected sublogic '([^']*)'\s*\n?\s*"
|
|
106
|
+
r"but found sublogic '([^']*)'")
|
|
107
|
+
|
|
108
|
+
#: A TPTP problem must contain at least one of these. Used by
|
|
109
|
+
#: :meth:`HetsClient.theory_tptp` to refuse a CASL (or error) body rather
|
|
110
|
+
#: than hand back something that would parse to an empty formula list.
|
|
111
|
+
_TPTP_STATEMENT_RE = re.compile(r"(?:^|[\s)(.])(fof|cnf|tff|thf)\s*\(")
|
|
112
|
+
|
|
113
|
+
#: The DOL ``logic <Name>.<Sublogic>`` line HETS puts in front of every
|
|
114
|
+
#: ``/theory`` rendering, and CASL's ``%{ ... }%`` block comment (which
|
|
115
|
+
#: carries HETS' own ``constants:``/``predicates:``/``sorts:`` signature
|
|
116
|
+
#: listing). Neither is TPTP syntax — see :func:`strip_hets_theory_header`.
|
|
117
|
+
#:
|
|
118
|
+
#: The WHOLE line is ``logic`` plus one DOL logic reference — the SAME notion of
|
|
119
|
+
#: "a Hets header" as :mod:`unicode_logic_kit.fol.tptp_input`'s refusal pointer
|
|
120
|
+
#: (it is imported from there, so the two cannot disagree): a logic NAME, an
|
|
121
|
+
#: identifier, optionally ``.`` and a free-form sublogic. The previous ``logic``
|
|
122
|
+
#: plus any whitespace-free word also matched the first line of a bare formula
|
|
123
|
+
#: such as ``logic &p``, and the splitter then ate it as a "header".
|
|
124
|
+
_LOGIC_LINE_RE = re.compile(
|
|
125
|
+
r"logic[ \t]+" + _HETS_LOGIC_REFERENCE + r"[ \t\r]*$", re.MULTILINE)
|
|
126
|
+
|
|
127
|
+
# The stable field set every normalized goal-result dict carries, whatever
|
|
128
|
+
# extra keys this HETS version's raw JSON happens to include.
|
|
129
|
+
_GOAL_FIELDS = (
|
|
130
|
+
"name", "result", "used_prover", "used_translation",
|
|
131
|
+
"prover_output", "used_time", "tactic_script",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class HetsNoTranslationsError(RuntimeError):
|
|
136
|
+
"""``GET /translations`` answered a well-formed list with no comorphism.
|
|
137
|
+
|
|
138
|
+
A ``RuntimeError`` subclass, like :class:`HetsOwlError`
|
|
139
|
+
(:mod:`~unicode_logic_kit.hets.owl_backend`) and for the same reason: the
|
|
140
|
+
server is up and the request was understood, the ANSWER is the problem.
|
|
141
|
+
Every existing ``except RuntimeError`` caller therefore keeps working.
|
|
142
|
+
|
|
143
|
+
Why this is raised rather than returned as ``[]``: an empty list reads as
|
|
144
|
+
"this logic has no comorphisms", and what HETS actually means is "I am not
|
|
145
|
+
telling you why". Measured on the real server — for an ontology whose
|
|
146
|
+
sublogic ``OWL22CASL`` does not cover, ``/translations`` answers
|
|
147
|
+
``<Translations><translations></translations></Translations>`` with HTTP
|
|
148
|
+
200, no exception and no reason, while ``/theory`` for the very same
|
|
149
|
+
library answers HTTP 422 with the sublogic mismatch spelled out. So the
|
|
150
|
+
reason exists; it just lives at a different endpoint, and this exception's
|
|
151
|
+
message is where that is written down.
|
|
152
|
+
|
|
153
|
+
``translations(..., allow_empty=True)`` restores the old return-``[]``
|
|
154
|
+
behaviour for a caller that genuinely wants it.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class HetsSublogicError(RuntimeError):
|
|
159
|
+
"""HETS refused a translation because the theory's sublogic is too rich.
|
|
160
|
+
|
|
161
|
+
Carries HETS' own three strings, parsed from the 422 body by
|
|
162
|
+
:data:`_SUBLOGIC_RE`, so a caller can BRANCH on the refusal instead of
|
|
163
|
+
grepping prose:
|
|
164
|
+
|
|
165
|
+
Attributes:
|
|
166
|
+
comorphism: the comorphism HETS was asked for, as HETS spells it
|
|
167
|
+
(``"OWL22CASL;CASL2TPTP_FOF"`` — with a semicolon, even though
|
|
168
|
+
the URL and the command line both take ``:``).
|
|
169
|
+
expected: the sublogic the comorphism covers
|
|
170
|
+
(``"NP-sROIQux-D|-|"``).
|
|
171
|
+
found: the sublogic the theory actually is
|
|
172
|
+
(``"NP-sROIQ-D|Literal|dateTime|decimal|integer|string|"``).
|
|
173
|
+
body: the 422 body, verbatim.
|
|
174
|
+
|
|
175
|
+
A ``RuntimeError`` subclass for the same reason as
|
|
176
|
+
:class:`HetsNoTranslationsError`.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, message: str, *, comorphism: str, expected: str,
|
|
180
|
+
found: str, body: str):
|
|
181
|
+
super().__init__(message)
|
|
182
|
+
self.comorphism = comorphism
|
|
183
|
+
self.expected = expected
|
|
184
|
+
self.found = found
|
|
185
|
+
self.body = body
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def strip_hets_theory_header(text: str) -> Tuple[str, str]:
|
|
189
|
+
r"""Split HETS' theory rendering into ``(header, body)``. Never raises.
|
|
190
|
+
|
|
191
|
+
``GET /theory?...&format=dol`` prefixes its output with DOL/CASL syntax
|
|
192
|
+
that is NOT part of the target logic::
|
|
193
|
+
|
|
194
|
+
logic TPTP.FOF
|
|
195
|
+
|
|
196
|
+
%{
|
|
197
|
+
|
|
198
|
+
constants: op_a,
|
|
199
|
+
op_b
|
|
200
|
+
|
|
201
|
+
predicates: pred_p: $i > $o
|
|
202
|
+
|
|
203
|
+
}%
|
|
204
|
+
|
|
205
|
+
fof(ax_ax1, axiom, ...).
|
|
206
|
+
|
|
207
|
+
TPTP has exactly two comment forms, ``%`` to end of line and
|
|
208
|
+
``/* ... */``. ``%{ ... }%`` is CASL's BLOCK comment and ``logic
|
|
209
|
+
<Name>.<Sublogic>`` is DOL library syntax, so a TPTP reader must not
|
|
210
|
+
learn either: ``%`` already swallows ``%{`` as an ordinary line comment
|
|
211
|
+
and the reader then chokes on the block's first content line, and
|
|
212
|
+
teaching it the block form would make it accept a CASL theory header,
|
|
213
|
+
treat the whole CASL body as a comment and return an EMPTY formula list
|
|
214
|
+
— a silent empty answer to a wrong-translation request. So the split
|
|
215
|
+
happens here, in the adapter that knows its input is a HETS rendering,
|
|
216
|
+
exactly as :func:`unicode_logic_kit.ace.runner._repair_ape_tptp` repairs
|
|
217
|
+
APE's pretty-printer inside the ACE adapter and
|
|
218
|
+
:mod:`unicode_logic_kit.fol.tptp_repair` sits OVER the reader rather than
|
|
219
|
+
inside its grammar.
|
|
220
|
+
|
|
221
|
+
The split is structural, never ``text[text.index("fof("):]``: a blind cut
|
|
222
|
+
would also swallow a HETS ``*** Error`` body whole, and a symbol whose
|
|
223
|
+
name happens to end in ``_fof`` inside the 2267-line signature block
|
|
224
|
+
would move the cut point. The rule, verified against both the real TPTP
|
|
225
|
+
and the real CASL rendering:
|
|
226
|
+
|
|
227
|
+
1. skip leading whitespace; if what follows is a WHOLE line of the form
|
|
228
|
+
``logic <Name>`` or ``logic <Name>.<Sublogic>`` (a logic name is an
|
|
229
|
+
identifier, so ``logic &p`` — a formula — is not a header), consume
|
|
230
|
+
through the end of that line;
|
|
231
|
+
2. repeatedly skip whitespace and, while what follows starts with
|
|
232
|
+
``%{``, consume through the first following ``}%`` (CASL block
|
|
233
|
+
comments do not nest);
|
|
234
|
+
3. what remains is the body.
|
|
235
|
+
|
|
236
|
+
An unterminated ``%{`` is left in the body rather than truncating the
|
|
237
|
+
text to nothing — the caller then gets a named refusal from
|
|
238
|
+
:meth:`HetsClient.theory_tptp` instead of a silently empty result.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
text: a ``/theory`` rendering, or any text at all.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
``(header, body)``. ``("", text)`` when there is no header, so the
|
|
245
|
+
call is safe to make unconditionally.
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
>>> strip_hets_theory_header("fof(a, axiom, p).")
|
|
249
|
+
('', 'fof(a, axiom, p).')
|
|
250
|
+
"""
|
|
251
|
+
index = 0
|
|
252
|
+
length = len(text)
|
|
253
|
+
while index < length and text[index].isspace():
|
|
254
|
+
index += 1
|
|
255
|
+
match = _LOGIC_LINE_RE.match(text, index)
|
|
256
|
+
if match:
|
|
257
|
+
end = text.find("\n", match.end())
|
|
258
|
+
index = length if end == -1 else end + 1
|
|
259
|
+
else:
|
|
260
|
+
index = 0
|
|
261
|
+
while True:
|
|
262
|
+
cursor = index
|
|
263
|
+
while cursor < length and text[cursor].isspace():
|
|
264
|
+
cursor += 1
|
|
265
|
+
if not text.startswith("%{", cursor):
|
|
266
|
+
break
|
|
267
|
+
close = text.find("}%", cursor + 2)
|
|
268
|
+
if close == -1:
|
|
269
|
+
# Unterminated: consume nothing, so the text survives for the
|
|
270
|
+
# caller's own refusal rather than vanishing into the header.
|
|
271
|
+
break
|
|
272
|
+
index = close + 2
|
|
273
|
+
return text[:index], text[index:]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _encode_iri(iri: str) -> str:
|
|
277
|
+
"""Percent-encode a stored-file path for use as a HETS REST IRI segment.
|
|
278
|
+
|
|
279
|
+
``safe=""`` is required (not the ``urllib.parse.quote`` default of
|
|
280
|
+
``safe="/"``): HETS's IRI convention encodes the path separators too —
|
|
281
|
+
see point 3 of this module's wire-protocol docstring.
|
|
282
|
+
"""
|
|
283
|
+
return urllib.parse.quote(iri, safe="")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _extract_goal_objects(node) -> List[dict]:
|
|
287
|
+
"""Recursively collect every dict shaped like a goal result.
|
|
288
|
+
|
|
289
|
+
A "goal result" is identified structurally — any dict carrying BOTH a
|
|
290
|
+
``"result"`` and a ``"used_prover"`` key — rather than by a fixed JSON
|
|
291
|
+
path, because the nesting HETS wraps goals in has already varied between
|
|
292
|
+
``/prove`` and ``/consistency-check`` responses and across HETS
|
|
293
|
+
versions (see point 7 of the module docstring). A dict matching the
|
|
294
|
+
signature is not itself recursed into further: its own values (e.g.
|
|
295
|
+
``used_prover``, ``used_time``) are metadata about THIS goal, not
|
|
296
|
+
containers of further sibling goals.
|
|
297
|
+
"""
|
|
298
|
+
found: List[dict] = []
|
|
299
|
+
if isinstance(node, dict):
|
|
300
|
+
if "result" in node and "used_prover" in node:
|
|
301
|
+
found.append(node)
|
|
302
|
+
else:
|
|
303
|
+
for value in node.values():
|
|
304
|
+
found.extend(_extract_goal_objects(value))
|
|
305
|
+
elif isinstance(node, list):
|
|
306
|
+
for item in node:
|
|
307
|
+
found.extend(_extract_goal_objects(item))
|
|
308
|
+
return found
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _normalize_goal(raw: dict) -> dict:
|
|
312
|
+
"""Project a raw goal-result dict onto :data:`_GOAL_FIELDS`.
|
|
313
|
+
|
|
314
|
+
``"result"`` is stripped of surrounding whitespace — HETS appends a
|
|
315
|
+
trailing newline (``"Proved\n"``) — so callers can compare against
|
|
316
|
+
plain ``"Proved"`` / ``"Disproved"`` / ``"Open"`` / ``"Consistent"``
|
|
317
|
+
without repeating ``.strip()`` at every call site. Missing fields become
|
|
318
|
+
``None`` rather than raising ``KeyError``, since which optional fields a
|
|
319
|
+
given prover populates is itself prover-specific (e.g. a failed run may
|
|
320
|
+
omit ``used_time``).
|
|
321
|
+
"""
|
|
322
|
+
out = {field: raw.get(field) for field in _GOAL_FIELDS}
|
|
323
|
+
if isinstance(out["result"], str):
|
|
324
|
+
out["result"] = out["result"].strip()
|
|
325
|
+
return out
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class HetsClient:
|
|
329
|
+
"""Thin REST client for one HETS server instance.
|
|
330
|
+
|
|
331
|
+
Holds no state about the server beyond ``base_url`` — every call is a
|
|
332
|
+
fresh HTTP request. Errors are never silent: an HTTP error status, an
|
|
333
|
+
HETS ``"*** Error"`` body (which can arrive with HTTP 200 — see point 4
|
|
334
|
+
of the module docstring), or a JSON body that fails to parse all raise
|
|
335
|
+
``RuntimeError`` carrying an excerpt of the offending body, so a failure
|
|
336
|
+
is always visible in the traceback rather than degrading to an empty
|
|
337
|
+
result the caller might mistake for "no goals found".
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def __init__(self, base_url: str, *, timeout: float = 30.0):
|
|
341
|
+
self.base_url = base_url.rstrip("/")
|
|
342
|
+
self.timeout = timeout
|
|
343
|
+
|
|
344
|
+
# -- low-level HTTP -----------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def _get(self, path: str) -> str:
|
|
347
|
+
return self._request("GET", path)
|
|
348
|
+
|
|
349
|
+
def _post(self, path: str, data: bytes, content_type: str) -> str:
|
|
350
|
+
return self._request("POST", path, data=data, content_type=content_type)
|
|
351
|
+
|
|
352
|
+
def _request(self, method: str, path: str, *, data: Optional[bytes] = None,
|
|
353
|
+
content_type: Optional[str] = None) -> str:
|
|
354
|
+
url = self.base_url + path
|
|
355
|
+
headers = {"Content-Type": content_type} if content_type else {}
|
|
356
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
357
|
+
try:
|
|
358
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
359
|
+
body = resp.read().decode("utf-8", "replace")
|
|
360
|
+
except urllib.error.HTTPError as exc:
|
|
361
|
+
body = exc.read().decode("utf-8", "replace") if exc.fp else ""
|
|
362
|
+
generic = RuntimeError(
|
|
363
|
+
f"hets: {method} {path} -> HTTP {exc.code}: "
|
|
364
|
+
f"{body[:_EXCERPT_CHARS]!r}"
|
|
365
|
+
)
|
|
366
|
+
# HTTP 422 with HETS' sublogic signature is the one HTTP error
|
|
367
|
+
# worth a TYPED exception: it is the server saying "this theory
|
|
368
|
+
# is richer than the comorphism you asked for", which a caller
|
|
369
|
+
# can act on (fall back to the lossy command-line route, or
|
|
370
|
+
# reduce the theory). Any OTHER 422 keeps the generic wording
|
|
371
|
+
# above, so the typed exception never swallows an unrelated
|
|
372
|
+
# failure.
|
|
373
|
+
if exc.code == 422:
|
|
374
|
+
signature = _SUBLOGIC_RE.search(body)
|
|
375
|
+
if signature:
|
|
376
|
+
comorphism, expected, found = signature.groups()
|
|
377
|
+
raise HetsSublogicError(
|
|
378
|
+
f"hets: {method} {path} -> HTTP 422: HETS refuses the "
|
|
379
|
+
f"comorphism {comorphism!r} for this theory — it "
|
|
380
|
+
f"covers sublogic {expected!r} but the theory is "
|
|
381
|
+
f"{found!r}. Either reduce the theory to that "
|
|
382
|
+
"sublogic, or take the LOSSY command-line route, "
|
|
383
|
+
"which translates anyway and reports what it omitted: "
|
|
384
|
+
"unicode_logic_kit.hets.owl_to_tptp(path, lossy=True) "
|
|
385
|
+
"(hets-server's -Y switch; the REST API has no "
|
|
386
|
+
f"equivalent). HETS' body was: "
|
|
387
|
+
f"{body[:_EXCERPT_CHARS]!r}",
|
|
388
|
+
comorphism=comorphism, expected=expected, found=found,
|
|
389
|
+
body=body,
|
|
390
|
+
) from exc
|
|
391
|
+
raise generic from exc
|
|
392
|
+
except urllib.error.URLError as exc:
|
|
393
|
+
raise RuntimeError(f"hets: {method} {path} failed: {exc.reason}") from exc
|
|
394
|
+
except http.client.HTTPException as exc:
|
|
395
|
+
# e.g. http.client.InvalidURL for control characters that
|
|
396
|
+
# slipped past encoding — a sibling of URLError, not a subclass,
|
|
397
|
+
# so it needs its own clause to honour the RuntimeError contract
|
|
398
|
+
# (review-confirmed leak).
|
|
399
|
+
raise RuntimeError(
|
|
400
|
+
f"hets: {method} {path} failed: {type(exc).__name__}: {exc}"
|
|
401
|
+
) from exc
|
|
402
|
+
self._check_error_body(method, path, body)
|
|
403
|
+
return body
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _check_error_body(method: str, path: str, body: str) -> None:
|
|
407
|
+
"""Raise if ``body`` is a HETS ``"*** Error"`` payload despite HTTP 200."""
|
|
408
|
+
if body.lstrip().startswith(_ERROR_PREFIX):
|
|
409
|
+
raise RuntimeError(
|
|
410
|
+
f"hets: {method} {path} returned an error body: "
|
|
411
|
+
f"{body[:_EXCERPT_CHARS]!r}"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
@staticmethod
|
|
415
|
+
def _parse_json(path: str, body: str) -> dict:
|
|
416
|
+
try:
|
|
417
|
+
return json.loads(body)
|
|
418
|
+
except json.JSONDecodeError as exc:
|
|
419
|
+
raise RuntimeError(
|
|
420
|
+
f"hets: {path} response was not valid JSON ({exc}); "
|
|
421
|
+
f"body excerpt: {body[:_EXCERPT_CHARS]!r}"
|
|
422
|
+
) from exc
|
|
423
|
+
|
|
424
|
+
# -- endpoints ------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
def version(self) -> str:
|
|
427
|
+
"""``GET /version`` -> the server's plain-text version banner, stripped."""
|
|
428
|
+
return self._get("/version").strip()
|
|
429
|
+
|
|
430
|
+
def upload(self, text: str, filename: str) -> str:
|
|
431
|
+
"""Upload ``text`` as ``filename``; return the stored-path IRI, RAW.
|
|
432
|
+
|
|
433
|
+
Two round trips per the wire protocol (points 2-3 above): fetch a
|
|
434
|
+
scratch folder, then POST the file into it. The return value is
|
|
435
|
+
deliberately the raw (unencoded) stored path — every other method on
|
|
436
|
+
this class takes that same raw ``iri`` and encodes it internally
|
|
437
|
+
(:func:`_encode_iri`), so callers never have to think about encoding,
|
|
438
|
+
and a caller that wants to log/join/inspect the path is not fighting
|
|
439
|
+
a pre-escaped string.
|
|
440
|
+
"""
|
|
441
|
+
folder = self._get("/folder").strip()
|
|
442
|
+
folder_basename = folder.rsplit("/", 1)[-1]
|
|
443
|
+
# Both path segments are percent-encoded like every IRI is: an
|
|
444
|
+
# unencoded '#' in a filename would be parsed as a URL fragment and
|
|
445
|
+
# SILENTLY truncate the stored name (review-confirmed live), and a
|
|
446
|
+
# space would crash urllib below the URLError layer.
|
|
447
|
+
stored_path = self._post(
|
|
448
|
+
f"/uploadFile/{urllib.parse.quote(folder_basename, safe='')}"
|
|
449
|
+
f"/{urllib.parse.quote(filename, safe='')}",
|
|
450
|
+
data=text.encode("utf-8"),
|
|
451
|
+
content_type="text/plain; charset=utf-8",
|
|
452
|
+
).strip()
|
|
453
|
+
return stored_path
|
|
454
|
+
|
|
455
|
+
def dg_raw(self, iri: str) -> str:
|
|
456
|
+
"""``GET /dg/<iri>?format=json`` -> the body exactly as HETS sent it.
|
|
457
|
+
|
|
458
|
+
No JSON parsing and NO escape repair. This is the method to reach for
|
|
459
|
+
when :meth:`dg` raises and the body itself needs inspecting — see
|
|
460
|
+
:mod:`~unicode_logic_kit.hets.haskell_json` for the one known reason a
|
|
461
|
+
HETS development-graph body is not valid JSON.
|
|
462
|
+
"""
|
|
463
|
+
return self._get(f"/dg/{_encode_iri(iri)}?format=json")
|
|
464
|
+
|
|
465
|
+
def dg(self, iri: str) -> dict:
|
|
466
|
+
"""``GET /dg/<iri>?format=json`` -> the parsed development-graph dict.
|
|
467
|
+
|
|
468
|
+
``json.loads`` is tried FIRST and the repair runs only when it
|
|
469
|
+
raises. That ordering is the whole design: a body the standard
|
|
470
|
+
library already accepts is never touched at all, so no legitimate
|
|
471
|
+
escape can be mangled by the repair, and the identity invariant is
|
|
472
|
+
structural rather than argued.
|
|
473
|
+
|
|
474
|
+
When the body IS invalid, :func:`~unicode_logic_kit.hets.haskell_json.repair_haskell_json`
|
|
475
|
+
gets one attempt at HETS' Haskell-``show`` escapes (``\\226\\128\\153``
|
|
476
|
+
for ``’`` and friends — see that module for why this is lossless
|
|
477
|
+
recovery of a known emitter). If it finds nothing to repair, the
|
|
478
|
+
original error is re-raised with its original wording; if it repairs
|
|
479
|
+
something and the result STILL does not parse, the error names the
|
|
480
|
+
repair, so a failure is attributable rather than mysterious.
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
RuntimeError: the body is not valid JSON, with or without the
|
|
484
|
+
repair.
|
|
485
|
+
~unicode_logic_kit.hets.haskell_json.HaskellJsonRepairError: a
|
|
486
|
+
Haskell escape in the body is not a Unicode scalar value.
|
|
487
|
+
"""
|
|
488
|
+
body = self.dg_raw(iri)
|
|
489
|
+
try:
|
|
490
|
+
return json.loads(body)
|
|
491
|
+
except json.JSONDecodeError:
|
|
492
|
+
pass
|
|
493
|
+
repair = repair_haskell_json(body)
|
|
494
|
+
if not repair:
|
|
495
|
+
return self._parse_json("dg", body)
|
|
496
|
+
return self._parse_json(f"dg (after repairing {repair.summary()})",
|
|
497
|
+
repair.text)
|
|
498
|
+
|
|
499
|
+
def provers(self, iri: str) -> List[str]:
|
|
500
|
+
"""``GET /provers/<iri>?format=json`` -> prover identifiers HETS can invoke."""
|
|
501
|
+
body = self._get(f"/provers/{_encode_iri(iri)}?format=json")
|
|
502
|
+
data = self._parse_json("provers", body)
|
|
503
|
+
return [p["identifier"] for p in data.get("provers", [])]
|
|
504
|
+
|
|
505
|
+
def translations(self, iri: str, *, node: Optional[str] = None,
|
|
506
|
+
allow_empty: bool = False) -> List[str]:
|
|
507
|
+
"""``GET /translations/<iri>`` -> comorphism names, from the ``<li>`` XML list.
|
|
508
|
+
|
|
509
|
+
The one endpoint that answers XML rather than JSON (point 6 of the
|
|
510
|
+
module docstring). Parsed with :mod:`xml.etree.ElementTree`, not a
|
|
511
|
+
regex, since comorphism names are free text HETS controls, not this
|
|
512
|
+
client.
|
|
513
|
+
|
|
514
|
+
HETS' own IDENTITY entry — an ``<li/>`` with no text — is DROPPED,
|
|
515
|
+
and so is a whitespace-only one. It is not a comorphism name and not
|
|
516
|
+
a legal ``translation=`` value: a caller looping
|
|
517
|
+
``for t in translations(iri): theory(iri, node=n, translation=t)``
|
|
518
|
+
would send ``translation=`` for it, and
|
|
519
|
+
:func:`~unicode_logic_kit.hets.bridge.register_hets_comorphisms` would
|
|
520
|
+
register an edge literally named ``hets:``. "No translation" is
|
|
521
|
+
already expressible as ``translation=None``, so dropping it loses no
|
|
522
|
+
capability. Order and duplicates among the real names are preserved,
|
|
523
|
+
and each name is returned STRIPPED of surrounding whitespace (a name
|
|
524
|
+
with a stray space or newline round-trips into ``translation=`` as a
|
|
525
|
+
different, unknown comorphism).
|
|
526
|
+
|
|
527
|
+
The root element must be ``<Translations>``: any other well-formed
|
|
528
|
+
document is the server refusing the request, which is raised, never
|
|
529
|
+
read as an empty list.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
iri: a stored-path IRI as returned by :meth:`upload`.
|
|
533
|
+
node: a development-graph node name, sent as ``?node=<name>``.
|
|
534
|
+
Purely additive and worth passing: measured on the real
|
|
535
|
+
server, 3.21 s without it against 0.12 s with it for the same
|
|
536
|
+
library. For a single-ontology library the node name is the
|
|
537
|
+
ontology IRI (which :meth:`dg` reports).
|
|
538
|
+
allow_empty: return ``[]`` instead of raising when HETS offers
|
|
539
|
+
nothing.
|
|
540
|
+
|
|
541
|
+
Raises:
|
|
542
|
+
HetsNoTranslationsError: the list came back with no comorphism in
|
|
543
|
+
it and ``allow_empty`` is false. See that class for why an
|
|
544
|
+
empty list is not an answer.
|
|
545
|
+
RuntimeError: the body was not valid XML, or was a well-formed
|
|
546
|
+
document whose root is not ``<Translations>`` (raised even
|
|
547
|
+
with ``allow_empty=True``).
|
|
548
|
+
"""
|
|
549
|
+
import xml.etree.ElementTree as ET
|
|
550
|
+
|
|
551
|
+
path = f"/translations/{_encode_iri(iri)}"
|
|
552
|
+
if node is not None:
|
|
553
|
+
path += f"?node={urllib.parse.quote(node, safe='')}"
|
|
554
|
+
body = self._get(path)
|
|
555
|
+
try:
|
|
556
|
+
root = ET.fromstring(body)
|
|
557
|
+
except ET.ParseError as exc:
|
|
558
|
+
raise RuntimeError(
|
|
559
|
+
f"hets: /translations response was not valid XML ({exc}); "
|
|
560
|
+
f"body excerpt: {body[:_EXCERPT_CHARS]!r}"
|
|
561
|
+
) from exc
|
|
562
|
+
if root.tag != "Translations":
|
|
563
|
+
# A well-formed document of another shape is the server REFUSING
|
|
564
|
+
# (or answering something else), not "this library has no
|
|
565
|
+
# comorphism": with allow_empty=True it used to come back as [],
|
|
566
|
+
# and without it as HetsNoTranslationsError, whose wording
|
|
567
|
+
# ("offers no comorphism") blames the library for it.
|
|
568
|
+
raise RuntimeError(
|
|
569
|
+
f"hets: GET {path} answered an XML document whose root is "
|
|
570
|
+
f"<{root.tag}>, not the <Translations> list this endpoint "
|
|
571
|
+
"returns — the server refused the request or answered "
|
|
572
|
+
"something else, so there is no list to read (and an empty "
|
|
573
|
+
"list is not substituted for it, allow_empty or not); "
|
|
574
|
+
f"body excerpt: {body[:_EXCERPT_CHARS]!r}")
|
|
575
|
+
names = [(li.text or "").strip() for li in root.iter("li")]
|
|
576
|
+
kept = [name for name in names if name]
|
|
577
|
+
if not kept and not allow_empty:
|
|
578
|
+
raise HetsNoTranslationsError(
|
|
579
|
+
f"hets: GET {path} offers no comorphism for this library "
|
|
580
|
+
f"({len(names)} <li> entr{'y' if len(names) == 1 else 'ies'}, "
|
|
581
|
+
"none of them a name). That is not the same as 'there are "
|
|
582
|
+
"none': this endpoint reports no reason at all, and the "
|
|
583
|
+
"reason lives on /theory, which answers HTTP 422 with the "
|
|
584
|
+
"sublogic mismatch spelled out — call "
|
|
585
|
+
"theory(iri, node=..., translation=...) and catch "
|
|
586
|
+
"HetsSublogicError to read it, or take the lossy "
|
|
587
|
+
"command-line route, unicode_logic_kit.hets.owl_to_tptp("
|
|
588
|
+
"path, lossy=True), which translates anyway and reports the "
|
|
589
|
+
"axioms it omitted. Pass allow_empty=True to get [] back "
|
|
590
|
+
"instead of this exception.")
|
|
591
|
+
return kept
|
|
592
|
+
|
|
593
|
+
def theory(self, iri: str, *, node: Optional[str] = None,
|
|
594
|
+
translation: Optional[str] = None) -> str:
|
|
595
|
+
"""``GET /theory/<iri>?…&format=dol`` -> the theory as rendered text.
|
|
596
|
+
|
|
597
|
+
Without ``translation`` this is the (sublogic-annotated) CASL theory
|
|
598
|
+
itself; with a comorphism name (from :meth:`translations`, e.g.
|
|
599
|
+
``CASL2SoftFOL``) it is the TRANSLATED theory in the target logic's
|
|
600
|
+
own concrete syntax (verified live: ``CASL2SoftFOL`` renders
|
|
601
|
+
SoftFOL/DFG ``list_of_symbols``/``formula(...)`` text). ``node``
|
|
602
|
+
selects a development-graph node by name and is EFFECTIVELY
|
|
603
|
+
MANDATORY: this HETS version answers HTTP 500 ``development graph
|
|
604
|
+
node missing`` when it is omitted — with AND without a translation,
|
|
605
|
+
even for single-node libraries (review-corrected; the bridge
|
|
606
|
+
resolves the node from :meth:`dg` for exactly this reason). Callers
|
|
607
|
+
should always pass it; the parameter stays optional only so a
|
|
608
|
+
future server that grows a default keeps working.
|
|
609
|
+
"""
|
|
610
|
+
params = ["format=dol"]
|
|
611
|
+
if node is not None:
|
|
612
|
+
params.append(f"node={urllib.parse.quote(node, safe='')}")
|
|
613
|
+
if translation is not None:
|
|
614
|
+
params.append(
|
|
615
|
+
f"translation={urllib.parse.quote(translation, safe='')}")
|
|
616
|
+
return self._get(f"/theory/{_encode_iri(iri)}?{'&'.join(params)}")
|
|
617
|
+
|
|
618
|
+
def theory_tptp(self, iri: str, *, node: str,
|
|
619
|
+
translation: str = "OWL22CASL:CASL2TPTP_FOF") -> str:
|
|
620
|
+
"""``GET /theory`` with a TPTP-target comorphism, header stripped.
|
|
621
|
+
|
|
622
|
+
The text :meth:`theory` returns for a TPTP comorphism is NOT a TPTP
|
|
623
|
+
problem: HETS puts a DOL ``logic TPTP.FOF`` line and a CASL
|
|
624
|
+
``%{ ... }%`` signature block in front of it, and
|
|
625
|
+
:func:`unicode_logic_kit.fol.tptp_input.parse_tptp` refuses both by
|
|
626
|
+
name. This method returns what the reader actually accepts, so the
|
|
627
|
+
usual call is ``parse_tptp(client.theory_tptp(iri, node=n))``.
|
|
628
|
+
|
|
629
|
+
Measured against the command-line route: for the real 1,554,903-char
|
|
630
|
+
rendering the stripped remainder is byte-identical (after leading
|
|
631
|
+
newlines) to the 1,367,212-byte file ``hets-server -o tptp`` writes,
|
|
632
|
+
so the REST route and the CLI route deliver the same text.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
iri: a stored-path IRI as returned by :meth:`upload`.
|
|
636
|
+
node: the development-graph node name. Mandatory here (unlike
|
|
637
|
+
:meth:`theory`, where it stays optional for a future server):
|
|
638
|
+
this HETS version answers HTTP 500 without it.
|
|
639
|
+
translation: a comorphism whose TARGET is TPTP. The default is
|
|
640
|
+
the one the OWL route needs; ``"OWL22CASL"`` alone returns
|
|
641
|
+
CASL and is refused below rather than silently parsed to an
|
|
642
|
+
empty formula list.
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
The TPTP problem text, header removed.
|
|
646
|
+
|
|
647
|
+
Raises:
|
|
648
|
+
HetsSublogicError: the server answered HTTP 422 because the
|
|
649
|
+
theory is richer than the comorphism covers.
|
|
650
|
+
RuntimeError: the stripped text holds no ``fof``/``cnf``/``tff``
|
|
651
|
+
statement — the comorphism's target was not TPTP, or HETS
|
|
652
|
+
answered something else entirely.
|
|
653
|
+
"""
|
|
654
|
+
text = self.theory(iri, node=node, translation=translation)
|
|
655
|
+
_header, body = strip_hets_theory_header(text)
|
|
656
|
+
if not _TPTP_STATEMENT_RE.search(body):
|
|
657
|
+
first_line = body.strip().split("\n", 1)[0] if body.strip() else ""
|
|
658
|
+
raise RuntimeError(
|
|
659
|
+
f"hets: /theory for node {node!r} with "
|
|
660
|
+
f"translation={translation!r} did not return a TPTP problem "
|
|
661
|
+
"— after stripping HETS' theory header the text has no "
|
|
662
|
+
f"fof/cnf/tff statement; its first line is {first_line!r}. "
|
|
663
|
+
"Pass a translation whose target is TPTP (e.g. "
|
|
664
|
+
"'OWL22CASL:CASL2TPTP_FOF'); 'OWL22CASL' alone returns CASL.")
|
|
665
|
+
return body
|
|
666
|
+
|
|
667
|
+
@staticmethod
|
|
668
|
+
def _prove_body(node: str, *, reasoner: Optional[str], translation: Optional[str],
|
|
669
|
+
time_limit: int) -> dict:
|
|
670
|
+
"""Build the JSON body shared by ``/prove`` and ``/consistency-check``.
|
|
671
|
+
|
|
672
|
+
``translation`` is omitted from the goal dict entirely when ``None``
|
|
673
|
+
(not sent as ``null``) — HETS picks its own default translation for
|
|
674
|
+
the chosen reasoner in that case (e.g. SPASS defaults to
|
|
675
|
+
``CASL2TPTP_FOF``), and an explicit ``null`` is a different, untested
|
|
676
|
+
wire shape this client does not want to invent.
|
|
677
|
+
"""
|
|
678
|
+
goal: Dict[str, object] = {"node": node}
|
|
679
|
+
if translation is not None:
|
|
680
|
+
goal["translation"] = translation
|
|
681
|
+
reasoner_config: Dict[str, object] = {"timeLimit": time_limit}
|
|
682
|
+
if reasoner is not None:
|
|
683
|
+
reasoner_config["reasoner"] = reasoner
|
|
684
|
+
goal["reasonerConfiguration"] = reasoner_config
|
|
685
|
+
return {"format": "json", "goals": [goal]}
|
|
686
|
+
|
|
687
|
+
def _run_goal_endpoint(self, endpoint: str, iri: str, node: str, *,
|
|
688
|
+
reasoner: Optional[str], translation: Optional[str],
|
|
689
|
+
time_limit: int) -> List[dict]:
|
|
690
|
+
body = self._prove_body(node, reasoner=reasoner, translation=translation,
|
|
691
|
+
time_limit=time_limit)
|
|
692
|
+
response = self._post(
|
|
693
|
+
f"/{endpoint}/{_encode_iri(iri)}",
|
|
694
|
+
data=json.dumps(body).encode("utf-8"),
|
|
695
|
+
content_type="application/json",
|
|
696
|
+
)
|
|
697
|
+
# Wire-protocol special case (review-discovered): a node with ZERO
|
|
698
|
+
# %implied goals answers the literal plain-text "nothing to prove"
|
|
699
|
+
# instead of JSON. That is the legitimate empty result, not a
|
|
700
|
+
# malformed response — "one dict per attempted goal" of zero goals.
|
|
701
|
+
if response.strip().lower() == "nothing to prove":
|
|
702
|
+
return []
|
|
703
|
+
parsed = self._parse_json(endpoint, response)
|
|
704
|
+
return [_normalize_goal(g) for g in _extract_goal_objects(parsed)]
|
|
705
|
+
|
|
706
|
+
def prove(self, iri: str, node: str, *, reasoner: Optional[str] = None,
|
|
707
|
+
translation: Optional[str] = None, time_limit: int = 10) -> List[dict]:
|
|
708
|
+
"""``POST /prove/<iri>`` for one goal node; normalized goal-result dicts.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
iri: a stored-path IRI as returned by :meth:`upload` (raw,
|
|
712
|
+
unencoded — this method encodes it).
|
|
713
|
+
node: the development-graph node name to prove goals for.
|
|
714
|
+
reasoner: prover identifier (e.g. ``"SPASS"``, ``"darwin"``);
|
|
715
|
+
``None`` lets HETS pick its default for the node's logic.
|
|
716
|
+
translation: comorphism name (e.g. ``"CASL2TPTP_FOF"``); ``None``
|
|
717
|
+
lets HETS pick its default for the chosen reasoner.
|
|
718
|
+
time_limit: per-goal seconds budget (HETS's ``timeLimit``).
|
|
719
|
+
|
|
720
|
+
Returns:
|
|
721
|
+
One dict per attempted goal (typically one per ``%implied``
|
|
722
|
+
axiom in the node), each with keys ``"name"``, ``"result"``
|
|
723
|
+
(whitespace-stripped — ``"Proved"`` / ``"Disproved"`` /
|
|
724
|
+
``"Open"``), ``"used_prover"``, ``"used_translation"``,
|
|
725
|
+
``"prover_output"``, ``"used_time"``, ``"tactic_script"``.
|
|
726
|
+
``"Open"`` is UNKNOWN, never a disproof — see this module's
|
|
727
|
+
docstring point 9.
|
|
728
|
+
"""
|
|
729
|
+
return self._run_goal_endpoint(
|
|
730
|
+
"prove", iri, node, reasoner=reasoner, translation=translation,
|
|
731
|
+
time_limit=time_limit)
|
|
732
|
+
|
|
733
|
+
def consistency_check(self, iri: str, node: str, *,
|
|
734
|
+
reasoner: str = "darwin-non-fd",
|
|
735
|
+
time_limit: int = 10) -> List[dict]:
|
|
736
|
+
"""``POST /consistency-check/<iri>`` for one node; same shape as :meth:`prove`.
|
|
737
|
+
|
|
738
|
+
Defaults ``reasoner`` to ``"darwin-non-fd"`` (unlike :meth:`prove`,
|
|
739
|
+
which defaults to ``None``/HETS's own choice) because
|
|
740
|
+
consistency-checking specifically wants a FINITE model finder able
|
|
741
|
+
to certify ``"Consistent"`` by exhibiting one — plain ``darwin`` and
|
|
742
|
+
the broken ``eprover``/``Vampire`` reasoners in this image are not
|
|
743
|
+
reliable for that (see :mod:`~unicode_logic_kit.hets.docker`'s
|
|
744
|
+
docstring on image quirks).
|
|
745
|
+
"""
|
|
746
|
+
return self._run_goal_endpoint(
|
|
747
|
+
"consistency-check", iri, node, reasoner=reasoner, translation=None,
|
|
748
|
+
time_limit=time_limit)
|