Source code for pyx12.segment

######################################################################
# 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.
#
######################################################################

"""
Implements an interface to a x12 segment.

A segment is comprised of a segment identifier and a sequence of elements.
An element can be a simple element or a composite.  A simple element is
treated as a composite element with one sub-element.

All indexing is zero based.
"""

from __future__ import annotations

import re
from collections.abc import Iterator

import pyx12.path
from pyx12.errors import EngineError

[docs] rec_seg_id = re.compile("^[A-Z][A-Z0-9]{1,2}$", re.S)
[docs] class Element: """ Holds a simple element, which is just a simple string. """
[docs] value: str
def __init__(self, ele_str: str | None) -> None: """ :param ele_str: 1::2 :type ele_str: string """ self.value = ele_str if ele_str is not None else "" def __eq__(self, other: object) -> bool: if isinstance(other, Element): return self.value == other.value return NotImplemented def __lt__(self, other: object) -> bool: return NotImplemented __le__ = __lt__ __le__ = __lt__ __gt__ = __lt__ __ge__ = __lt__ __hash__ = None # type: ignore[assignment] def __len__(self) -> int: """ :rtype: int """ return 1 def __repr__(self) -> str: """ :rtype: string """ return self.value
[docs] def format(self) -> str: """ :rtype: string """ return self.value
[docs] def get_value(self) -> str: """ :rtype: string """ return self.value
[docs] def set_value(self, elem_str: str | None) -> None: """ :param elem_str: Element string value :type elem_str: string """ self.value = elem_str if elem_str is not None else ""
[docs] def is_composite(self) -> bool: """ :rtype: boolean """ return False
[docs] def is_element(self) -> bool: """ :rtype: boolean """ return True
[docs] def is_empty(self) -> bool: """ :rtype: boolean """ if self.value is not None and self.value != "": return False else: return True
# return ''.join([`num` for num in xrange(loop_count)]) # def has_invalid_character(self,
[docs] class Composite: """ Can be a simple element or a composite. A simple element is treated as a composite element with one sub-element. """
[docs] subele_term: str
[docs] subele_term_orig: str
[docs] elements: list[Element]
def __init__(self, ele_str: str, subele_term: str | None = None) -> None: """ :type ele_str: string :raises EngineError: If a terminator is None and no default """ if subele_term is None or len(subele_term) != 1: raise EngineError( "The sub-element terminator must be a single character, is %s" % (subele_term) ) self.subele_term = subele_term self.subele_term_orig = subele_term if ele_str is None: raise EngineError("Element string is None") members = ele_str.split(self.subele_term) self.elements = [] for elem in members: self.elements.append(Element(elem)) def __eq__(self, other: object) -> bool: if isinstance(other, Composite): if len(self.elements) != len(other.elements): return False for i in range(len(self.elements)): if self.elements[i] != other.elements[i]: return False return True return NotImplemented def __lt__(self, other: object) -> bool: return NotImplemented __le__ = __lt__ __le__ = __lt__ __gt__ = __lt__ __ge__ = __lt__ __hash__ = None # type: ignore[assignment] def __getitem__(self, idx: int) -> Element: """ returns Element instance for idx """ return self.elements[idx] def __setitem__(self, idx: int, val: Element) -> None: """ 1 based index [0] throws exception sets element value for idx """ self.elements[idx] = val def __len__(self) -> int: """ :rtype: int """ return len(self.elements) def __repr__(self) -> str: """ :rtype: string """ return self.format(self.subele_term)
[docs] def format(self, subele_term: str | None = None) -> str: """ Format a composite :return: string :raises EngineError: If terminator is None and no default """ if subele_term is None: subele_term = self.subele_term if subele_term is None: raise EngineError("subele_term is None") for i in range(len(self.elements) - 1, -1, -1): if not self.elements[i].is_empty(): break return subele_term.join([Element.__repr__(x) for x in self.elements[: i + 1]])
[docs] def get_value(self) -> str: """ Get value of simple element """ if len(self.elements) == 1: return self.elements[0].get_value() else: raise IndexError("value of composite is undefined")
[docs] def set_subele_term(self, subele_term: str) -> None: """ :param subele_term: Sub-element terminator value :type subele_term: string """ self.subele_term = subele_term
[docs] def is_composite(self) -> bool: """ :rtype: boolean """ if len(self.elements) > 1: return True else: return False
[docs] def is_element(self) -> bool: """ :rtype: boolean """ if len(self.elements) == 1: return True else: return False
[docs] def is_empty(self) -> bool: """ :rtype: boolean """ for ele in self.elements: if not ele.is_empty(): return False return True
[docs] def values_iterator(self) -> Iterator[tuple[str, str]]: for j in range(len(self.elements)): if not self.elements[j].is_empty(): subele_ord = f"{j + 1}" yield (subele_ord, self.elements[j].get_value())
[docs] class Segment: """ Encapsulates a X12 segment. Contains composites. """
[docs] seg_term: str
[docs] seg_term_orig: str
[docs] ele_term: str
[docs] ele_term_orig: str
[docs] subele_term: str
[docs] subele_term_orig: str
[docs] repetition_term: str
[docs] seg_id: str | None
[docs] elements: list[Composite]
def __init__( self, seg_str: str | None, seg_term: str, ele_term: str, subele_term: str, repetition_term: str = "^", ) -> None: """ """ self.seg_term = seg_term self.seg_term_orig = seg_term self.ele_term = ele_term self.ele_term_orig = ele_term self.subele_term = subele_term self.subele_term_orig = subele_term self.repetition_term = repetition_term self.seg_id = None self.elements = [] if seg_str is None or seg_str == "": return if seg_str[-1] == seg_term: elems = seg_str[:-1].split(self.ele_term) else: elems = seg_str.split(self.ele_term) if elems: self.seg_id = elems[0] for ele in elems[1:]: if self.seg_id == "ISA": # Special handling for ISA segment # guarantee subele_term will not be matched self.elements.append(Composite(ele, ele_term)) else: self.elements.append(Composite(ele, subele_term)) def __eq__(self, other: object) -> bool: if isinstance(other, Segment): if self.seg_id != other.seg_id: return False if len(self.elements) != len(other.elements): return False for i in range(len(self.elements)): if self.elements[i] != other.elements[i]: return False return True return NotImplemented def __lt__(self, other: object) -> bool: return NotImplemented __le__ = __lt__ __le__ = __lt__ __gt__ = __lt__ __ge__ = __lt__ __hash__ = None # type: ignore[assignment] def __repr__(self) -> str: """ :rtype: string """ return self.format(self.seg_term, self.ele_term, self.subele_term)
[docs] def append(self, val: str) -> None: """ Append a composite to the segment :param val: String value of composite :type val: string """ self.elements.append(Composite(val, self.subele_term))
def __len__(self) -> int: """ :rtype: int """ return len(self.elements)
[docs] def get_seg_id(self) -> str | None: """ :rtype: string """ return self.seg_id
def _parse_refdes(self, ref_des: str) -> tuple[int | None, int | None]: """ Format of ref_des: - a simple element: TST02 - a composite: TST03 where TST03 is a composite - a sub-element: TST03-2 - or any of the above with the segment ID omitted (02, 03, 03-1) :param ref_des: X12 Reference Designator :type ref_des: string :rtype: tuple(ele_idx, subele_idx) :raises EngineError: If the given ref_des does not match the segment ID or if the indexes are not valid integers """ xp = pyx12.path.X12Path(ref_des) if xp.seg_id is not None and xp.seg_id != self.seg_id: err_str = "Invalid Reference Designator: %s, seg_id: %s" % (ref_des, self.seg_id) raise EngineError(err_str) ele_idx = xp.ele_idx - 1 if xp.ele_idx is not None else None comp_idx = xp.subele_idx - 1 if xp.subele_idx is not None else None return (ele_idx, comp_idx)
[docs] def get(self, ref_des: str) -> Element | Composite | None: """ :param ref_des: X12 Reference Designator :type ref_des: string :return: Element or Composite :rtype: L{segment.Composite} :raises IndexError: If ref_des does not contain a valid element index """ ele_idx, comp_idx = self._parse_refdes(ref_des) if ele_idx is None: raise IndexError(f"{ref_des} is not a valid element index") if ele_idx >= self.__len__(): return None if comp_idx is None: return self.elements[ele_idx] else: if comp_idx >= self.elements[ele_idx].__len__(): return None return self.elements[ele_idx][comp_idx]
[docs] def get_value(self, ref_des: str) -> str | None: """ :param ref_des: X12 Reference Designator :type ref_des: string """ comp1 = self.get(ref_des) if comp1 is None: return None else: return comp1.format()
[docs] def get_value_by_ref_des(self, ref_des: str) -> str: """ :param ref_des: X12 Reference Designator :type ref_des: string Attention: Deprecated - use get_value """ raise DeprecationWarning("Use Segment.get_value")
[docs] def set(self, ref_des: str, val: str) -> None: """ Set the value of an element or subelement identified by the Reference Designator :param ref_des: X12 Reference Designator :type ref_des: string :param val: New value :type val: string """ ele_idx, comp_idx = self._parse_refdes(ref_des) if ele_idx is None: raise IndexError(f"{ref_des} is not a valid element index") while len(self.elements) <= ele_idx: # insert blank values before our value if needed self.elements.append(Composite("", self.subele_term)) if self.seg_id == "ISA" and ele_idx == 15: # Special handling for ISA segment # guarantee subele_term will not be matched self.elements[ele_idx] = Composite(val, self.ele_term) return if comp_idx is None: self.elements[ele_idx] = Composite(val, self.subele_term) else: while len(self.elements[ele_idx]) <= comp_idx: # insert blank values before our value if needed self.elements[ele_idx].elements.append(Element("")) self.elements[ele_idx][comp_idx] = Element(val)
[docs] def is_element(self, ref_des: str) -> bool: """ :param ref_des: X12 Reference Designator :type ref_des: string """ ele_idx = self._parse_refdes(ref_des)[0] if ele_idx is None: raise IndexError(f"{ref_des} is not a valid element index") return self.elements[ele_idx].is_element()
[docs] def is_composite(self, ref_des: str) -> bool: """ :param ref_des: X12 Reference Designator :type ref_des: string """ ele_idx = self._parse_refdes(ref_des)[0] if ele_idx is None: raise IndexError(f"{ref_des} is not a valid element index") return self.elements[ele_idx].is_composite()
[docs] def ele_len(self, ref_des: str) -> int: """ :param ref_des: X12 Reference Designator :type ref_des: string :return: number of sub-elements in an element or composite :rtype: int """ ele_idx = self._parse_refdes(ref_des)[0] if ele_idx is None: raise IndexError(f"{ref_des} is not a valid element index") return len(self.elements[ele_idx])
[docs] def set_seg_term(self, seg_term: str) -> None: """ :param seg_term: Segment terminator :type seg_term: string """ self.seg_term = seg_term
[docs] def set_ele_term(self, ele_term: str) -> None: """ :param ele_term: Element terminator :type ele_term: string """ self.ele_term = ele_term
[docs] def set_subele_term(self, subele_term: str) -> None: """ :param subele_term: Sub-element terminator :type subele_term: string """ self.subele_term = subele_term
[docs] def format( self, seg_term: str | None = None, ele_term: str | None = None, subele_term: str | None = None, ) -> str: """ :rtype: string :raises EngineError: If a terminator is None and no default """ if seg_term is None: seg_term = self.seg_term if ele_term is None: ele_term = self.ele_term if subele_term is None: subele_term = self.subele_term if seg_term is None: raise EngineError("seg_term is None") if ele_term is None: raise EngineError("ele_term is None") if subele_term is None: raise EngineError("subele_term is None") str_elems: list[str] = [] # get index of last non-empty element i = 0 for i in range(len(self.elements) - 1, -1, -1): if not self.elements[i].is_empty(): break for ele in self.elements[: i + 1]: str_elems.append(ele.format(subele_term)) return "%s%s%s%s" % (self.seg_id, ele_term, ele_term.join(str_elems), seg_term)
[docs] def format_ele_list(self, str_elems: list[str], subele_term: str | None = None) -> None: """ Modifies the parameter str_elems Strips trailing empty composites """ if subele_term is None: subele_term = self.subele_term # Find last non-empty composite for i in range(len(self.elements) - 1, -1, -1): if not self.elements[i].is_empty(): break for ele in self.elements[: i + 1]: str_elems.append(ele.format(subele_term))
[docs] def is_empty(self) -> bool: """ :rtype: boolean """ if len(self.elements) == 0: return True for ele in self.elements: if not ele.is_empty(): return False return True
[docs] def is_seg_id_valid(self) -> bool: """ Is the Segment identifier valid? EBNF: <seg_id> ::= <letter_or_digit> <letter_or_digit> [<letter_or_digit>] :rtype: boolean """ if not self.seg_id or len(self.seg_id) < 2 or len(self.seg_id) > 3: return False else: m = rec_seg_id.search(self.seg_id) if not m: return False # Invalid char matched return True
[docs] def copy(self) -> Segment: return self.__copy__()
def __copy__(self) -> Segment: return Segment(self.format(), self.seg_term, self.ele_term, self.subele_term)
[docs] def values_iterator(self) -> Iterator[tuple[str, str, str | None, str]]: """ Enumerate over the values in the segment, adding the path, element index and sub-element index """ for i in range(len(self.elements)): if self.elements[i].is_composite(): for comp_ord, val in self.elements[i].values_iterator(): ele_ord = f"{i + 1:0>2}" refdes = f"{self.seg_id}{ele_ord}-{comp_ord}" yield (refdes, ele_ord, comp_ord, val) else: if not self.elements[i].is_empty(): ele_ord = f"{i + 1:0>2}" refdes = f"{self.seg_id}{ele_ord}" yield (refdes, ele_ord, None, self.elements[i].get_value())