cannect 1.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 (58) hide show
  1. cannect/__init__.py +30 -0
  2. cannect/api/__init__.py +0 -0
  3. cannect/config.py +114 -0
  4. cannect/core/__init__.py +0 -0
  5. cannect/core/ascet/__init__.py +3 -0
  6. cannect/core/ascet/amd.py +698 -0
  7. cannect/core/ascet/formula.py +33 -0
  8. cannect/core/ascet/oid.py +29 -0
  9. cannect/core/ascet/ws.py +154 -0
  10. cannect/core/can/__init__.py +2 -0
  11. cannect/core/can/ascet/__init__.py +3 -0
  12. cannect/core/can/ascet/_db2code.py +344 -0
  13. cannect/core/can/ascet/_db2elem.py +399 -0
  14. cannect/core/can/ascet/comdef.py +256 -0
  15. cannect/core/can/ascet/comrx.py +139 -0
  16. cannect/core/can/ascet/diag.py +691 -0
  17. cannect/core/can/db/__init__.py +4 -0
  18. cannect/core/can/db/reader.py +148 -0
  19. cannect/core/can/db/schema.py +269 -0
  20. cannect/core/can/db/specification/__init__.py +0 -0
  21. cannect/core/can/db/specification/message.py +230 -0
  22. cannect/core/can/db/specification/styles.py +81 -0
  23. cannect/core/can/db/specification/wrapper.py +161 -0
  24. cannect/core/can/db/vcs.py +104 -0
  25. cannect/core/can/rule.py +229 -0
  26. cannect/core/can/testcase/__init__.py +0 -0
  27. cannect/core/can/testcase/unitcase/__init__.py +0 -0
  28. cannect/core/can/testcase/unitcase/asw2can.py +48 -0
  29. cannect/core/can/testcase/unitcase/decode.py +63 -0
  30. cannect/core/can/testcase/unitcase/diagnosis.py +479 -0
  31. cannect/core/can/testcase/unitcase/encode.py +60 -0
  32. cannect/core/ir/__init__.py +2 -0
  33. cannect/core/ir/changehistory.py +310 -0
  34. cannect/core/ir/delivereables.py +71 -0
  35. cannect/core/ir/diff.py +97 -0
  36. cannect/core/ir/ir.py +581 -0
  37. cannect/core/ir/sdd.py +148 -0
  38. cannect/core/mdf.py +66 -0
  39. cannect/core/subversion.py +136 -0
  40. cannect/core/testcase/__init__.py +3 -0
  41. cannect/core/testcase/plotter.py +181 -0
  42. cannect/core/testcase/style.py +981 -0
  43. cannect/core/testcase/testcase.py +160 -0
  44. cannect/core/testcase/unitcase.py +227 -0
  45. cannect/errors.py +20 -0
  46. cannect/schema/__init__.py +5 -0
  47. cannect/schema/candb.py +226 -0
  48. cannect/schema/datadictionary.py +60 -0
  49. cannect/utils/__init__.py +3 -0
  50. cannect/utils/excel.py +29 -0
  51. cannect/utils/logger.py +81 -0
  52. cannect/utils/ppt.py +236 -0
  53. cannect/utils/tools.py +207 -0
  54. cannect-1.0.0.dist-info/METADATA +214 -0
  55. cannect-1.0.0.dist-info/RECORD +58 -0
  56. cannect-1.0.0.dist-info/WHEEL +5 -0
  57. cannect-1.0.0.dist-info/licenses/LICENSE +21 -0
  58. cannect-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+ import pprint, os
3
+
4
+
5
+
6
+ class DataDictionary(dict):
7
+ """
8
+ 데이터 저장 Dictionary
9
+ built-in: dict의 확장으로 저장 요소에 대해 attribute 접근 방식을 허용
10
+ 기본 제공 Alias (별칭): dD, dDict
11
+
12
+ 사용 예시)
13
+ myData = DataDictionary(name='JEHYEUK', age=34, division='Vehicle Solution Team')
14
+ print(myData.name, myData['name'], myData.name == myData['name'])
15
+
16
+ /* ----------------------------------------------------------------------------------------
17
+ | 결과
18
+ -------------------------------------------------------------------------------------------
19
+ | JEHYEUK JEHYEUK True
20
+ ---------------------------------------------------------------------------------------- */
21
+ """
22
+ def __init__(self, data=None, **kwargs):
23
+ super().__init__()
24
+
25
+ data = data or {}
26
+ data.update(kwargs)
27
+ for key, value in data.items():
28
+ if isinstance(value, dict):
29
+ self[key] = DataDictionary(**value)
30
+ continue
31
+ if isinstance(value, list):
32
+ self[key] = value
33
+ continue
34
+ try:
35
+ if os.path.isdir(value) or os.path.isfile(value):
36
+ value = Path(value)
37
+ except TypeError:
38
+ pass
39
+ self[key] = value
40
+ return
41
+
42
+ def __getattr__(self, attr):
43
+ if attr in self:
44
+ return self[attr]
45
+ return super().__getattribute__(attr)
46
+
47
+ def __setattr__(self, attr, value):
48
+ if isinstance(value, dict):
49
+ self[attr] = DataDictionary(**value)
50
+ else:
51
+ try:
52
+ if os.path.isdir(value) or os.path.isfile(value):
53
+ value = Path(value)
54
+ except TypeError:
55
+ pass
56
+ self[attr] = value
57
+ return
58
+
59
+ def __str__(self) -> str:
60
+ return pprint.pformat(self)
@@ -0,0 +1,3 @@
1
+ from .excel import ComExcel
2
+ from .logger import Logger
3
+ from cannect.utils import tools
cannect/utils/excel.py ADDED
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+ from pywintypes import com_error
3
+ from typing import Union
4
+ import win32com.client as win32
5
+
6
+
7
+ class ComExcel:
8
+
9
+ app = None
10
+ app_close :bool = False
11
+ app_visible:bool = False
12
+ def __new__(cls, *args, **kwargs):
13
+ try:
14
+ cls.app = win32.GetActiveObject("Excel.Application")
15
+ except com_error:
16
+ cls.app = win32.Dispatch("Excel.Application")
17
+ cls.app_close = True
18
+ cls.app_visible = False
19
+ return super().__new__(cls)
20
+
21
+ def __init__(self, path:Union[str, Path]):
22
+ self.wb = wb = self.app.Workbooks.Open(path)
23
+ self.ws = wb.ActiveSheet
24
+ return
25
+
26
+ # def __del__(self):
27
+ # if self.app_close:
28
+ # self.wb.Close()
29
+ # self.app.Quit()
@@ -0,0 +1,81 @@
1
+ from io import StringIO
2
+ import logging, time, os
3
+
4
+
5
+ class Logger(logging.Logger):
6
+
7
+ _timer = None
8
+
9
+ @classmethod
10
+ def kst(cls, *args):
11
+ return time.localtime(time.mktime(time.gmtime()) + 9 * 3600)
12
+
13
+ def __init__(self, file:str='', clean_record:bool=False):
14
+
15
+ formatter = logging.Formatter(
16
+ fmt=f"%(asctime)s %(message)s",
17
+ datefmt="%Y-%m-%d %H:%M:%S"
18
+ )
19
+ formatter.converter = self.kst
20
+
21
+ console_handler = logging.StreamHandler()
22
+ console_handler.setLevel(logging.INFO)
23
+ console_handler.setFormatter(formatter)
24
+
25
+ self._buffer = StringIO()
26
+ memory = logging.StreamHandler(stream=self._buffer)
27
+ memory.setLevel(logging.INFO)
28
+ memory.setFormatter(formatter)
29
+
30
+ super().__init__(name='pyems', level=logging.DEBUG)
31
+
32
+ self.file = file
33
+ self.propagate = False
34
+ self.addHandler(console_handler)
35
+ self.addHandler(memory)
36
+ if file:
37
+ if os.path.exists(file) and clean_record:
38
+ os.remove(file)
39
+ file_handler = logging.FileHandler(file, encoding="utf-8")
40
+ file_handler.setLevel(logging.DEBUG)
41
+ file_handler.setFormatter(formatter)
42
+ self.addHandler(file_handler)
43
+ self._held = ''
44
+ return
45
+
46
+ def __call__(self, io:str):
47
+ return self.info(io)
48
+
49
+ @property
50
+ def stream(self) -> str:
51
+ return self._buffer.getvalue()
52
+
53
+ def log(self, msg:str='', *args, **kwargs):
54
+ if self._held:
55
+ msg = self._held + msg
56
+ super().info(msg, *args, **kwargs)
57
+ self._held = ''
58
+ return
59
+
60
+ def read(self, file:str=''):
61
+ if not file:
62
+ file = self.file
63
+ with open(file, 'r', encoding="utf-8") as f:
64
+ return f.read()
65
+
66
+ def hold(self, msg:str):
67
+ self._held += msg
68
+
69
+ def run(self, context:str=''):
70
+ if context:
71
+ self.info(context)
72
+ self._timer = time.perf_counter()
73
+ return
74
+
75
+ def end(self, context:str=''):
76
+ try:
77
+ context = f'{context} {time.perf_counter() - self._timer:.2f}s'
78
+ self.info(context)
79
+ except (AttributeError, TypeError, Exception):
80
+ raise RuntimeError('Logger is not started. Please call .run() method first.')
81
+ return
cannect/utils/ppt.py ADDED
@@ -0,0 +1,236 @@
1
+ from pywintypes import com_error
2
+ from typing import Generator, List
3
+ import win32com.client as win32
4
+ import os
5
+
6
+
7
+ # def add_OLE(
8
+ # ppt_win32,
9
+ # obj:str,
10
+ # slide:int=1,
11
+ # left:int=475,
12
+ # top:int=192,
13
+ # width:int=100,
14
+ # height:int=100,
15
+ # close:bool=True,
16
+ # ):
17
+ # slide = ppt_win32.Slides.Item(slide)
18
+ #
19
+ # # 아이콘으로 표시 여부 / 링크 여부
20
+ # display_as_icon = True
21
+ # link = False # True면 링크, False면 프레젠테이션에 임베드
22
+ #
23
+ # # AddOLEObject (파일 확장자에 따라 ProgID 자동 판단; 명시 가능)
24
+ # shape = slide.Shapes.AddOLEObject(
25
+ # Left=left, Top=top, Width=width, Height=height,
26
+ # ClassName="", # 예: "Excel.Sheet.12" (Office 2007+)
27
+ # FileName=obj,
28
+ # DisplayAsIcon=display_as_icon,
29
+ # IconFileName=" ", # 아이콘 파일 지정 가능(생략 시 기본)
30
+ # IconIndex=0,
31
+ # # IconLabel=os.path.basename(obj),
32
+ # IconLabel='',
33
+ # Link=link
34
+ # )
35
+ #
36
+ # if close:
37
+ # ppt_win32.SaveAs(ppt_path)
38
+ # ppt_win32.close()
39
+ # return
40
+
41
+
42
+ class PptRW:
43
+
44
+ app = None
45
+ app_close:bool = False
46
+ def __new__(cls, *args, **kwargs):
47
+ try:
48
+ cls.app = win32.GetActiveObject("PowerPoint.Application")
49
+ except com_error:
50
+ cls.app = win32.Dispatch("PowerPoint.Application")
51
+ cls.app_close = True
52
+ cls.app.Visible = True
53
+ return super().__new__(cls)
54
+
55
+ def __init__(self, path:str, logger=None):
56
+ if logger is not None:
57
+ logger.log(f'[WRITE PPT ON "{os.path.basename(path)}"]')
58
+ if self.app.Presentations.Count > 0:
59
+ for n in range(1, self.app.Presentations.Count + 1):
60
+ _ppt = self.app.Presentations.Item(n)
61
+ if os.path.join(_ppt.Path, _ppt.Name) == path:
62
+ self.ppt = _ppt
63
+ return
64
+ self.ppt = self.app.Presentations.Open(path)
65
+ self.log = logger
66
+ return
67
+
68
+ def __iter__(self) -> Generator[win32.CDispatch, None, None]:
69
+ for i in range(1, self.ppt.Slides.Count + 1):
70
+ yield self.ppt.Slides.Item(i)
71
+
72
+ def _get_table(self, n_slide:int, n_table:int):
73
+ slide = self.ppt.Slides.Item(n_slide)
74
+ n = 0
75
+ for shape in slide.Shapes:
76
+ if shape.HasTable:
77
+ n += 1
78
+ if not n == n_table:
79
+ continue
80
+ return shape.Table
81
+ raise com_error(f'Table Not Found')
82
+
83
+ def get_slide_n(self, title:str) -> List:
84
+ ns = []
85
+ for n, slide in enumerate(self, start=1):
86
+ if slide.Shapes.Count == 0:
87
+ continue
88
+ for m in range(1, slide.Shapes.Count + 1):
89
+ shape = slide.Shapes.Item(1)
90
+ if not shape.HasTextFrame:
91
+ continue
92
+ text = shape.TextFrame.TextRange.Text
93
+ if (title.lower() in text.lower()) and (not n in ns):
94
+ ns.append(n)
95
+ return ns
96
+
97
+ def set_shape(self, n_slide:int, n_shape:int, width:float=None, height:float=None, left=None, top=None):
98
+ if width:
99
+ self.ppt.Slides.Item(n_slide).Shapes(n_shape).Width = width
100
+ if height:
101
+ self.ppt.Slides.Item(n_slide).Shapes(n_shape).Height = height
102
+ if left:
103
+ self.ppt.Slides.Item(n_slide).Shapes(n_shape).Left = left
104
+ if top:
105
+ self.ppt.Slides.Item(n_slide).Shapes(n_shape).Top = top
106
+ return
107
+
108
+ def set_table_height(self, n_slide:int, n_table:int, row:int, height:float):
109
+ self._get_table(n_slide, n_table).Rows(row).Height = height
110
+ return
111
+
112
+ def set_table_text_align(
113
+ self,
114
+ n_slide:int,
115
+ n_table:int,
116
+ cell:tuple,
117
+ horizontal:int=1,
118
+ vertical:int=1
119
+ ):
120
+ cell = self._get_table(n_slide, n_table).Cell(*cell)
121
+ # text_frame =
122
+ # text_range = text_frame.TextRange
123
+
124
+ cell.Shape.TextFrame.TextRange.ParagraphFormat.Alignment = horizontal
125
+ cell.Shape.TextFrame.VerticalAnchor = vertical
126
+ return
127
+
128
+ def set_table_font(
129
+ self,
130
+ n_slide:int,
131
+ n_table:int,
132
+ cell:tuple,
133
+ name:str=None,
134
+ size:int=None,
135
+ bold:bool=None,
136
+ color:str=None,
137
+ ):
138
+ font = self._get_table(n_slide, n_table).Cell(*cell).Shape.TextFrame.TextRange.Font
139
+ if name is not None:
140
+ font.Name = name
141
+ if size is not None:
142
+ font.Size = size
143
+ if bold is not None:
144
+ font.Bold = bold
145
+ if color is not None:
146
+ font.Color.RGB = color
147
+ return
148
+
149
+ def set_text(
150
+ self,
151
+ n_slide:int,
152
+ n_shape:int,
153
+ text:str,
154
+ pos:str='new',
155
+ ):
156
+ shape = self.ppt.Slides.Item(n_slide).Shapes(n_shape)
157
+ if shape.HasTextFrame:
158
+ if pos.lower() == 'after':
159
+ shape.TextFrame.TextRange.InsertAfter(text) # Error
160
+ elif pos.lower() == 'before':
161
+ shape.TextFrame.TextRange.InsertBefore(text)
162
+ else:
163
+ shape.TextFrame.TextRange.Text = text
164
+ return
165
+
166
+ def set_text_font(
167
+ self,
168
+ n_slide:int,
169
+ n_shape:int,
170
+ name:str=None,
171
+ size:int=None,
172
+ bold:bool=None,
173
+ color:str=None,
174
+ ):
175
+ font = self.ppt.Slides.Item(n_slide).Shapes(n_shape).TextFrame.TextRange.Font
176
+ if name is not None:
177
+ font.Name = name
178
+ if size is not None:
179
+ font.Size = size
180
+ if bold is not None:
181
+ font.Bold = bold
182
+ if color is not None:
183
+ font.Color.RGB = color
184
+ return
185
+
186
+ def set_text_in_table(
187
+ self,
188
+ n_slide:int,
189
+ n_table:int,
190
+ cell:tuple,
191
+ text:str,
192
+ pos:str='new'
193
+ ):
194
+ table = self._get_table(n_slide, n_table)
195
+ if pos.lower() == 'after':
196
+ table.Cell(cell[0], cell[1]).Shape.TextFrame.TextRange.InsertAfter(text)
197
+ elif pos.lower() == 'before':
198
+ table.Cell(cell[0], cell[1]).Shape.TextFrame.TextRange.InsertBefore(text)
199
+ else:
200
+ table.Cell(cell[0], cell[1]).Shape.TextFrame.TextRange.Text = text
201
+ return
202
+
203
+ def replace_text_in_table(
204
+ self,
205
+ n_slide:int,
206
+ n_table:int,
207
+ cell:tuple,
208
+ prev:str,
209
+ post:str
210
+ ):
211
+ self._get_table(n_slide, n_table) \
212
+ .Cell(cell[0], cell[1]) \
213
+ .Shape \
214
+ .TextFrame \
215
+ .TextRange \
216
+ .Replace(prev, post)
217
+ return
218
+
219
+ def close(self):
220
+ self.ppt.Save()
221
+ self.ppt.Close()
222
+ if self.app_close:
223
+ self.app.Quit()
224
+ return
225
+
226
+
227
+
228
+
229
+ if __name__ == "__main__":
230
+ # ppt = PptRW(r"D:\Archive\00_프로젝트\2017 통신개발-\2025\DS1229 CR10785896 CNG PIO\0000_변경내역서 양식.pptx")
231
+ # ppt.set_text(1, 1, "Hello World", insert=False)
232
+ # ppt.set_text_in_table(2, (2, 1), "Testing", insert=False)
233
+ # ppt.close()
234
+
235
+ print(win32.constants)
236
+ print(win32.constants.msoAnchorTop)
cannect/utils/tools.py ADDED
@@ -0,0 +1,207 @@
1
+ from pathlib import Path
2
+ from typing import AnyStr, Callable, Iterable, List, Union
3
+ from xml.etree.ElementTree import Element, ElementTree
4
+ from xml.dom import minidom
5
+ import os, zipfile, shutil, io, re
6
+
7
+
8
+ def unzip(src: Union[str, Path], to: Union[str, Path] = "") -> bool:
9
+ """
10
+ 압축(.zip) 해제
11
+ :param src: 압축파일 경로
12
+ :param to : [optional] 압축파일을 풀 경로
13
+ :return:
14
+ """
15
+ if not to:
16
+ to = os.path.dirname(src)
17
+ else:
18
+ os.makedirs(to, exist_ok=True)
19
+
20
+ src = str(src)
21
+ if not os.path.isfile(src):
22
+ raise KeyError(f"src: {src}는 경로가 포함된 파일(Full-Directory)이어야 합니다.")
23
+ if src.endswith('.zip'):
24
+ zip_obj = zipfile.ZipFile(src)
25
+ zip_obj.extractall(to)
26
+ # elif src.endswith('.7z'):
27
+ # with py7zr.SevenZipFile(src, 'r') as arc:
28
+ # arc.extractall(path=to)
29
+ else:
30
+ # raise KeyError(f"src: {src}는 .zip 또는 .7z 압축 파일만 입력할 수 있습니다.")
31
+ raise KeyError(f"src: {src}는 .zip 압축 파일만 입력할 수 있습니다.")
32
+ return True
33
+
34
+ def zip(path:Union[str, Path]):
35
+ name = os.path.basename(path)
36
+ shutil.make_archive(name, "zip", root_dir=path)
37
+ shutil.move(f'{name}.zip', path)
38
+ return
39
+
40
+ def copy_to(file:Union[str, Path], dst:Union[str, Path]) -> str:
41
+ shutil.copy(file, dst)
42
+ # if '.' in os.path.basename(file):
43
+ # shutil.copy(file, dst)
44
+ # else:
45
+ # shutil.move(file, dst)
46
+ return os.path.join(dst, os.path.basename(file))
47
+
48
+ def find_file(root:Union[str, Path], filename:Union[str, Path]) -> Union[str, List[str]]:
49
+ """
50
+ @filename: 확장자까지 포함한 단일 파일 이름
51
+ """
52
+ found = []
53
+ for _root, _dir, _files in os.walk(root):
54
+ for _file in _files:
55
+ if _file == filename:
56
+ found.append(os.path.join(_root, _file))
57
+ if not found:
58
+ return ""
59
+ if len(found) == 1:
60
+ return found[0]
61
+ return found
62
+
63
+
64
+ def clear(path: str, leave_path: bool = True):
65
+ """
66
+ 지정한 경로의 파일 및 하위 디렉토리를 모두 삭제
67
+
68
+ :param path: 삭제 대상이 될 폴더 경로
69
+ :param leave_path: True면 폴더를 남기고 내용만 삭제, False면 폴더 자체도 삭제
70
+ """
71
+ if not os.path.exists(path):
72
+ return
73
+
74
+ try:
75
+ if leave_path:
76
+ for item in os.listdir(path):
77
+ item_path = os.path.join(path, item)
78
+ if os.path.isfile(item_path) or os.path.islink(item_path):
79
+ os.unlink(item_path)
80
+ elif os.path.isdir(item_path):
81
+ shutil.rmtree(item_path)
82
+ else:
83
+ shutil.rmtree(path)
84
+ except Exception as e:
85
+ print(f"Error occurs while clearing directory: {e}")
86
+
87
+ def path_abbreviate(path: Union[str, Path]) -> str:
88
+ sep = os.path.sep
89
+ split = str(path).split(sep)
90
+ return f"{sep.join(split[:2])}{sep} ... {sep}{sep.join(split[-3:])}"
91
+
92
+
93
+ class xml:
94
+
95
+ @classmethod
96
+ def to_str(cls, xml: Union[Element, ElementTree], xml_declaration: bool = False) -> str:
97
+ """
98
+ xml 요소(태그) 및 그 하위 요소를 소스 문자열로 변환
99
+
100
+ :param xml:
101
+ :param xml_declaration:
102
+
103
+ :return:
104
+ """
105
+ if isinstance(xml, Element):
106
+ xml = ElementTree(xml)
107
+
108
+ stream = io.StringIO()
109
+ xml.write(
110
+ file_or_filename=stream,
111
+ encoding='unicode',
112
+ xml_declaration=False,
113
+ method='xml',
114
+ )
115
+ dom = f'{minidom.parseString(stream.getvalue()).toprettyxml()}' \
116
+ .replace('<?xml version="1.0" ?>', '<?xml version="1.0" encoding="UTF-8"?>') \
117
+ .replace("<CodeBlock/>", "<CodeBlock></CodeBlock>") \
118
+ .replace("<Comment/>", "<Comment></Comment>") \
119
+ .replace("ns0:", "") \
120
+ .replace('xmlns:ns0="http://www.w3.org/2000/09/xmldsig#" ', '') \
121
+ .replace('<Signature>', '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">')
122
+ # if not xml.getroot().tag == 'Specifications':
123
+ dom = "\n".join([l for l in dom.split("\n") if "<" in l or ';' in l or '=' in l or not l.startswith("\t")]) # dom = '\n'.join([line for line in dom.split('\n') if line.strip()])
124
+ if not xml_declaration:
125
+ dom = dom.replace('<?xml version="1.0" encoding="UTF-8"?>\n', '')
126
+ return dom
127
+
128
+ @classmethod
129
+ def to_dict(cls, xml:Union[Element, ElementTree], target:str='ascet', depth:str='recursive') -> dict:
130
+ """
131
+ xml 요소(태그) 및 그 하위 요소의 text 및 attribute를 dictionary로 변환
132
+
133
+ :param xml:
134
+ :param target:
135
+ :param depth:
136
+ :return:
137
+ """
138
+ if isinstance(xml, Element):
139
+ xml = ElementTree(xml)
140
+
141
+ if not depth == 'recursive':
142
+ return xml.getroot().attrib
143
+
144
+ attr = {}
145
+ for elem in xml.iter():
146
+ copy = elem.attrib.copy()
147
+ if target == "ascet":
148
+ if elem.tag == 'PhysicalInterval' and 'min' in copy:
149
+ copy['physMin'] = copy['min']
150
+ copy['physMax'] = copy['max']
151
+ del copy['min'], copy['max']
152
+ if elem.tag == 'ImplementationInterval' and 'min' in copy:
153
+ copy['implMin'] = copy['min']
154
+ copy['implMax'] = copy['max']
155
+ del copy['min'], copy['max']
156
+
157
+ if (elem.tag == 'Comment' and elem.text is not None) or elem.tag == 'CodeBlock':
158
+ attr[elem.tag.lower()] = elem.text
159
+
160
+ attr.update(copy)
161
+ return attr
162
+
163
+
164
+
165
+ class KeywordSearch:
166
+ """
167
+ Dataset에 대한 키워드 검색
168
+ """
169
+ logger:Callable = print
170
+ def __init__(self, *samples):
171
+ self._samples = list(samples)
172
+ return
173
+
174
+ def __getitem__(self, item:str)-> str:
175
+ result = self.search(self._samples, item)
176
+ if not result:
177
+ self.logger(f"NOT FOUND: '{item}' IN THE MODEL")
178
+ return result
179
+
180
+ @classmethod
181
+ def search(cls, samples: Iterable[str], keyword: str) -> Union[str, List[str]]:
182
+ """
183
+ 간단한 와일드카드 검색 함수.
184
+ - '*' 는 0글자 이상 임의의 문자열과 매칭됩니다.
185
+ - 대소문자를 구분하지 않습니다.
186
+ - 결과가 1개이면 str을, 그 외(0개 또는 2개 이상)면 List[str]를 반환합니다.
187
+ """
188
+ samples = list(samples)
189
+ if keyword is None:
190
+ return ""
191
+
192
+ kw = keyword.strip()
193
+ regex_fragments = []
194
+ for ch in kw:
195
+ if ch == '*':
196
+ regex_fragments.append(".*")
197
+ else:
198
+ regex_fragments.append(re.escape(ch))
199
+ regex_body = "".join(regex_fragments)
200
+ pattern = re.compile(f"^{regex_body}$", flags=re.IGNORECASE)
201
+ matches = [s for s in samples if pattern.match(s)]
202
+ if not matches:
203
+ return ""
204
+ elif len(matches) == 1:
205
+ return matches[0]
206
+ else:
207
+ return matches