pyx12 4.0.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 (99) hide show
  1. pyx12/__init__.py +10 -0
  2. pyx12/codes.py +120 -0
  3. pyx12/dataele.py +103 -0
  4. pyx12/decorators.py +79 -0
  5. pyx12/errh_json.py +161 -0
  6. pyx12/errh_xml.py +324 -0
  7. pyx12/error_997.py +457 -0
  8. pyx12/error_999.py +391 -0
  9. pyx12/error_debug.py +148 -0
  10. pyx12/error_handler.py +1359 -0
  11. pyx12/error_html.py +255 -0
  12. pyx12/error_item.py +102 -0
  13. pyx12/error_visitor.py +80 -0
  14. pyx12/errors.py +73 -0
  15. pyx12/map/270.4010.X092.A1.xml +2995 -0
  16. pyx12/map/271.4010.X092.A1.xml +4874 -0
  17. pyx12/map/276.4010.X093.A1.xml +2166 -0
  18. pyx12/map/277.4010.X093.A1.xml +2669 -0
  19. pyx12/map/277.5010.X212.xml +4179 -0
  20. pyx12/map/277.5010.X214.xml +2605 -0
  21. pyx12/map/277U.4010.X070.xml +2668 -0
  22. pyx12/map/278.4010.X094.27.A1.xml +7321 -0
  23. pyx12/map/278.4010.X094.A1.xml +9385 -0
  24. pyx12/map/820.4010.X061.A1.xml +2313 -0
  25. pyx12/map/820.5010.X218.v2.xml +1029 -0
  26. pyx12/map/820.5010.X218.xml +3142 -0
  27. pyx12/map/830.4010.PS.xml +881 -0
  28. pyx12/map/834.4010.X095.A1.xml +4032 -0
  29. pyx12/map/834.5010.X220.A1.v2.xml +2145 -0
  30. pyx12/map/834.5010.X220.A1.xml +5127 -0
  31. pyx12/map/835.4010.X091.A1.xml +3587 -0
  32. pyx12/map/835.5010.X221.A1.v2.xml +1410 -0
  33. pyx12/map/835.5010.X221.A1.xml +4233 -0
  34. pyx12/map/837.4010.X096.A1.xml +39589 -0
  35. pyx12/map/837.4010.X097.A1.xml +11478 -0
  36. pyx12/map/837.4010.X098.A1.xml +23842 -0
  37. pyx12/map/837.5010.X222.A1.xml +25900 -0
  38. pyx12/map/837Q3.I.5010.X223.A1.v2.xml +9987 -0
  39. pyx12/map/837Q3.I.5010.X223.A1.xml +30108 -0
  40. pyx12/map/841.4010.XXXC.xml +865 -0
  41. pyx12/map/997.4010.xml +961 -0
  42. pyx12/map/999.5010.xml +1283 -0
  43. pyx12/map/999.5010X231.A1.xml +1283 -0
  44. pyx12/map/README +16 -0
  45. pyx12/map/README.md +20 -0
  46. pyx12/map/codes.xml +2844 -0
  47. pyx12/map/codes.xsd +40 -0
  48. pyx12/map/comp_test.xml +69 -0
  49. pyx12/map/dataele.xml +363 -0
  50. pyx12/map/dataele.xsd +60 -0
  51. pyx12/map/dump.py +22 -0
  52. pyx12/map/map.v2.xsd +135 -0
  53. pyx12/map/map.xsd +150 -0
  54. pyx12/map/maps.xml +85 -0
  55. pyx12/map/maps.xsd +109 -0
  56. pyx12/map/ord.py +39 -0
  57. pyx12/map/x12.control.00401.xml +386 -0
  58. pyx12/map/x12.control.00501.xml +369 -0
  59. pyx12/map/x12simple.dtd +16 -0
  60. pyx12/map_if/__init__.py +39 -0
  61. pyx12/map_if/_base.py +189 -0
  62. pyx12/map_if/_composite.py +160 -0
  63. pyx12/map_if/_element.py +391 -0
  64. pyx12/map_if/_loader.py +58 -0
  65. pyx12/map_if/_loop.py +318 -0
  66. pyx12/map_if/_root.py +222 -0
  67. pyx12/map_if/_segment.py +513 -0
  68. pyx12/map_index.py +146 -0
  69. pyx12/map_override.py +45 -0
  70. pyx12/map_walker.py +695 -0
  71. pyx12/nodeCounter.py +88 -0
  72. pyx12/params.py +160 -0
  73. pyx12/path.py +218 -0
  74. pyx12/rawx12file.py +98 -0
  75. pyx12/scripts/__init__.py +22 -0
  76. pyx12/scripts/x12html.py +166 -0
  77. pyx12/scripts/x12info.py +107 -0
  78. pyx12/scripts/x12norm.py +106 -0
  79. pyx12/scripts/x12valid.py +197 -0
  80. pyx12/scripts/x12xml.py +153 -0
  81. pyx12/scripts/xmlx12.py +105 -0
  82. pyx12/segment.py +589 -0
  83. pyx12/syntax.py +147 -0
  84. pyx12/validation.py +275 -0
  85. pyx12/version.py +1 -0
  86. pyx12/x12context.py +1250 -0
  87. pyx12/x12file.py +687 -0
  88. pyx12/x12metadata.py +278 -0
  89. pyx12/x12n_document.py +290 -0
  90. pyx12/x12xml.py +234 -0
  91. pyx12/x12xml_simple.py +128 -0
  92. pyx12/xmlwriter.py +155 -0
  93. pyx12/xmlx12_simple.py +60 -0
  94. pyx12-4.0.0.dist-info/METADATA +173 -0
  95. pyx12-4.0.0.dist-info/RECORD +99 -0
  96. pyx12-4.0.0.dist-info/WHEEL +5 -0
  97. pyx12-4.0.0.dist-info/entry_points.txt +7 -0
  98. pyx12-4.0.0.dist-info/licenses/LICENSE.txt +28 -0
  99. pyx12-4.0.0.dist-info/top_level.txt +1 -0
pyx12/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Validate and transform HIPAA X12 documents
3
+ """
4
+
5
+ __author__ = "John Holland <john.holland@swmbh.org>"
6
+ __status__ = "production"
7
+ from .version import __version__ as __version__
8
+
9
+ __date__ = "2021-05-28"
10
+ __revision__ = ""
pyx12/codes.py ADDED
@@ -0,0 +1,120 @@
1
+ ######################################################################
2
+ # Copyright
3
+ # John Holland <john@zoner.org>
4
+ # All rights reserved.
5
+ #
6
+ # This software is licensed as described in the file LICENSE.txt, which
7
+ # you should have received as part of this distribution.
8
+ #
9
+ ######################################################################
10
+
11
+ """
12
+ External Codes interface
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os.path
19
+ from importlib.resources import files as _res_files
20
+ from typing import IO, Any, TypedDict
21
+
22
+ import defusedxml.ElementTree as et
23
+
24
+ from pyx12.errors import EngineError
25
+
26
+
27
+ class CodesError(Exception):
28
+ """Class for code modules errors."""
29
+
30
+
31
+ class _CodeSet(TypedDict):
32
+ name: str | None
33
+ dataele: str | None
34
+ codes: frozenset[str | None]
35
+
36
+
37
+ class ExternalCodes:
38
+ """
39
+ Validates an ID against an external list of codes
40
+ """
41
+
42
+ codes: dict[str | None, _CodeSet]
43
+ exclude_list: list[str]
44
+
45
+ def __init__(self, base_path: str | None = None, exclude: str | None = None) -> None:
46
+ """
47
+ Initialize the external list of codes
48
+
49
+ :param base_path: Override directory containing codes.xml.
50
+ If None, uses package resource folder.
51
+ :param exclude: comma-separated string of external code names to ignore.
52
+
53
+ ``self.codes`` maps codeset_id to ``{'name', 'dataele', 'codes'}``.
54
+ """
55
+ logger = logging.getLogger("pyx12")
56
+ self.codes = {}
57
+ codes_file = "codes.xml"
58
+ # ElementTree.parse accepts either text- or binary-mode streams; the
59
+ # file source differs between override path and bundled package resource.
60
+ code_fd: IO[Any]
61
+ if base_path is not None:
62
+ logger.debug(f"Looking for codes file '{codes_file}' in map_path '{base_path}'")
63
+ code_fd = open(os.path.join(base_path, codes_file), encoding="utf-8")
64
+ else:
65
+ logger.debug(f"Looking for codes file '{codes_file}' in package resources")
66
+ code_fd = _res_files("pyx12").joinpath("map", codes_file).open("rb")
67
+
68
+ self.exclude_list = exclude.split(",") if exclude is not None else []
69
+ with code_fd:
70
+ parser = et.XMLParser(encoding="utf-8")
71
+ for cElem in et.parse(code_fd, parser=parser).iter("codeset"):
72
+ codeset_id = cElem.findtext("id")
73
+ self.codes[codeset_id] = {
74
+ "name": cElem.findtext("name"),
75
+ "dataele": cElem.findtext("data_ele"),
76
+ "codes": frozenset(c.text for c in cElem.iterfind("version/code")),
77
+ }
78
+
79
+ def isValid(self, key: str | None, code: str | None, check_dte: str | None = None) -> bool:
80
+ """
81
+ Return True if code is in the codeset identified by key.
82
+ """
83
+ if not key:
84
+ raise EngineError(f"bad key {key!r}")
85
+ if key in self.exclude_list:
86
+ return True
87
+ if key not in self.codes:
88
+ raise EngineError(f'External Code "{key}" is not defined')
89
+ return code in self.codes[key]["codes"]
90
+
91
+ def debug_print(self, count: int = 10) -> None:
92
+ """Debug print first <count> codes for each codeset (alphabetical)."""
93
+ for key in self.codes:
94
+ print(sorted(self.codes[key]["codes"], key=lambda c: c or "")[:count])
95
+
96
+
97
+ def list_external_codesets(base_path: str | None = None) -> list[tuple[str, str]]:
98
+ """Return ``(id, name)`` for every codeset in ``codes.xml``.
99
+
100
+ Used by CLI scripts to populate ``--help`` with the list of names
101
+ accepted by the ``--exclude-external-codes`` flag. Loads the metadata
102
+ only — does not parse code values — so the cost is small.
103
+
104
+ :param base_path: Override directory containing codes.xml.
105
+ If None, uses package resource folder.
106
+ """
107
+ code_fd: IO[Any]
108
+ if base_path is not None:
109
+ code_fd = open(os.path.join(base_path, "codes.xml"), encoding="utf-8")
110
+ else:
111
+ code_fd = _res_files("pyx12").joinpath("map", "codes.xml").open("rb")
112
+ pairs: list[tuple[str, str]] = []
113
+ with code_fd:
114
+ parser = et.XMLParser(encoding="utf-8")
115
+ for cs in et.parse(code_fd, parser=parser).iter("codeset"):
116
+ cid = (cs.findtext("id") or "").strip()
117
+ name = (cs.findtext("name") or "").strip()
118
+ if cid:
119
+ pairs.append((cid, name))
120
+ return pairs
pyx12/dataele.py ADDED
@@ -0,0 +1,103 @@
1
+ ######################################################################
2
+ # Copyright (c) 2001-2019
3
+ # John Holland <john@zoner.org>
4
+ # All rights reserved.
5
+ #
6
+ # This software is licensed as described in the file LICENSE.txt, which
7
+ # you should have received as part of this distribution.
8
+ #
9
+ ######################################################################
10
+
11
+ """
12
+ Interface to normalized Data Elements
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os.path
19
+ from importlib.resources import files as _res_files
20
+ from typing import IO, Any, TypedDict
21
+
22
+ import defusedxml.ElementTree as et
23
+
24
+ # Intrapackage imports
25
+ from pyx12.errors import EngineError
26
+
27
+
28
+ class DataElementsError(Exception):
29
+ """Class for data elements module errors."""
30
+
31
+
32
+ class _DataEle(TypedDict):
33
+ data_type: str | None
34
+ min_len: int
35
+ max_len: int
36
+ name: str | None
37
+
38
+
39
+ class DataElements:
40
+ """
41
+ Interface to normalized Data Elements
42
+ """
43
+
44
+ dataele: dict[str | None, _DataEle]
45
+
46
+ def __init__(self, base_path: str | None = None) -> None:
47
+ """
48
+ Initialize the list of data elements
49
+
50
+ :param base_path: Override directory containing dataele.xml. If None,
51
+ uses package resource folder
52
+ :type base_path: string
53
+
54
+ Note: ``self.dataele`` maps to the data element
55
+ ``{ele_num: {data_type, min_len, max_len, name}}``
56
+ """
57
+ logger = logging.getLogger("pyx12")
58
+ self.dataele = {}
59
+ dataele_file = "dataele.xml"
60
+ # ElementTree.parse accepts either text- or binary-mode streams; the
61
+ # file source differs between override path and bundled package resource.
62
+ fd: IO[Any]
63
+ if base_path is not None:
64
+ logger.debug(
65
+ f"Looking for data element definition file '{dataele_file}' in map_path '{base_path}'"
66
+ )
67
+ fd = open(os.path.join(base_path, dataele_file), encoding="utf-8")
68
+ else:
69
+ logger.debug(
70
+ f"Looking for data element definition file '{dataele_file}' in package resources"
71
+ )
72
+ fd = _res_files("pyx12").joinpath("map", dataele_file).open("rb")
73
+ with fd:
74
+ parser = et.XMLParser(encoding="utf-8")
75
+ for eElem in et.parse(fd, parser=parser).iter("data_ele"):
76
+ ele_num = eElem.get("ele_num")
77
+ self.dataele[ele_num] = {
78
+ "data_type": eElem.get("data_type"),
79
+ "min_len": int(eElem.get("min_len")),
80
+ "max_len": int(eElem.get("max_len")),
81
+ "name": eElem.get("name"),
82
+ }
83
+
84
+ def get_by_elem_num(self, ele_num: str | None) -> _DataEle:
85
+ """
86
+ Get the element characteristics for the indexed element code
87
+ :param ele_num: the data element code
88
+ :type ele_num: string
89
+ :return: {data_type, min_len, max_len, name}
90
+ :rtype: dict
91
+ """
92
+ if not ele_num:
93
+ raise EngineError(f"Bad data element {ele_num!r}")
94
+ if ele_num not in self.dataele:
95
+ raise EngineError(f'Data Element "{ele_num}" is not defined')
96
+ return self.dataele[ele_num]
97
+
98
+ def debug_print(self) -> None:
99
+ """
100
+ Debug print data elements
101
+ """
102
+ for ele_num in self.dataele:
103
+ print(self.dataele[ele_num])
pyx12/decorators.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+ import functools
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ # See https://wiki.python.org/moin/PythonDecoratorLibrary
9
+
10
+
11
+ def dump_args(func: Callable[..., Any]) -> Callable[..., Any]:
12
+ "This decorator dumps out the arguments passed to a function before calling it"
13
+ argnames = func.__code__.co_varnames[: func.__code__.co_argcount]
14
+ fname = func.__name__
15
+
16
+ def echo_func(*args: Any, **kwargs: Any) -> Any:
17
+ print(
18
+ (
19
+ fname,
20
+ ":",
21
+ ", ".join(
22
+ "%s=%r" % entry for entry in list(zip(argnames, args)) + list(kwargs.items())
23
+ ),
24
+ )
25
+ )
26
+ return func(*args, **kwargs)
27
+
28
+ return echo_func
29
+
30
+
31
+ def memoize(obj: Callable[..., Any]) -> Callable[..., Any]:
32
+ cache: dict[Any, Any] = {}
33
+ obj.cache = cache # type: ignore[attr-defined]
34
+
35
+ @functools.wraps(obj)
36
+ def memoizer(*args: Any, **kwargs: Any) -> Any:
37
+ if kwargs: # frozenset is used to ensure hashability
38
+ key: Any = (args, frozenset(kwargs.items()))
39
+ else:
40
+ key = args
41
+ if key not in cache:
42
+ cache[key] = obj(*args, **kwargs)
43
+ return cache[key]
44
+
45
+ return memoizer
46
+
47
+
48
+ class memoized:
49
+ """Decorator. Caches a function's return value each time it is called.
50
+ If called later with the same arguments, the cached value is returned
51
+ (not reevaluated).
52
+ """
53
+
54
+ func: Callable[..., Any]
55
+ cache: dict[Any, Any]
56
+
57
+ def __init__(self, func: Callable[..., Any]) -> None:
58
+ self.func = func
59
+ self.cache = {}
60
+
61
+ def __call__(self, *args: Any) -> Any:
62
+ if not isinstance(args, collections.abc.Hashable):
63
+ # uncacheable. a list, for instance.
64
+ # better to not cache than blow up.
65
+ return self.func(*args)
66
+ if args in self.cache:
67
+ return self.cache[args]
68
+ else:
69
+ value = self.func(*args)
70
+ self.cache[args] = value
71
+ return value
72
+
73
+ def __repr__(self) -> str:
74
+ """Return the function's docstring."""
75
+ return self.func.__doc__ or ""
76
+
77
+ def __get__(self, obj: Any, objtype: Any) -> Callable[..., Any]:
78
+ """Support instance methods."""
79
+ return functools.partial(self.__call__, obj)
pyx12/errh_json.py ADDED
@@ -0,0 +1,161 @@
1
+ ######################################################################
2
+ # Copyright
3
+ # John Holland <john@zoner.org>
4
+ # All rights reserved.
5
+ #
6
+ # This software is licensed as described in the file LICENSE.txt, which
7
+ # you should have received as part of this distribution.
8
+ #
9
+ ######################################################################
10
+
11
+ """
12
+ Generates a JSON document of validation errors.
13
+ Visitor - Visits an error_handler composite (issue #50).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from typing import Any, TextIO
20
+
21
+ import pyx12.error_visitor
22
+
23
+
24
+ class errh_json_visitor(pyx12.error_visitor.error_visitor):
25
+ """
26
+ Visit an error_handler composite. Accumulate a nested dict mirroring
27
+ the ISA / GS / ST / segment / element hierarchy and dump it once as
28
+ JSON in visit_root_post.
29
+
30
+ Streaming JSON during the walk would require closing arrays mid-walk;
31
+ the err_handler tree always fits in memory for any X12 document, so
32
+ accumulate-then-dump is the right shape.
33
+ """
34
+
35
+ fd: TextIO
36
+ indent: int | None
37
+ doc: dict[str, Any]
38
+ _stack: list[dict[str, Any]]
39
+ _cur_seg: dict[str, Any] | None
40
+
41
+ def __init__(self, fd: TextIO, indent: int | None = 2) -> None:
42
+ """
43
+ :param fd: target file
44
+ :type fd: file descriptor
45
+ :param indent: json.dump indent argument; pass None for compact output
46
+ :type indent: int | None
47
+ """
48
+ self.fd = fd
49
+ self.indent = indent
50
+ self.doc = {"interchanges": []}
51
+ self._stack = []
52
+ self._cur_seg = None
53
+
54
+ def visit_root_post(self, errh: Any) -> None:
55
+ """
56
+ :param errh: Error handler
57
+ :type errh: L{error_handler.err_handler}
58
+ """
59
+ json.dump(self.doc, self.fd, indent=self.indent, default=str)
60
+
61
+ def visit_isa_pre(self, err_isa: Any) -> None:
62
+ """
63
+ :param err_isa: ISA error node
64
+ :type err_isa: L{error_handler.err_isa}
65
+ """
66
+ isa_dict: dict[str, Any] = {
67
+ "isa_trn_set_id": err_isa.isa_trn_set_id,
68
+ "ta1_req": err_isa.ta1_req,
69
+ "orig_date": err_isa.orig_date,
70
+ "orig_time": err_isa.orig_time,
71
+ "cur_line": err_isa.get_cur_line(),
72
+ "errors": [],
73
+ "groups": [],
74
+ }
75
+ self.doc["interchanges"].append(isa_dict)
76
+ self._stack.append(isa_dict)
77
+
78
+ def visit_isa_post(self, err_isa: Any) -> None:
79
+ isa_dict = self._stack[-1]
80
+ isa_dict["errors"] = [
81
+ {"err_cde": cde, "err_str": err_str} for (cde, err_str) in err_isa.errors
82
+ ]
83
+ self._stack.pop()
84
+
85
+ def visit_gs_pre(self, err_gs: Any) -> None:
86
+ gs_dict: dict[str, Any] = {
87
+ "gs_control_num": err_gs.gs_control_num,
88
+ "fic": err_gs.fic,
89
+ "vriic": err_gs.vriic,
90
+ "ack_code": err_gs.ack_code,
91
+ "st_count_orig": err_gs.st_count_orig,
92
+ "st_count_recv": err_gs.st_count_recv,
93
+ "cur_line": err_gs.get_cur_line(),
94
+ "errors": [],
95
+ "transactions": [],
96
+ }
97
+ self._stack[-1]["groups"].append(gs_dict)
98
+ self._stack.append(gs_dict)
99
+
100
+ def visit_gs_post(self, err_gs: Any) -> None:
101
+ gs_dict = self._stack[-1]
102
+ gs_dict["errors"] = [
103
+ {"err_cde": cde, "err_str": err_str} for (cde, err_str) in err_gs.errors
104
+ ]
105
+ self._stack.pop()
106
+
107
+ def visit_st_pre(self, err_st: Any) -> None:
108
+ st_dict: dict[str, Any] = {
109
+ "trn_set_id": err_st.trn_set_id,
110
+ "trn_set_control_num": err_st.trn_set_control_num,
111
+ "vriic": err_st.vriic,
112
+ "ack_code": err_st.ack_code,
113
+ "cur_line": err_st.get_cur_line(),
114
+ "errors": [],
115
+ "segments": [],
116
+ }
117
+ self._stack[-1]["transactions"].append(st_dict)
118
+ self._stack.append(st_dict)
119
+
120
+ def visit_st_post(self, err_st: Any) -> None:
121
+ st_dict = self._stack[-1]
122
+ st_dict["errors"] = [
123
+ {"err_cde": cde, "err_str": err_str} for (cde, err_str) in err_st.errors
124
+ ]
125
+ self._stack.pop()
126
+
127
+ def visit_seg(self, err_seg: Any) -> None:
128
+ seg_dict: dict[str, Any] = {
129
+ "seg_id": err_seg.seg_id,
130
+ "seg_count": err_seg.seg_count,
131
+ "pos": err_seg.pos,
132
+ "name": err_seg.name,
133
+ "ls_id": err_seg.ls_id,
134
+ "cur_line": err_seg.get_cur_line(),
135
+ "errors": [
136
+ {"err_cde": cde, "err_str": err_str, "err_val": err_val}
137
+ for (cde, err_str, err_val) in err_seg.errors
138
+ ],
139
+ "elements": [],
140
+ }
141
+ self._stack[-1]["segments"].append(seg_dict)
142
+ self._cur_seg = seg_dict
143
+
144
+ def visit_ele(self, err_ele: Any) -> None:
145
+ # err_ele.accept is only invoked from err_seg.accept (ISA / GS / ST
146
+ # accept methods do not walk their .elements list), so a non-None
147
+ # _cur_seg is guaranteed at this point.
148
+ if self._cur_seg is None:
149
+ return
150
+ ele_dict: dict[str, Any] = {
151
+ "ele_pos": err_ele.ele_pos,
152
+ "subele_pos": err_ele.subele_pos,
153
+ "repeat_pos": err_ele.repeat_pos,
154
+ "ele_ref_num": err_ele.ele_ref_num,
155
+ "name": err_ele.name,
156
+ "errors": [
157
+ {"err_cde": cde, "err_str": err_str, "err_val": err_val}
158
+ for (cde, err_str, err_val) in err_ele.errors
159
+ ],
160
+ }
161
+ self._cur_seg["elements"].append(ele_dict)