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.
- datacom_device_api/__init__.py +0 -0
- datacom_device_api/data/__init__.py +0 -0
- datacom_device_api/data/table.py +25 -0
- datacom_device_api/operation/__init__.py +0 -0
- datacom_device_api/operation/table_operation.py +89 -0
- datacom_device_api/operation/table_read_from_xlsx.py +103 -0
- datacom_device_api/operation/table_save_to_db.py +20 -0
- datacom_device_api/process/__init__.py +0 -0
- datacom_device_api/process/terminal_process.py +244 -0
- datacom_device_api/utils/__init__.py +0 -0
- datacom_device_api/utils/directory_util.py +13 -0
- datacom_device_api/utils/logger.py +14 -0
- datacom_device_api-0.1.5.dist-info/METADATA +11 -0
- datacom_device_api-0.1.5.dist-info/RECORD +16 -0
- datacom_device_api-0.1.5.dist-info/WHEEL +5 -0
- datacom_device_api-0.1.5.dist-info/top_level.txt +1 -0
|
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 @@
|
|
|
1
|
+
datacom_device_api
|