datacom-device-api 0.1.5__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.
File without changes
File without changes
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Generic, TypeVarTuple, Unpack
3
+
4
+ Ts = TypeVarTuple('Ts')
5
+
6
+ @dataclass
7
+ class Table(Generic[Unpack[Ts]]):
8
+ table_name: str
9
+ headers: list[str]
10
+ fragment_refrence_size: int
11
+ total_fragment: int
12
+ total_row: int
13
+ fragments: list[TableFragment[Unpack[Ts]]]
14
+ def __post_init__(self):
15
+ for fragment in self.fragments:
16
+ if fragment.fragment_offset < 0 or fragment.fragment_size <= 0:
17
+ raise ValueError('Invalid fragment offset or size')
18
+ if len(fragment.rows) > fragment.fragment_size:
19
+ raise ValueError('Invalid fragment size or row count')
20
+
21
+ @dataclass
22
+ class TableFragment(Generic[Unpack[Ts]]):
23
+ fragment_offset: int
24
+ fragment_size: int
25
+ rows: list[tuple[Unpack[Ts]]]
File without changes
@@ -0,0 +1,89 @@
1
+
2
+ from typing import Any, Callable, Generic, Optional, TypeVar, TypeVarTuple, Union, Unpack, overload
3
+
4
+ from datacom_device_api.data.table import Table, TableFragment
5
+
6
+ Ts = TypeVarTuple('Ts')
7
+
8
+ class TableOperation(Generic[Unpack[Ts]]):
9
+ def __init__(
10
+ self,
11
+ table_name: str,
12
+ headers: list[str],
13
+ fragment_refrence_size: int = 1000
14
+ ) -> None:
15
+ self.__table = Table[Unpack[Ts]](
16
+ table_name=table_name,
17
+ headers=headers,
18
+ fragment_refrence_size=fragment_refrence_size,
19
+ total_fragment=0,
20
+ total_row=0,
21
+ fragments=[],
22
+ )
23
+
24
+ def add_fragment(self, fragment: TableFragment[Unpack[Ts]]) -> None:
25
+ if fragment.fragment_size > self.__table.fragment_refrence_size:
26
+ raise ValueError('fragment size is too large')
27
+
28
+ self.__table.fragments.append(fragment)
29
+ self.__table.total_fragment += 1
30
+ self.__table.total_row += len(fragment.rows)
31
+
32
+ @property
33
+ def fragment_refrence_size(self) -> int:
34
+ return self.__table.fragment_refrence_size
35
+
36
+ def get_row_by_row_index(self, row_index: int) -> Optional[tuple[Unpack[Ts]]]:
37
+ if row_index >= self.__table.total_row:
38
+ raise ValueError('row index is out of range')
39
+
40
+ self.__table.fragments.sort(key=lambda fragment: fragment.fragment_offset)
41
+
42
+ fragment_index = row_index // self.__table.fragment_refrence_size
43
+ fragment = self.__table.fragments[fragment_index]
44
+
45
+ return fragment.rows[row_index % fragment.fragment_size]
46
+
47
+ @overload
48
+ def get_row_by_header(self, header: str, value: Any) -> list[tuple[*Ts]]: ...
49
+ @overload
50
+ def get_row_by_header(self, header: int, value: Any) -> list[tuple[*Ts]]: ...
51
+
52
+ def get_row_by_header(self, header: Any, value: Any) -> list[tuple[*Ts]]:
53
+ if isinstance(header, int):
54
+ header_index = header
55
+ elif isinstance(header, str):
56
+ if header not in self.__table.headers:
57
+ raise ValueError('header name not found')
58
+ header_index = self.__table.headers.index(header)
59
+ else:
60
+ raise ValueError('header must be int or str')
61
+
62
+ if type(value) not in [*Ts]:
63
+ raise ValueError('value type of value')
64
+
65
+ result = []
66
+ for fragment in self.__table.fragments:
67
+ for row in fragment.rows:
68
+ if row[header_index] == value:
69
+ result.append(row)
70
+
71
+ return result
72
+
73
+ @property
74
+ def table(self) -> Table[Unpack[Ts]]:
75
+ return self.__table
76
+
77
+ @property
78
+ def headers(self) -> list[str]:
79
+ return self.__table.headers
80
+
81
+ def for_each_row_by_fragment_index(self, fragment_index: int, callback: Callable[[tuple[*Ts]], None]):
82
+ fragment = self.__table.fragments[fragment_index]
83
+ for row in fragment.rows:
84
+ callback(row)
85
+
86
+ def for_each_all(self, callback: Callable[[tuple[*Ts]], None]):
87
+ for frament in self.__table.fragments:
88
+ for row in frament.rows:
89
+ callback(row)
@@ -0,0 +1,103 @@
1
+ from typing import Any, Callable, Generic, TypeVarTuple
2
+
3
+ from openpyxl import Workbook, load_workbook
4
+
5
+ from datacom_device_api.data.table import Table, TableFragment
6
+ from datacom_device_api.operation.table_operation import TableOperation
7
+
8
+
9
+ Ts = TypeVarTuple('Ts')
10
+
11
+ class TableReadFromXlsx(Generic[*Ts]):
12
+ def __init__(self, xlsx_file_path: str) -> None:
13
+ try:
14
+ self.__workbook = load_workbook(xlsx_file_path, read_only=True)
15
+ except FileNotFoundError:
16
+ raise FileNotFoundError(f"{xlsx_file_path} is not found.")
17
+ except Exception as e:
18
+ raise e
19
+
20
+ @property
21
+ def get_sheet_names(self) -> list[str]:
22
+ return self.__workbook.sheetnames
23
+
24
+
25
+ def _generate_table_fragment(self, worksheet, min_row: int, max_row: int, convert_func: Callable[[str, Any], Any], headers: list[str]) -> TableFragment[*Ts]:
26
+ if not hasattr(worksheet, 'iter_rows'):
27
+ raise AttributeError("worksheet does not have iter_rows method.")
28
+
29
+ row_datas: list[tuple] = []
30
+ for row_data in worksheet.iter_rows(min_row=min_row, max_row=max_row):
31
+ tmp = tuple(convert_func(headers[i], row_data[i].value ) for i in range(len(headers)))
32
+ print(tmp)
33
+ row_datas.append(tmp)
34
+
35
+ if min_row == 1 or min_row == 2:
36
+ fragment_offset = 0
37
+ else:
38
+ fragment_offset = min_row - 1
39
+
40
+ return TableFragment[*Ts](
41
+ rows=row_datas,
42
+ fragment_offset=fragment_offset,
43
+ fragment_size=len(row_datas),
44
+ )
45
+
46
+ def generate_table_data(self, sheet_name: str, convert_func: Callable[[str, Any], Any]) -> Table[*Ts]:
47
+ if sheet_name not in self.__workbook.sheetnames:
48
+ raise ValueError(f"{sheet_name} is not found.")
49
+
50
+ worksheet = self.__workbook[sheet_name]
51
+ total_rows = worksheet.max_row - 1
52
+
53
+ headers = []
54
+ for row_data in worksheet.iter_rows(min_row=1, max_row=1):
55
+ headers.extend([item.value for item in row_data])
56
+
57
+ table = TableOperation[*Ts](
58
+ table_name=sheet_name,
59
+ headers=headers,
60
+ )
61
+
62
+ fragments_count = -(-total_rows // table.fragment_refrence_size)
63
+ fragments_size = []
64
+
65
+ remaining_count = total_rows
66
+ for _ in range(fragments_count):
67
+ remaining_count -= (table.fragment_refrence_size)
68
+ if remaining_count < 0:
69
+ fragments_size.append(remaining_count + table.fragment_refrence_size)
70
+ else:
71
+ fragments_size.append(table.fragment_refrence_size)
72
+
73
+ offset = 2
74
+ for size in fragments_size:
75
+ fragment = self._generate_table_fragment(
76
+ worksheet=worksheet,
77
+ min_row=offset,
78
+ max_row=offset + size - 1,
79
+ convert_func=convert_func,
80
+ headers=headers,
81
+ )
82
+
83
+ table.add_fragment(fragment)
84
+ offset += size
85
+
86
+ return table.table
87
+
88
+ def _convert_func(header: str, value: Any) -> Any:
89
+ if header == 'USER_NAME':
90
+ return str(value)
91
+ return value
92
+
93
+ if __name__ == "__main__":
94
+ xlsx_file = "C:\\Users\\24168\\Projects\\Datacom_tools\\tables\\test.xlsx"
95
+ sheet_name = "Sheet1"
96
+ xlsx = TableReadFromXlsx(xlsx_file)
97
+ xlsx.generate_table_data(sheet_name, _convert_func)
98
+
99
+ # workbook = load_workbook(xlsx_file, read_only=True)
100
+ # sheet = workbook[sheet_name]
101
+
102
+ # for row_data in sheet.iter_rows(min_row=1, max_row=10):
103
+ # print(row_data)
@@ -0,0 +1,20 @@
1
+ from typing import Generic, TypeVarTuple, Unpack
2
+ from pymysql.cursors import Cursor
3
+ from pymysql.connections import Connection
4
+ from datacom_device_api.data.table import Table
5
+
6
+ Ts = TypeVarTuple('Ts')
7
+
8
+ class TableSaveToDB(Generic[Unpack[Ts]]):
9
+ def __init__(self, connect: Connection) -> None:
10
+ self.__cursor = connect.cursor()
11
+ self.__conn = connect
12
+ def save_to_db(self, db_table_name: str, table: Table[Unpack[Ts]]) -> None:
13
+ sql = f'INSERT INTO {db_table_name} VALUES ({",".join(["%s"] * len(table.headers))})'
14
+
15
+ for fragment in table.fragments:
16
+ values_list = fragment.rows
17
+ self.__cursor.executemany(sql, values_list)
18
+ self.__conn.commit()
19
+ print(table)
20
+
File without changes
@@ -0,0 +1,244 @@
1
+ """多进程任务执行与进度展示封装。
2
+
3
+ 提供 TerminalProcess 类,封装 ProcessPoolExecutor 和 Rich Progress,
4
+ 支持任务提交、进度监听、异常处理、上下文管理器等能力。
5
+ """
6
+
7
+ from concurrent.futures import Future, ProcessPoolExecutor
8
+ from dataclasses import dataclass
9
+ from functools import partial
10
+ from multiprocessing import Manager
11
+ import os
12
+ import queue
13
+ import threading
14
+ from typing import Any, Callable, Optional
15
+ from uuid import UUID
16
+ import uuid
17
+
18
+ from rich.progress import Progress, TaskID
19
+
20
+ REPORT_FUNC_TYPE = Callable[[float, float, str], None]
21
+ EXECUTE_FUNC_TYPE = Callable[[REPORT_FUNC_TYPE], Any]
22
+
23
+
24
+ @dataclass
25
+ class ProgressMessage:
26
+ """进程间传递的进度消息。
27
+
28
+ Attributes:
29
+ type: 消息类型,取值为 'progress' / 'done' / 'error'
30
+ task_id: 任务唯一标识
31
+ message: 进度描述文本
32
+ progress: 当前进度值
33
+ total: 总进度值
34
+ """
35
+
36
+ type: str
37
+ task_id: UUID
38
+ message: str
39
+ progress: float
40
+ total: float
41
+
42
+
43
+ __PROGRESS_UPDATE_FN_TYPE = Callable[[ProgressMessage], None]
44
+
45
+
46
+ class TerminalProcess:
47
+ """多进程任务执行器,支持进度展示与监听器机制。
48
+
49
+ 每个实例拥有独立的进程池、进度队列和进度条,
50
+ 多实例之间状态互不干扰。
51
+
52
+ 用法:
53
+ with TerminalProcess() as process:
54
+ process.submit(task1)
55
+ process.submit(task2)
56
+ """
57
+
58
+ def __init__(self, max_workers: Optional[int] = None):
59
+ """初始化终端进程执行器。
60
+
61
+ Args:
62
+ max_workers: 进程池最大并发数,默认为 CPU 核心数
63
+ """
64
+ self.__pool = ProcessPoolExecutor(max_workers=max_workers or os.cpu_count())
65
+ self.__manager = Manager()
66
+ self.__progress_queue: queue.Queue[ProgressMessage] = self.__manager.Queue()
67
+
68
+ self.__task_future_count = 0
69
+ self.__tasks: dict[UUID, Future[Any]] = {}
70
+ self.__task_update_lisening_fn_dict: dict[UUID, list[__PROGRESS_UPDATE_FN_TYPE]] = {}
71
+
72
+ self.__progress_thread_stop_event = threading.Event()
73
+ self.__progress_tasks: dict[UUID, TaskID] = {}
74
+ self.__progress = Progress()
75
+ self.__progress_thread: Optional[threading.Thread] = None
76
+ self.__progress_main_task: TaskID = self.__progress.add_task(
77
+ 'main', total=self.__task_future_count
78
+ )
79
+
80
+ def trigger_progress(self, task_id: UUID, message: ProgressMessage) -> None:
81
+ """触发指定任务的所有进度监听器。
82
+
83
+ 若 task_id 未注册监听器则静默跳过。
84
+ """
85
+ listening_fns = self.__task_update_lisening_fn_dict.get(task_id, [])
86
+ for fn in listening_fns:
87
+ fn(message)
88
+
89
+ def listen_progress(self, task_id: UUID, fn: __PROGRESS_UPDATE_FN_TYPE) -> None:
90
+ """为指定任务注册一个进度监听器。
91
+
92
+ 同一任务可注册多个监听器,按注册顺序依次调用。
93
+ """
94
+ self.__task_update_lisening_fn_dict.setdefault(task_id, []).append(fn)
95
+
96
+ def update_progress(self, message: ProgressMessage) -> None:
97
+ """更新内置进度条的显示。
98
+
99
+ 任务完成时自动推进主进度条计数。
100
+ 若消息对应的任务不存在则静默跳过。
101
+ """
102
+ task_id = self.__progress_tasks.get(message.task_id)
103
+ if task_id is None:
104
+ return
105
+ self.__progress.update(task_id, completed=message.progress, total=message.total)
106
+ self.__progress.refresh()
107
+
108
+ if message.type == 'done':
109
+ self.__progress.update(self.__progress_main_task, advance=1)
110
+
111
+ @staticmethod
112
+ def _warrper_fn(
113
+ fn: EXECUTE_FUNC_TYPE,
114
+ progress_queue: queue.Queue[ProgressMessage],
115
+ task_id: UUID,
116
+ *args,
117
+ **kwargs,
118
+ ) -> Any:
119
+ """任务包装函数,在子进程中执行。
120
+
121
+ 封装进度上报、完成标记和异常处理,通过队列将进度消息
122
+ 发送回主进程。任务正常结束或异常均会发送对应类型的消息。
123
+ """
124
+ done_send = False
125
+
126
+ def report(progress: float, total: float, message: str, msg_type: str = 'progress'):
127
+ nonlocal done_send
128
+
129
+ if done_send:
130
+ return
131
+
132
+ current_type = msg_type
133
+ if progress >= total:
134
+ current_type = 'done'
135
+ done_send = True
136
+
137
+ progress_queue.put(ProgressMessage(
138
+ type=current_type,
139
+ task_id=task_id,
140
+ message=message,
141
+ progress=progress,
142
+ total=total,
143
+ ))
144
+
145
+ try:
146
+ result = fn(report, *args, **kwargs)
147
+ if not done_send:
148
+ report(100, 100, 'done', msg_type='done')
149
+ return result
150
+ except Exception as e:
151
+ progress_queue.put(ProgressMessage(
152
+ type='error',
153
+ task_id=task_id,
154
+ message=str(e),
155
+ progress=0,
156
+ total=0,
157
+ ))
158
+ raise
159
+
160
+ def run_display_progress(self) -> None:
161
+ """进度消费线程主循环。
162
+
163
+ 持续从队列中取出进度消息,依次更新内置进度条并触发监听器。
164
+ 使用带超时的轮询避免阻塞,确保停止事件触发后能及时退出。
165
+ """
166
+ while not self.__progress_thread_stop_event.is_set() or not self.__progress_queue.empty():
167
+ try:
168
+ message = self.__progress_queue.get(timeout=0.1)
169
+ self.update_progress(message)
170
+ self.trigger_progress(message.task_id, message)
171
+ except queue.Empty:
172
+ if (
173
+ self.__progress_thread_stop_event.is_set()
174
+ and all(futrue.done() for futrue in self.__tasks.values())
175
+ ):
176
+ break
177
+
178
+ def submit(
179
+ self,
180
+ fn: EXECUTE_FUNC_TYPE,
181
+ task_name: Optional[str] = None,
182
+ *args,
183
+ **kwargs,
184
+ ) -> Optional[Future[Any]]:
185
+ """提交一个任务到进程池执行。
186
+
187
+ Args:
188
+ fn: 任务函数,第一个参数为 report 回调,签名为 (progress, total, message)
189
+ task_name: 进度条显示的任务名,默认为 task_{uuid}
190
+ *args: 传递给任务函数的位置参数
191
+ **kwargs: 传递给任务函数的关键字参数
192
+
193
+ Returns:
194
+ 任务的 Future 对象,可用于获取结果或异常
195
+ """
196
+ task_id = uuid.uuid4()
197
+ task_fn = partial(TerminalProcess._warrper_fn, fn, self.__progress_queue, task_id)
198
+ future = self.__pool.submit(task_fn, *args, **kwargs)
199
+ self.__tasks[task_id] = future
200
+ self.__task_update_lisening_fn_dict[task_id] = []
201
+
202
+ self.__task_future_count += 1
203
+ progress_task = self.__progress.add_task(task_name or f'task_{task_id}', total=1)
204
+ self.__progress_tasks[task_id] = progress_task
205
+ self.__progress.refresh()
206
+
207
+ self.__progress.update(self.__progress_main_task, total=self.__task_future_count)
208
+ return future
209
+
210
+ def start(self) -> None:
211
+ """启动进度条显示与消息消费线程。"""
212
+ self.__progress.start()
213
+ self.__progress_thread_stop_event.clear()
214
+ self.__progress_thread = threading.Thread(target=self.run_display_progress)
215
+ self.__progress_thread.start()
216
+
217
+ def shutdown(self) -> None:
218
+ """停止任务执行并释放资源。
219
+
220
+ 按顺序执行:等待所有任务完成 → 消费完剩余进度消息 →
221
+ 关闭进程池 → 停止进度条。确保不会因消息未消费完而丢失进度。
222
+ """
223
+ for future in self.__tasks.values():
224
+ try:
225
+ future.result()
226
+ except Exception:
227
+ pass
228
+
229
+ self.__progress_thread_stop_event.set()
230
+ if self.__progress_thread is not None:
231
+ self.__progress_thread.join()
232
+ self.__progress_thread = None
233
+
234
+ self.__pool.shutdown(wait=True)
235
+ self.__progress.stop()
236
+
237
+ def __enter__(self) -> 'TerminalProcess':
238
+ """进入上下文管理器时自动启动。"""
239
+ self.start()
240
+ return self
241
+
242
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
243
+ """退出上下文管理器时自动释放资源。"""
244
+ self.shutdown()
File without changes
@@ -0,0 +1,13 @@
1
+ import os
2
+
3
+ def get_project_root_dir() -> str:
4
+ return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+ def get_work_dir() -> str:
6
+ return os.getcwd()
7
+
8
+ def test():
9
+ print(get_project_root_dir())
10
+ print(get_work_dir())
11
+
12
+ if __name__ == '__main__':
13
+ test()
@@ -0,0 +1,14 @@
1
+ import logging
2
+ class Logger(object):
3
+ def __init__(self, name: str) -> None:
4
+ self.__logger = logging.getLogger(name)
5
+ self.__logger.setLevel(logging.INFO)
6
+ formatter = logging.Formatter('[%(asctime)s-%(levelname)s](%(name)s): %(message)s')
7
+
8
+ consolo_handler = logging.StreamHandler()
9
+ consolo_handler.setLevel(logging.INFO)
10
+ consolo_handler.setFormatter(formatter)
11
+ self.__logger.addHandler(consolo_handler)
12
+
13
+ def __getattr__(self, name):
14
+ return getattr(self.__logger, name)
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: datacom-device-api
3
+ Version: 0.1.5
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: dataclasses>=0.8
8
+ Requires-Dist: openpyxl>=3.1.5
9
+ Requires-Dist: ops>=3.8.0
10
+ Requires-Dist: pymysql>=1.2.0
11
+ Requires-Dist: rich>=15.0.0
@@ -0,0 +1,16 @@
1
+ datacom_device_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ datacom_device_api/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ datacom_device_api/data/table.py,sha256=tl77fUGrP6sRlSZGA4M9fB98O1Ld2udPQqMM8slHCqg,827
4
+ datacom_device_api/operation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ datacom_device_api/operation/table_operation.py,sha256=8G9BBytGA6lv4sfBzykuJtIhp9Ejl4I6RXaJVfUyVc0,3295
6
+ datacom_device_api/operation/table_read_from_xlsx.py,sha256=6x7lihDIEfTzbhQaEZ0qbY7IVOPgfd4dSPPTXeWwsz8,3746
7
+ datacom_device_api/operation/table_save_to_db.py,sha256=PkPWnLfyJRfctEbYIQHAVSo4DSbcS6B4jf0IH9VuuHo,768
8
+ datacom_device_api/process/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ datacom_device_api/process/terminal_process.py,sha256=ODqi-4W-NLjvHxGyCv_-VceJ9t0QnMBgLlJKEtPDv9Y,8407
10
+ datacom_device_api/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ datacom_device_api/utils/directory_util.py,sha256=HCzG73yBoeLDv5dXfKBI6T-TeWH6fjLLSf0SWNfOqJg,307
12
+ datacom_device_api/utils/logger.py,sha256=iyQQTHyhbf_uuNJw95FWD8hy_vVqbzeeNEpZskh1fpI,559
13
+ datacom_device_api-0.1.5.dist-info/METADATA,sha256=jCt2F06zteypOYMCQkAmKRW440H0h70OyOy_UIzjFoU,319
14
+ datacom_device_api-0.1.5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
15
+ datacom_device_api-0.1.5.dist-info/top_level.txt,sha256=KeCYNpjqBXcR2uDjjoZc4uFUuswkUoxnfn-UTGPSfmI,19
16
+ datacom_device_api-0.1.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ datacom_device_api