euromod_linking.compat ====================== .. py:module:: euromod_linking.compat .. autoapi-nested-parse:: Method requirements versus what the model actually provides. A methodology declares what its runs need — ``lma_labour_alignment`` needs the LMA add-on and the ``LMA_trans`` extension switch — and this module checks that the model in front of it has them. An unknown add-on is dropped by the engine with only a console message, so left unchecked both simulations complete normally and produce identical output, and the failure surfaces at the end as `NoEffectError`, after paying for two runs. Answering the question first is why this is called from the ``validate_only`` path. Two kinds of evidence, deliberately unequal. Capability — authoritative -------------------------- Whether the model exposes an add-on and an extension switch is cheap and exact: ``model.addons`` is a directory listing, and `query.system_extension_names` reads what the system will actually accept. This is what decides. Where the model cannot be read at all the answer is *undeterminable*, recorded as a note, never as a failure — "cannot validate" is not "invalid". Release — advisory ------------------ Which EUROMOD release a model folder holds is not reliably knowable. The documented marker ``XMLParam/Config/EuromodVersion.txt`` has writer code in the UI but ships in no real release; the folder name is what the UI itself falls back to (``EM_AppContext.GetProjectName`` feeding ````), and renaming the folder erases it. So a detected release only enriches the message — *"the LMA_trans extension is first shipped in J2.54, this model looks like J2.19"* — and failing to detect one never blocks anything. Set ``EUROMOD_SKIP_COMPAT_CHECK=1`` to bypass the check, mirroring the connector's own ``EUROMOD_SKIP_VERSION_CHECK``. Attributes ---------- .. autoapisummary:: euromod_linking.compat.RELEASE_RE euromod_linking.compat.RELEASE_SOURCES Classes ------- .. autoapisummary:: euromod_linking.compat.CompatibilityReport euromod_linking.compat.Requirement Functions --------- .. autoapisummary:: euromod_linking.compat.canonical_release euromod_linking.compat.check_all euromod_linking.compat.check_compatibility euromod_linking.compat.compatibility_matrix euromod_linking.compat.model_release euromod_linking.compat.parse_release euromod_linking.compat.release_geq Module Contents --------------- .. py:class:: CompatibilityReport What one method needs, what this model has, and the gap between them. ``problems`` are hard failures — the run would not do what was asked. ``notes`` are advisory: an undeterminable requirement, or a release that looks older than the method's floor. Read-only property ``ok`` is True when there are no problems; notes never make a report not-ok. This is the field to check. .. py:attribute:: country_code :type: str .. py:attribute:: method :type: str .. py:attribute:: min_model_release :type: str | None :value: None .. py:attribute:: model_release :type: str | None :value: None .. py:attribute:: notes :type: tuple :value: () .. py:property:: ok :type: bool True when nothing blocks the run. Notes do not make a report not-ok. .. py:attribute:: problems :type: tuple :value: () .. py:attribute:: release_source :type: str | None :value: None .. py:attribute:: requirements :type: tuple :value: () .. py:attribute:: system_name :type: str .. py:class:: Requirement One thing a method needs from the model, and whether it is there. ``satisfied`` is tri-state: True, False, or None when the model could not be read — a distinction that matters, because treating "cannot read" as "not present" would refuse to run against a perfectly good model. .. py:attribute:: detail :type: str :value: '' .. py:attribute:: kind :type: str .. py:attribute:: name :type: str .. py:attribute:: satisfied :type: bool | None :value: None .. py:attribute:: system :type: str | None :value: None .. py:function:: canonical_release(release: str | None) -> str | None ``"..._j2.54"`` -> ``"J2.54"``; unparseable input comes back stripped. Every detection source is run through this, so a report shows one spelling of a release whether it came from a lowercased folder name, a licence file or the change log. .. py:function:: check_all(system, *, deep_release: bool = False) -> list[CompatibilityReport] A report per registered methodology, in registry order. .. py:function:: check_compatibility(system, method, *, deep_release: bool = False) -> CompatibilityReport Can this methodology actually run against this model and system? `method` is a reference like ``"lma_labour_alignment"`` or an `MethodSpec`. `system` is a live connector System, from which the country, the model and the model path are reached. Checks each declared add-on and extension switch against the model, and compares the detected release against the method's ``min_model_release``. Only the first of those can produce a problem; the release only ever adds a note explaining one. With ``deep_release`` the change log is parsed too, at the cost of reading a large workbook. .. py:function:: compatibility_matrix(model, country_code: str | None = None, *, deep_release: bool = False) Which methodologies this model supports, as a DataFrame. One row per (country, system, methodology) with ``ok``, the detected release, and the problems — the answer to "what can I actually run here" without writing a scenario first. Restrict with `country_code`, since walking every country of a full model means loading every country. .. py:function:: model_release(model_path: str, *, deep: bool = False) Best-effort ``(release, source)`` for a model folder, e.g. ``("J2.19", "folder-name")``. Tries the documented version file, then the folder name, then a public release's licence file; with ``deep=True`` it finally parses the change log, which is slow but survives a renamed folder. ``(None, None)`` means the release could not be determined — which callers must treat as unknown, never as too old. .. py:function:: parse_release(release: str | None) Split ``"J2.54"`` into a sortable ``("J", 2, 54, False)``, or None. The last element is the trailing ``+`` of a beta/rolling release, which sorts just above the bare version so ``J1.86+`` is not read as older than ``J1.86``. None for anything that does not look like a release at all — callers treat that as "cannot compare", not as "older". .. py:function:: release_geq(release: str | None, floor: str | None) Is `release` at least `floor`? None when either cannot be parsed. Fails *open* — the caller turns None into a note, not a refusal. This is the opposite of ``euromod.utils._version.version_geq``, which gates the engine and so fails closed; here the capability probe is the real check and the release is only there to write a better message. .. py:data:: RELEASE_RE .. py:data:: RELEASE_SOURCES :value: ('version-file', 'folder-name', 'licence-file', 'em-log')