Coverage for libs/sdc_etl_libs/sdc_file_helpers/TechnicalStandards/EDI/SDCEDIObject.py : 89%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import datetime
2import functools
3import json
4import logging
5from collections import deque, namedtuple
7from sdc_etl_libs.sdc_file_helpers.TechnicalStandards.EDI.SDCEDIExceptions import \
8 EDITransactionNotFound
11class SDCEDIObject:
13 Segment = namedtuple('Segment', ['SEGMENT_ID', 'POSITION', 'DETAIL'])
14 FIELD_SEPARATOR = '~'
15 SEGMENT_SEPARATOR = "|"
17 def __init__(self, file_obj_, field_separator_=FIELD_SEPARATOR, segment_separator_=SEGMENT_SEPARATOR):
18 """
19 Creates an Python object representing a EDI file
20 :param file_obj_: Raw file object
21 :return: SDCEDIFile
22 """
23 self.field_separator = field_separator_
24 self.segment_separator = segment_separator_
25 self.segments = self._extract_segments(file_obj_)
27 @staticmethod
28 def _get_segment_name(segment_id: str, position: int) -> str:
29 """
30 Make key joining segment_id and position
31 :param segment_id: str
32 :param position: int
33 :return: str
34 """
35 number = str(position + 1).zfill(2)
36 return '{0}{1}'.format(segment_id, number)
38 @staticmethod
39 def _extract_separator(edi_lines) -> str:
40 """
41 Extract character separator
42 :param edi_lines: list[str]
43 :return: str
44 """
45 return edi_lines[0][3:4]
47 def _extract_segment(self, enumeration_token):
48 """
49 Build an instance of Segment class by parsing an edi text line
50 :param enumeration_token:str
51 :return: Segment
52 """
53 # get position and text token
54 position, tokens = enumeration_token
55 # build null object if text is empty
56 if not tokens:
57 return self.Segment(SEGMENT_ID='Nil', POSITION=-1, DETAIL={})
58 # converts list to cons list
59 queue_tokens = deque(tokens)
60 # extract head and tail
61 # head is edi segment, tail are segment attributes
62 head, tail = queue_tokens.popleft(), queue_tokens
63 # convert text fields to dict
64 tail_detail = [{self._get_segment_name(head, i): field} for i, field in enumerate(queue_tokens)]
65 # reduce by dict to consolidate dicts
66 detail = functools.reduce(lambda dict_a, dict_b: dict(dict_a, **dict_b), tail_detail, {})
67 # return Segment object
68 return self.Segment(SEGMENT_ID=head, POSITION=position, DETAIL=detail)
70 def _find_segment(self, segment_id: str):
71 """
72 Searches segments by id
73 :param segment_id:str
74 :return: list[Segment]
75 """
76 return list(filter(lambda segment: segment.SEGMENT_ID == segment_id, self.segments))
78 def _convert_segment_to_json(self, segment_id):
79 """
80 Converts a segment into json
81 :param segment_id:str
82 :return: JSON object
83 """
84 return json.dumps([segment.DETAIL for segment in self._find_segment(segment_id)])
86 def _extract_segments(self, file_obj):
87 """
88 Build a list of objects of type Segment
89 :param file_obj:StringIO
90 :return: list[Segment]
91 """
92 # move cursor to the first position in StringIO stream
93 file_obj.seek(0)
94 # extract edi_lines
95 # new line is defined as \\n
96 edi_lines = [segment for segment in file_obj.read().decode().splitlines()
97 ] if self.segment_separator == '\\n' else [
98 segment for segment in file_obj.read().decode().split(self.segment_separator)
99 ]
100 # extract separator
101 #separator = self._extract_separator(edi_lines) - alternative algorithm
102 separator = self.field_separator
103 # extract segments as tokens
104 edi_lines_tokens = [segment.split(separator) for segment in edi_lines]
105 # identify position of each token
106 edi_lines_tokens_with_pos = [(pos, value) for pos, value in enumerate(edi_lines_tokens)]
107 # extract segments from tokens
108 return list(map(self._extract_segment, edi_lines_tokens_with_pos))
110 def get_transactions_groups(self):
111 """
112 Group by transactions
113 :return: list[Segment]
114 """
115 # detail section
116 # find the beginning of each transaction
117 list_edi_st = self._find_segment('ST')
118 # find the end for of each transaction
119 list_edi_se = self._find_segment('SE')
120 # find start position of each transaction groups
121 edi_invoices_positions = [segment.POSITION for segment in list_edi_st]
122 # find last transaction
123 last_se = list_edi_se[len(list_edi_se) - 1]
124 # add last transaction position to the list of positions
125 edi_invoices_positions.append(last_se.POSITION)
126 # create a list of intervals
127 transaction_intervals = list(zip(edi_invoices_positions, edi_invoices_positions[1:]))
128 # extract transaction groups using intervals
129 transaction_groups = [self.segments[start:end] for start, end in transaction_intervals]
130 return transaction_groups
132 def get_headers(self):
133 """
134 Get Segment object with headers information
135 :return: list[Segment]
136 """
137 isa_query = self._find_segment('ISA')
138 gs_query = self._find_segment('GS')
139 ge_query = self._find_segment('GE')
140 iea_query = self._find_segment('IEA')
142 if gs_query:
143 gs = gs_query[0]
144 else:
145 gs = None
146 return [isa_query[0], gs, ge_query[0], iea_query[0]]
148 def get_data_date(self):
149 """
150 Get origin date
151 :return: datetime
152 """
153 try:
154 # parse segment B3
155 list_edi_b3 = self._find_segment('B3')
156 # segment B3 position 6 contains the date
157 data_date = list_edi_b3[0].DETAIL['B306']
158 return datetime.datetime.strptime(data_date, '%Y%m%d').strftime("%Y-%m-%d %H:%M:%S.%f")
159 except Exception as e:
160 raise EDITransactionNotFound("Transactions not found")
162 @staticmethod
163 def convert_segment_to_edi(segment, field_separator=FIELD_SEPARATOR, segment_separator=SEGMENT_SEPARATOR):
164 """
165 Transform a Segment Object to EDI text
166 :param segment:Segment
167 :param field_separator:str
168 :param segment_separator:str
169 :return: list[Segment]
170 """
171 segment_fields = [field_separator + segment.DETAIL[field] for field in segment.DETAIL]
172 return segment.SEGMENT_ID + (''.join(segment_fields)) + segment_separator
174 def get_edi_type(self):
175 """
176 Extract type of EDI file
177 :return: str
178 """
179 # edi type is the first field in a ST transaction
180 return self._find_segment('ST')[0].DETAIL['ST01']
182 def get_ack_997_msg(self):
183 """
184 Extract segments that conform the response EDI 997
185 :return: list[Segment]
186 """
187 # 997 EDI segments
189 # ISA
190 isa = self._find_segment('ISA')
191 icn = isa[0].DETAIL['ISA13']
193 # extract sender ID
194 interchange_id_qualifier_sender = isa[0].DETAIL['ISA05']
195 interchange_sender_id = isa[0].DETAIL['ISA06']
196 # extract receiver ID
197 interchange_id_qualifier_receiver = isa[0].DETAIL['ISA07']
198 interchange_receiver_id = isa[0].DETAIL['ISA08']
200 # inverse 210 destinations
201 isa[0].DETAIL['ISA05'] = interchange_id_qualifier_receiver
202 isa[0].DETAIL['ISA06'] = interchange_receiver_id
203 isa[0].DETAIL['ISA07'] = interchange_id_qualifier_sender
204 isa[0].DETAIL['ISA08'] = interchange_sender_id
206 # GS
207 gs = self._find_segment('GS')
208 if len(gs) > 0:
209 gs[0].DETAIL['GS01'] = 'FA'
210 gs_number = gs[0].DETAIL['GS06']
211 gs = gs[0]
212 else:
213 gs = None
214 gs_number = '0'
216 # ST
217 st = self.Segment(SEGMENT_ID='ST', POSITION=2, DETAIL={'ST01': '997', 'ST02': icn})
219 # AK1
220 ak1 = self.Segment(SEGMENT_ID='AK1', POSITION=3, DETAIL={'AK101': 'IM', 'AK102': gs_number})
222 # AK9
223 number_of_transactions = str(len(self._find_segment('ST')))
224 ak9 = self.Segment(
225 SEGMENT_ID='AK9',
226 POSITION=3,
227 DETAIL={
228 'AK101': 'A',
229 'AK102': number_of_transactions,
230 'AK103': number_of_transactions,
231 'AK104': number_of_transactions
232 })
234 # SE
235 se = self.Segment(SEGMENT_ID='SE', POSITION=4, DETAIL={'SE01': '4', 'SE02': icn})
237 # GE
238 ge = self.Segment(SEGMENT_ID='GE', POSITION=4, DETAIL={'GE01': '1', 'GE02': icn})
240 # IEA
241 iea = self._find_segment('IEA')
243 list_ack_segments = [isa[0]]
244 if gs:
245 list_ack_segments.append(gs)
247 list_ack_segments.append(st)
248 list_ack_segments.append(ak1)
249 list_ack_segments.append(ak9)
250 list_ack_segments.append(se)
251 list_ack_segments.append(ge)
252 list_ack_segments.append(iea[0])
253 return list_ack_segments
255 @staticmethod
256 def object_to_edi(list_segments) -> str:
257 """
258 Transform an EDI Object to EDI text
259 :param list_segments:list[Segment]
260 :return: str
261 """
262 segments_format_edi = [SDCEDIObject.convert_segment_to_edi(segment) for segment in list_segments]
263 return ''.join(segments_format_edi)