Source code for pyx12.codes

######################################################################
# Copyright
#   John Holland <john@zoner.org>
# All rights reserved.
#
# This software is licensed as described in the file LICENSE.txt, which
# you should have received as part of this distribution.
#
######################################################################

"""
External Codes interface
"""

from __future__ import annotations

import logging
import os.path
from importlib.resources import files as _res_files
from typing import IO, Any, TypedDict

import defusedxml.ElementTree as et

from pyx12.errors import EngineError


[docs] class CodesError(Exception): """Class for code modules errors."""
class _CodeSet(TypedDict): name: str | None dataele: str | None codes: frozenset[str | None]
[docs] class ExternalCodes: """ Validates an ID against an external list of codes """
[docs] codes: dict[str | None, _CodeSet]
[docs] exclude_list: list[str]
def __init__(self, base_path: str | None = None, exclude: str | None = None) -> None: """ Initialize the external list of codes :param base_path: Override directory containing codes.xml. If None, uses package resource folder. :param exclude: comma-separated string of external code names to ignore. ``self.codes`` maps codeset_id to ``{'name', 'dataele', 'codes'}``. """ logger = logging.getLogger("pyx12") self.codes = {} codes_file = "codes.xml" # ElementTree.parse accepts either text- or binary-mode streams; the # file source differs between override path and bundled package resource. code_fd: IO[Any] if base_path is not None: logger.debug(f"Looking for codes file '{codes_file}' in map_path '{base_path}'") code_fd = open(os.path.join(base_path, codes_file), encoding="utf-8") else: logger.debug(f"Looking for codes file '{codes_file}' in package resources") code_fd = _res_files("pyx12").joinpath("map", codes_file).open("rb") self.exclude_list = exclude.split(",") if exclude is not None else [] with code_fd: parser = et.XMLParser(encoding="utf-8") for cElem in et.parse(code_fd, parser=parser).iter("codeset"): codeset_id = cElem.findtext("id") self.codes[codeset_id] = { "name": cElem.findtext("name"), "dataele": cElem.findtext("data_ele"), "codes": frozenset(c.text for c in cElem.iterfind("version/code")), }
[docs] def isValid(self, key: str | None, code: str | None, check_dte: str | None = None) -> bool: """ Return True if code is in the codeset identified by key. """ if not key: raise EngineError(f"bad key {key!r}") if key in self.exclude_list: return True if key not in self.codes: raise EngineError(f'External Code "{key}" is not defined') return code in self.codes[key]["codes"]
[docs] def debug_print(self, count: int = 10) -> None: """Debug print first <count> codes for each codeset (alphabetical).""" for key in self.codes: print(sorted(self.codes[key]["codes"], key=lambda c: c or "")[:count])
[docs] def list_external_codesets(base_path: str | None = None) -> list[tuple[str, str]]: """Return ``(id, name)`` for every codeset in ``codes.xml``. Used by CLI scripts to populate ``--help`` with the list of names accepted by the ``--exclude-external-codes`` flag. Loads the metadata only — does not parse code values — so the cost is small. :param base_path: Override directory containing codes.xml. If None, uses package resource folder. """ code_fd: IO[Any] if base_path is not None: code_fd = open(os.path.join(base_path, "codes.xml"), encoding="utf-8") else: code_fd = _res_files("pyx12").joinpath("map", "codes.xml").open("rb") pairs: list[tuple[str, str]] = [] with code_fd: parser = et.XMLParser(encoding="utf-8") for cs in et.parse(code_fd, parser=parser).iter("codeset"): cid = (cs.findtext("id") or "").strip() name = (cs.findtext("name") or "").strip() if cid: pairs.append((cid, name)) return pairs