oafuncs 0.0.97.12__tar.gz → 0.0.97.14__tar.gz
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.
- {oafuncs-0.0.97.12/oafuncs.egg-info → oafuncs-0.0.97.14}/PKG-INFO +1 -1
- oafuncs-0.0.97.14/oafuncs/_script/cprogressbar.py +443 -0
- {oafuncs-0.0.97.12/oafuncs/oa_tool → oafuncs-0.0.97.14/oafuncs/_script}/email.py +26 -24
- oafuncs-0.0.97.14/oafuncs/_script/netcdf_merge.py +107 -0
- {oafuncs-0.0.97.12/oafuncs/oa_tool → oafuncs-0.0.97.14/oafuncs/_script}/parallel.py +5 -3
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_data.py +3 -24
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_date.py +24 -2
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/hycom_3hourly.py +39 -25
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/idm.py +5 -3
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_draw.py +8 -2
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_nc.py +35 -38
- oafuncs-0.0.97.14/oafuncs/oa_tool.py +83 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14/oafuncs.egg-info}/PKG-INFO +1 -1
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs.egg-info/SOURCES.txt +4 -5
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/setup.py +1 -1
- oafuncs-0.0.97.12/oafuncs/_script/auto_optimized_parallel_executor.py +0 -459
- oafuncs-0.0.97.12/oafuncs/_script/cprogressbar.py +0 -150
- oafuncs-0.0.97.12/oafuncs/_script/netcdf_merge.py +0 -354
- oafuncs-0.0.97.12/oafuncs/oa_tool/__init__.py +0 -7
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/LICENSE.txt +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/MANIFEST.in +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/README.md +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/__init__.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/_data/OAFuncs.png +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/_data/hycom_3hourly.png +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/_script/parallel_example_usage.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/_script/plot_dataset.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/_script/replace_file_concent.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_cmap.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/User_Agent-list.txt +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/__init__.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/literature.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/test_ua.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_down/user_agent.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_file.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_help.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_model/__init__.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_model/roms/__init__.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_model/roms/test.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_model/wrf/__init__.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_model/wrf/little_r.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_python.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_sign/__init__.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_sign/meteorological.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_sign/ocean.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs/oa_sign/scientific.py +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs.egg-info/dependency_links.txt +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs.egg-info/requires.txt +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/oafuncs.egg-info/top_level.txt +0 -0
- {oafuncs-0.0.97.12 → oafuncs-0.0.97.14}/setup.cfg +0 -0
@@ -0,0 +1,443 @@
|
|
1
|
+
import random
|
2
|
+
import re
|
3
|
+
import shutil
|
4
|
+
import sys
|
5
|
+
import threading
|
6
|
+
import time
|
7
|
+
import warnings
|
8
|
+
from collections import deque
|
9
|
+
from typing import Any, Callable, Deque, Iterable, List, NamedTuple, Optional, Union
|
10
|
+
|
11
|
+
import numpy as np
|
12
|
+
from oafuncs.oa_cmap import get as get_cmap
|
13
|
+
|
14
|
+
try:
|
15
|
+
# import matplotlib
|
16
|
+
from matplotlib.colors import LinearSegmentedColormap, to_hex, to_rgb
|
17
|
+
except ImportError:
|
18
|
+
raise ImportError("This module requires matplotlib. Install with: pip install matplotlib")
|
19
|
+
|
20
|
+
|
21
|
+
class ProgressSample(NamedTuple):
|
22
|
+
"""进度采样点"""
|
23
|
+
|
24
|
+
timestamp: float
|
25
|
+
"""采样时间戳"""
|
26
|
+
completed: float
|
27
|
+
"""已完成步数"""
|
28
|
+
|
29
|
+
|
30
|
+
class Task:
|
31
|
+
"""进度任务信息类"""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
description: str,
|
36
|
+
total: Optional[float],
|
37
|
+
completed: float = 0,
|
38
|
+
visible: bool = True,
|
39
|
+
color: Any = "cyan",
|
40
|
+
get_time: Callable[[], float] = time.time,
|
41
|
+
):
|
42
|
+
self.description = description
|
43
|
+
self.total = total
|
44
|
+
self.completed = completed
|
45
|
+
self.visible = visible
|
46
|
+
self.color = color
|
47
|
+
self._get_time = get_time
|
48
|
+
|
49
|
+
self.start_time: Optional[float] = None
|
50
|
+
self.stop_time: Optional[float] = None
|
51
|
+
self.finished_time: Optional[float] = None
|
52
|
+
self.finished_speed: Optional[float] = None
|
53
|
+
self._progress: Deque[ProgressSample] = deque(maxlen=1000)
|
54
|
+
|
55
|
+
def get_time(self) -> float:
|
56
|
+
"""获取当前时间"""
|
57
|
+
return self._get_time()
|
58
|
+
|
59
|
+
def start(self) -> None:
|
60
|
+
"""开始任务"""
|
61
|
+
if self.start_time is None:
|
62
|
+
self.start_time = self.get_time()
|
63
|
+
|
64
|
+
def stop(self) -> None:
|
65
|
+
"""停止任务"""
|
66
|
+
if self.stop_time is None:
|
67
|
+
self.stop_time = self.get_time()
|
68
|
+
|
69
|
+
@property
|
70
|
+
def started(self) -> bool:
|
71
|
+
"""任务是否已开始"""
|
72
|
+
return self.start_time is not None
|
73
|
+
|
74
|
+
@property
|
75
|
+
def finished(self) -> bool:
|
76
|
+
"""任务是否已完成"""
|
77
|
+
return self.finished_time is not None
|
78
|
+
|
79
|
+
@property
|
80
|
+
def percentage(self) -> float:
|
81
|
+
"""完成百分比"""
|
82
|
+
if not self.total:
|
83
|
+
return 0.0
|
84
|
+
completed = (self.completed / self.total) * 100.0
|
85
|
+
return min(100.0, max(0.0, completed))
|
86
|
+
|
87
|
+
@property
|
88
|
+
def elapsed(self) -> Optional[float]:
|
89
|
+
"""已用时间"""
|
90
|
+
if self.start_time is None:
|
91
|
+
return None
|
92
|
+
if self.stop_time is not None:
|
93
|
+
return self.stop_time - self.start_time
|
94
|
+
return self.get_time() - self.start_time
|
95
|
+
|
96
|
+
@property
|
97
|
+
def remaining(self) -> Optional[float]:
|
98
|
+
"""剩余步数"""
|
99
|
+
if self.total is None:
|
100
|
+
return None
|
101
|
+
return self.total - self.completed
|
102
|
+
|
103
|
+
@property
|
104
|
+
def speed(self) -> Optional[float]:
|
105
|
+
"""估计速度(步数/秒)"""
|
106
|
+
if self.start_time is None:
|
107
|
+
return None
|
108
|
+
|
109
|
+
progress = self._progress
|
110
|
+
if not progress:
|
111
|
+
return None
|
112
|
+
|
113
|
+
total_time = progress[-1].timestamp - progress[0].timestamp
|
114
|
+
if total_time < 0.001:
|
115
|
+
return None
|
116
|
+
|
117
|
+
iter_progress = iter(progress)
|
118
|
+
next(iter_progress)
|
119
|
+
total_completed = sum(sample.completed for sample in iter_progress)
|
120
|
+
|
121
|
+
speed = total_completed / total_time
|
122
|
+
return speed
|
123
|
+
|
124
|
+
@property
|
125
|
+
def time_remaining(self) -> Optional[float]:
|
126
|
+
"""预估剩余时间"""
|
127
|
+
if self.finished:
|
128
|
+
return 0.0
|
129
|
+
|
130
|
+
speed = self.speed
|
131
|
+
if not speed:
|
132
|
+
return None
|
133
|
+
|
134
|
+
remaining = self.remaining
|
135
|
+
if remaining is None:
|
136
|
+
return None
|
137
|
+
|
138
|
+
estimate = remaining / speed
|
139
|
+
return estimate
|
140
|
+
|
141
|
+
|
142
|
+
class ColorProgressBar:
|
143
|
+
"""彩色进度条,支持多种终端环境和颜色渐变效果"""
|
144
|
+
|
145
|
+
def __init__(
|
146
|
+
self,
|
147
|
+
iterable: Iterable,
|
148
|
+
description: str = "Working ...",
|
149
|
+
total: Optional[float] = None,
|
150
|
+
completed: float = 0,
|
151
|
+
color: Any = "cyan",
|
152
|
+
cmap: Union[str, List[str]] = None,
|
153
|
+
update_interval: float = 0.1,
|
154
|
+
bar_length: int = None,
|
155
|
+
speed_estimate_period: float = 30.0,
|
156
|
+
):
|
157
|
+
self.iterable = iterable
|
158
|
+
self.description = description
|
159
|
+
self.color = color
|
160
|
+
self.cmap = cmap
|
161
|
+
self.update_interval = update_interval
|
162
|
+
self.bar_length = bar_length
|
163
|
+
self.speed_estimate_period = speed_estimate_period
|
164
|
+
self.mark_num = random.randint(0, 13) # 随机选择填充字符的数量
|
165
|
+
|
166
|
+
# 线程安全锁
|
167
|
+
self._lock = threading.RLock()
|
168
|
+
|
169
|
+
# 尝试获取总数
|
170
|
+
if total is None and hasattr(iterable, "__len__"):
|
171
|
+
total = len(iterable)
|
172
|
+
|
173
|
+
# 创建任务
|
174
|
+
self.task = Task(
|
175
|
+
description=description,
|
176
|
+
total=total,
|
177
|
+
completed=completed,
|
178
|
+
color=color,
|
179
|
+
)
|
180
|
+
|
181
|
+
# 输出和渲染相关
|
182
|
+
self._file = sys.stdout
|
183
|
+
self._gradient_colors = self._generate_gradient() if cmap and self.task.total else None
|
184
|
+
self._last_update_time = 0
|
185
|
+
|
186
|
+
# 检测终端环境
|
187
|
+
self._is_terminal = hasattr(self._file, "isatty") and self._file.isatty()
|
188
|
+
self._is_jupyter = "ipykernel" in sys.modules
|
189
|
+
|
190
|
+
def _generate_gradient(self) -> Optional[List[str]]:
|
191
|
+
"""生成渐变色列表(修复内置colormap支持)"""
|
192
|
+
try:
|
193
|
+
if isinstance(self.cmap, list):
|
194
|
+
cmap = LinearSegmentedColormap.from_list("custom_cmap", self.cmap)
|
195
|
+
elif hasattr(self.cmap, "__call__") and hasattr(self.cmap, "N"):
|
196
|
+
# 直接处理已经是colormap对象的情况
|
197
|
+
cmap = self.cmap
|
198
|
+
else:
|
199
|
+
cmap = get_cmap(self.cmap) # 使用oafuncs.oa_cmap.get获取内置colormap;也可获取原本自定义的colormap
|
200
|
+
|
201
|
+
return [to_hex(cmap(i)) for i in np.linspace(0, 1, int(self.task.total))]
|
202
|
+
except Exception as e:
|
203
|
+
warnings.warn(f"Colormap generation failed: {str(e)}. cmap type: {type(self.cmap)}")
|
204
|
+
return None
|
205
|
+
|
206
|
+
def _hex_to_ansi(self, hex_color: str) -> str:
|
207
|
+
"""将颜色转换为ANSI真彩色代码"""
|
208
|
+
try:
|
209
|
+
rgb = [int(x * 255) for x in to_rgb(hex_color)]
|
210
|
+
return f"\033[38;2;{rgb[0]};{rgb[1]};{rgb[2]}m"
|
211
|
+
except ValueError as e:
|
212
|
+
warnings.warn(f"Invalid color value: {e}, falling back to cyan")
|
213
|
+
return "\033[96m"
|
214
|
+
|
215
|
+
def _resolve_color(self, index: int) -> str:
|
216
|
+
"""解析当前应使用的颜色"""
|
217
|
+
if self._gradient_colors:
|
218
|
+
# 确保索引不超过颜色数
|
219
|
+
index = min(index, len(self._gradient_colors) - 1)
|
220
|
+
try:
|
221
|
+
return self._hex_to_ansi(self._gradient_colors[index])
|
222
|
+
except (IndexError, ValueError):
|
223
|
+
pass
|
224
|
+
|
225
|
+
return self._process_color_value(self.task.color)
|
226
|
+
|
227
|
+
def _process_color_value(self, color: Any) -> str:
|
228
|
+
"""处理颜色输入格式"""
|
229
|
+
preset_map = {
|
230
|
+
"red": "\033[91m",
|
231
|
+
"green": "\033[92m",
|
232
|
+
"yellow": "\033[93m",
|
233
|
+
"cyan": "\033[96m",
|
234
|
+
"blue": "\033[94m",
|
235
|
+
"magenta": "\033[95m",
|
236
|
+
"white": "\033[97m",
|
237
|
+
}
|
238
|
+
|
239
|
+
if color in preset_map:
|
240
|
+
return preset_map[color]
|
241
|
+
|
242
|
+
try:
|
243
|
+
hex_color = to_hex(color)
|
244
|
+
return self._hex_to_ansi(hex_color)
|
245
|
+
except (ValueError, TypeError) as e:
|
246
|
+
warnings.warn(f"Color parsing failed: {e}, using cyan")
|
247
|
+
return preset_map["cyan"]
|
248
|
+
|
249
|
+
def _strip_ansi(self, text: str) -> str:
|
250
|
+
"""移除所有ANSI转义序列"""
|
251
|
+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
252
|
+
return ansi_escape.sub("", text)
|
253
|
+
|
254
|
+
def _format_bar(self, progress: float, width: int) -> str:
|
255
|
+
"""格式化进度条显示"""
|
256
|
+
filled_list = ["▊", "█", "▓", "▒", "░", "#", "=", ">", "▌", "▍", "▎", "▏", "*"]
|
257
|
+
# filled = "░"
|
258
|
+
# filled = filled_list[2] # 选择中间的填充字符
|
259
|
+
# filled = random.choice(filled_list) # 随机选择填充字符
|
260
|
+
filled = filled_list[min(self.mark_num, len(filled_list) - 1)] # 随机选择填充字符
|
261
|
+
empty = " "
|
262
|
+
# 为其他信息保留更多空间
|
263
|
+
max_width = max(10, width - 60) # 至少保留10个字符的进度条
|
264
|
+
filled_length = int(round(max_width * progress))
|
265
|
+
return filled * filled_length + empty * (max_width - filled_length)
|
266
|
+
|
267
|
+
def update(self, advance: float = 1) -> None:
|
268
|
+
"""更新进度"""
|
269
|
+
with self._lock:
|
270
|
+
current_time = time.time()
|
271
|
+
old_sample_time = current_time - self.speed_estimate_period
|
272
|
+
|
273
|
+
completed_start = self.task.completed
|
274
|
+
self.task.completed += advance
|
275
|
+
update_completed = self.task.completed - completed_start
|
276
|
+
|
277
|
+
# 更新速度采样
|
278
|
+
progress = self.task._progress
|
279
|
+
|
280
|
+
# 清理旧的采样数据
|
281
|
+
while progress and progress[0].timestamp < old_sample_time:
|
282
|
+
progress.popleft()
|
283
|
+
|
284
|
+
# 添加新采样点
|
285
|
+
if update_completed > 0:
|
286
|
+
progress.append(ProgressSample(current_time, update_completed))
|
287
|
+
|
288
|
+
# 检查是否完成
|
289
|
+
if self.task.total is not None and self.task.completed >= self.task.total and self.task.finished_time is None:
|
290
|
+
self.task.finished_time = self.task.elapsed
|
291
|
+
self.task.finished_speed = self.task.speed
|
292
|
+
|
293
|
+
def render(self) -> str:
|
294
|
+
"""渲染进度条"""
|
295
|
+
with self._lock:
|
296
|
+
# 应该在锁保护下访问task对象
|
297
|
+
task = self.task
|
298
|
+
|
299
|
+
# 获取终端宽度
|
300
|
+
try:
|
301
|
+
term_width = self.bar_length or (shutil.get_terminal_size().columns if self._is_terminal else 80)
|
302
|
+
except (AttributeError, OSError):
|
303
|
+
term_width = 80 # 默认终端宽度
|
304
|
+
|
305
|
+
# 确保有效宽度不小于最低限制
|
306
|
+
effective_width = max(15, term_width - 40)
|
307
|
+
if effective_width < 10:
|
308
|
+
warnings.warn("Terminal width is too small for proper progress bar rendering.")
|
309
|
+
effective_width = 10 # 设置最低宽度限制
|
310
|
+
|
311
|
+
# 计算进度信息
|
312
|
+
progress = task.completed / task.total if task.total else 0
|
313
|
+
index = int(task.completed)
|
314
|
+
current_color = self._resolve_color(index) if self._gradient_colors else self._resolve_color(0)
|
315
|
+
reset_code = "\033[0m"
|
316
|
+
|
317
|
+
# 计算时间和速度信息
|
318
|
+
elapsed = task.elapsed or 0
|
319
|
+
remaining = task.time_remaining
|
320
|
+
speed = task.speed or 0
|
321
|
+
|
322
|
+
# 调整时间单位
|
323
|
+
if elapsed >= 3600:
|
324
|
+
elapsed_info = f"Elapsed: {elapsed / 3600:.1f}h"
|
325
|
+
elif elapsed >= 60:
|
326
|
+
elapsed_info = f"Elapsed: {elapsed / 60:.1f}m"
|
327
|
+
else:
|
328
|
+
elapsed_info = f"Elapsed: {elapsed:.1f}s"
|
329
|
+
|
330
|
+
time_info = f"ETA: {remaining:.1f}s" if task.total and remaining and remaining > 0 else elapsed_info
|
331
|
+
|
332
|
+
# 构建进度条视觉部分
|
333
|
+
bar = self._format_bar(progress, effective_width)
|
334
|
+
|
335
|
+
# 获取当前时间
|
336
|
+
current_time = time.strftime("%H:%M:%S", time.localtime())
|
337
|
+
|
338
|
+
# 组织显示文本
|
339
|
+
count_info = f"{int(task.completed)}/{int(task.total)}" if task.total else str(int(task.completed))
|
340
|
+
percent = f"{progress:.1%}" if task.total else ""
|
341
|
+
rate_info = f"{speed:.1f}it/s" if speed else ""
|
342
|
+
|
343
|
+
# 构建完整的进度条行
|
344
|
+
line = f"{current_time} {self.description} {current_color}[{bar}]{reset_code} {count_info} {percent} [{time_info} | {rate_info}]"
|
345
|
+
|
346
|
+
# 确保不超出终端宽度
|
347
|
+
if len(self._strip_ansi(line)) > term_width:
|
348
|
+
line = line[: term_width - 3] + "..."
|
349
|
+
|
350
|
+
return line
|
351
|
+
|
352
|
+
def refresh(self) -> None:
|
353
|
+
"""刷新显示进度条"""
|
354
|
+
if not self._is_terminal and not self._is_jupyter:
|
355
|
+
return
|
356
|
+
|
357
|
+
with self._lock:
|
358
|
+
line = self.render()
|
359
|
+
|
360
|
+
# 根据环境选择不同的输出方式
|
361
|
+
if self._is_jupyter:
|
362
|
+
# Jupyter环境,使用换行模式
|
363
|
+
self._file.write(f"{line}\n")
|
364
|
+
elif self._is_terminal:
|
365
|
+
# 标准终端环境,覆盖同一行
|
366
|
+
self._file.write(f"\r{line}")
|
367
|
+
else:
|
368
|
+
# 非交互式环境,仅在完成时输出
|
369
|
+
if self.task.finished:
|
370
|
+
self._file.write(f"{line}\n")
|
371
|
+
|
372
|
+
# 强制刷新输出
|
373
|
+
self._file.flush()
|
374
|
+
|
375
|
+
def __iter__(self):
|
376
|
+
"""迭代器实现,支持进度显示"""
|
377
|
+
self.task.start()
|
378
|
+
self._last_update_time = time.time()
|
379
|
+
|
380
|
+
try:
|
381
|
+
# 迭代原始可迭代对象
|
382
|
+
for i, item in enumerate(self.iterable):
|
383
|
+
yield item
|
384
|
+
|
385
|
+
# 更新进度
|
386
|
+
self.update(1)
|
387
|
+
|
388
|
+
# 判断是否需要刷新显示
|
389
|
+
now = time.time()
|
390
|
+
should_refresh = (now - self._last_update_time >= self.update_interval) or (self.task.total and self.task.completed >= self.task.total)
|
391
|
+
|
392
|
+
if should_refresh:
|
393
|
+
self.refresh()
|
394
|
+
self._last_update_time = now
|
395
|
+
|
396
|
+
finally:
|
397
|
+
# 完成后进行清理
|
398
|
+
if self._is_terminal and not self._is_jupyter:
|
399
|
+
self._file.write("\n")
|
400
|
+
self._file.flush()
|
401
|
+
|
402
|
+
@classmethod
|
403
|
+
def gradient_color(cls, colors: List[str], n: int) -> List[str]:
|
404
|
+
"""生成渐变色列表"""
|
405
|
+
cmap = LinearSegmentedColormap.from_list("gradient", colors)
|
406
|
+
return [to_hex(cmap(i)) for i in np.linspace(0, 1, n)]
|
407
|
+
|
408
|
+
|
409
|
+
# 验证示例
|
410
|
+
if __name__ == "__main__":
|
411
|
+
for _ in ColorProgressBar(range(100), description="Diverging:", cmap="diverging_1"):
|
412
|
+
# print("\rTesting...", end="")
|
413
|
+
time.sleep(0.1)
|
414
|
+
|
415
|
+
for _ in ColorProgressBar(range(100), description="Colorful:", cmap="colorful_1"):
|
416
|
+
time.sleep(0.1)
|
417
|
+
|
418
|
+
for _ in ColorProgressBar(range(100), description="Viridis:", cmap="viridis"):
|
419
|
+
time.sleep(0.1)
|
420
|
+
|
421
|
+
# 使用自定义渐变色
|
422
|
+
for _ in ColorProgressBar(range(50), description="Custom_cmap:", cmap=["#FF0000", "#0000FF"]):
|
423
|
+
time.sleep(0.1)
|
424
|
+
|
425
|
+
for _ in ColorProgressBar(range(50), description="Default:"):
|
426
|
+
time.sleep(0.1)
|
427
|
+
|
428
|
+
for _ in ColorProgressBar(range(50), description="Custom_color:", color="#FF00FF"):
|
429
|
+
time.sleep(0.1)
|
430
|
+
|
431
|
+
# 测试无法获取长度的迭代器
|
432
|
+
def infinite_generator():
|
433
|
+
i = 0
|
434
|
+
while True:
|
435
|
+
yield i
|
436
|
+
i += 1
|
437
|
+
|
438
|
+
# 限制为20个元素,但进度条不知道总长度
|
439
|
+
gen = infinite_generator()
|
440
|
+
for i, _ in enumerate(ColorProgressBar(gen, description="Unknown_length:")):
|
441
|
+
if i >= 20:
|
442
|
+
break
|
443
|
+
time.sleep(0.1)
|
@@ -1,17 +1,19 @@
|
|
1
1
|
#!/usr/bin/env python
|
2
2
|
# coding=utf-8
|
3
|
-
|
3
|
+
"""
|
4
4
|
Author: Liu Kun && 16031215@qq.com
|
5
|
-
Date:
|
5
|
+
Date: 2025-04-04 20:21:59
|
6
6
|
LastEditors: Liu Kun && 16031215@qq.com
|
7
|
-
LastEditTime:
|
8
|
-
FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\
|
9
|
-
Description:
|
7
|
+
LastEditTime: 2025-04-04 20:21:59
|
8
|
+
FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\_script\\email.py
|
9
|
+
Description:
|
10
10
|
EditPlatform: vscode
|
11
11
|
ComputerInfo: XPS 15 9510
|
12
12
|
SystemInfo: Windows 11
|
13
13
|
Python Version: 3.12
|
14
|
-
|
14
|
+
"""
|
15
|
+
|
16
|
+
|
15
17
|
|
16
18
|
|
17
19
|
import random
|
@@ -22,29 +24,29 @@ from email.mime.text import MIMEText
|
|
22
24
|
|
23
25
|
from rich import print
|
24
26
|
|
25
|
-
__all__ = [
|
27
|
+
__all__ = ["send"]
|
26
28
|
|
27
29
|
|
28
30
|
def _email_info():
|
29
31
|
email_dict = {
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
32
|
+
"liukun0312@vip.qq.com": [4, 13, -10, 2, -10, 4, -7, -8, 8, -1, 3, -2, -11, -6, -9, -7],
|
33
|
+
"756866877@qq.com": [4, -2, -3, 13, 12, 8, -6, 9, -12, 13, -10, -12, -11, -12, -4, -11],
|
34
|
+
"99138763@qq.com": [0, 6, 12, 2, 9, 9, 4, -1, 11, -7, -12, 6, -11, -3, -5, -11],
|
35
|
+
"1031260@qq.com": [-1, -5, -9, 9, -3, 4, -8, -7, -12, -2, 0, -9, -11, -3, -7, -10],
|
34
36
|
}
|
35
37
|
keys = list(email_dict.keys())
|
36
|
-
choose_email = random.randint(0, len(keys)-1)
|
38
|
+
choose_email = random.randint(0, len(keys) - 1)
|
37
39
|
msg_from = keys[choose_email]
|
38
40
|
return msg_from, email_dict[msg_from]
|
39
41
|
|
40
42
|
|
41
43
|
def _decode_password(password):
|
42
|
-
return
|
44
|
+
return "".join([chr(i + 109) for i in password])
|
43
45
|
|
44
46
|
|
45
47
|
def _send_message(title, content, msg_to):
|
46
48
|
# 1. 连接邮箱服务器
|
47
|
-
con = smtplib.SMTP_SSL(
|
49
|
+
con = smtplib.SMTP_SSL("smtp.qq.com", 465)
|
48
50
|
|
49
51
|
# 2. 登录邮箱
|
50
52
|
msg_from, password = _email_info()
|
@@ -55,14 +57,14 @@ def _send_message(title, content, msg_to):
|
|
55
57
|
msg = MIMEMultipart()
|
56
58
|
|
57
59
|
# 设置邮件主题
|
58
|
-
subject = Header(title,
|
59
|
-
msg[
|
60
|
+
subject = Header(title, "utf-8").encode()
|
61
|
+
msg["Subject"] = subject
|
60
62
|
|
61
63
|
# 设置邮件发送者
|
62
|
-
msg[
|
64
|
+
msg["From"] = msg_from
|
63
65
|
|
64
66
|
# 设置邮件接受者
|
65
|
-
msg[
|
67
|
+
msg["To"] = msg_to
|
66
68
|
|
67
69
|
# # 添加html内容
|
68
70
|
# content = """
|
@@ -78,18 +80,18 @@ def _send_message(title, content, msg_to):
|
|
78
80
|
|
79
81
|
# or
|
80
82
|
# content = '发送内容'
|
81
|
-
msg.attach(MIMEText(content,
|
83
|
+
msg.attach(MIMEText(content, "plain", "utf-8"))
|
82
84
|
|
83
85
|
# 4.发送邮件
|
84
86
|
con.sendmail(msg_from, msg_to, msg.as_string())
|
85
87
|
con.quit()
|
86
88
|
|
87
|
-
print(f
|
88
|
-
print(
|
89
|
+
print(f"已通过{msg_from}成功向{msg_to}发送邮件!")
|
90
|
+
print("发送内容为:\n{}\n\n".format(content))
|
89
91
|
|
90
92
|
|
91
|
-
def send(title=
|
92
|
-
|
93
|
+
def send(title="Title", content=None, send_to="16031215@qq.com"):
|
94
|
+
"""
|
93
95
|
Description: 发送邮件
|
94
96
|
|
95
97
|
Args:
|
@@ -102,7 +104,7 @@ def send(title='Title', content=None, send_to='16031215@qq.com'):
|
|
102
104
|
|
103
105
|
Example:
|
104
106
|
send(title='Title', content='Content', '123@qq.com')
|
105
|
-
|
107
|
+
"""
|
106
108
|
if content is None:
|
107
109
|
# 避免发送空邮件,或有人误调用
|
108
110
|
return
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import os
|
2
|
+
from typing import List, Optional, Union
|
3
|
+
from dask.diagnostics import ProgressBar
|
4
|
+
import xarray as xr
|
5
|
+
from oafuncs import pbar
|
6
|
+
|
7
|
+
def merge_nc(file_list: Union[str, List[str]], var_name: Optional[Union[str, List[str]]] = None, dim_name: Optional[str] = None, target_filename: Optional[str] = None) -> None:
|
8
|
+
"""
|
9
|
+
Description:
|
10
|
+
Merge variables from multiple NetCDF files along a specified dimension and write to a new file.
|
11
|
+
If var_name is a string, it is considered a single variable; if it is a list and has only one element, it is also a single variable;
|
12
|
+
If the list has more than one element, it is a multi-variable; if var_name is None, all variables are merged.
|
13
|
+
|
14
|
+
Parameters:
|
15
|
+
file_list: List of NetCDF file paths or a single file path as a string
|
16
|
+
var_name: Name of the variable to be extracted or a list of variable names, default is None, which means all variables are extracted
|
17
|
+
dim_name: Dimension name used for merging
|
18
|
+
target_filename: Target file name after merging
|
19
|
+
|
20
|
+
Example:
|
21
|
+
merge(file_list, var_name='u', dim_name='time', target_filename='merged.nc')
|
22
|
+
merge(file_list, var_name=['u', 'v'], dim_name='time', target_filename='merged.nc')
|
23
|
+
merge(file_list, var_name=None, dim_name='time', target_filename='merged.nc')
|
24
|
+
"""
|
25
|
+
|
26
|
+
if target_filename is None:
|
27
|
+
target_filename = "merged.nc"
|
28
|
+
|
29
|
+
# 确保目标路径存在
|
30
|
+
target_dir = os.path.dirname(target_filename)
|
31
|
+
if target_dir and not os.path.exists(target_dir):
|
32
|
+
os.makedirs(target_dir)
|
33
|
+
|
34
|
+
if isinstance(file_list, str):
|
35
|
+
file_list = [file_list]
|
36
|
+
|
37
|
+
# 初始化变量名列表
|
38
|
+
if var_name is None:
|
39
|
+
with xr.open_dataset(file_list[0]) as ds:
|
40
|
+
var_names = list(ds.variables.keys())
|
41
|
+
elif isinstance(var_name, str):
|
42
|
+
var_names = [var_name]
|
43
|
+
elif isinstance(var_name, list):
|
44
|
+
var_names = var_name
|
45
|
+
else:
|
46
|
+
raise ValueError("var_name must be a string, a list of strings, or None")
|
47
|
+
|
48
|
+
# 初始化合并数据字典
|
49
|
+
merged_data = {}
|
50
|
+
|
51
|
+
for i, file in pbar(enumerate(file_list),description="Reading files", color="green",total=len(file_list)):
|
52
|
+
with xr.open_dataset(file) as ds:
|
53
|
+
for var in var_names:
|
54
|
+
data_var = ds[var]
|
55
|
+
if dim_name in data_var.dims:
|
56
|
+
merged_data.setdefault(var, []).append(data_var)
|
57
|
+
elif var not in merged_data:
|
58
|
+
merged_data[var] = data_var.fillna(0) # 用0填充NaN值
|
59
|
+
|
60
|
+
for var in pbar(merged_data, description="Merging variables", color="#9b45d1"):
|
61
|
+
if isinstance(merged_data[var], list):
|
62
|
+
merged_data[var] = xr.concat(merged_data[var], dim=dim_name).fillna(0)
|
63
|
+
# print(f"Variable '{var}' merged: min={merged_data[var].min().values:.3f}, max={merged_data[var].max().values:.3f}, mean={merged_data[var].mean().values:.3f}")
|
64
|
+
|
65
|
+
# 修改写入数据部分,支持压缩并设置基数和比例因子
|
66
|
+
# print("\nWriting data to file ...")
|
67
|
+
if os.path.exists(target_filename):
|
68
|
+
print("Warning: The target file already exists. Removing it ...")
|
69
|
+
os.remove(target_filename)
|
70
|
+
|
71
|
+
with xr.Dataset(merged_data) as merged_dataset:
|
72
|
+
encoding = {}
|
73
|
+
for var in merged_dataset.data_vars:
|
74
|
+
data = merged_dataset[var].values
|
75
|
+
# print(f"Variable '{var}' ready for writing: min={data.min():.3f}, max={data.max():.3f}, mean={data.mean():.3f}")
|
76
|
+
if data.dtype.kind in {"i", "u", "f"}: # 仅对数值型数据进行压缩
|
77
|
+
data_range = data.max() - data.min()
|
78
|
+
if data_range > 0: # 避免范围过小导致的精度问题
|
79
|
+
scale_factor = data_range / (2**16 - 1)
|
80
|
+
add_offset = data.min()
|
81
|
+
encoding[var] = {
|
82
|
+
"zlib": True,
|
83
|
+
"complevel": 4,
|
84
|
+
"dtype": "int16",
|
85
|
+
"scale_factor": scale_factor,
|
86
|
+
"add_offset": add_offset,
|
87
|
+
"_FillValue": -32767,
|
88
|
+
}
|
89
|
+
else:
|
90
|
+
encoding[var] = {"zlib": True, "complevel": 4} # 范围过小时禁用缩放
|
91
|
+
else:
|
92
|
+
encoding[var] = {"zlib": True, "complevel": 4} # 非数值型数据不使用缩放
|
93
|
+
|
94
|
+
# 确保写入时不会因编码问题导致数据丢失
|
95
|
+
# merged_dataset.to_netcdf(target_filename, encoding=encoding)
|
96
|
+
delayed_write = merged_dataset.to_netcdf(target_filename, encoding=encoding, compute=False)
|
97
|
+
with ProgressBar():
|
98
|
+
delayed_write.compute()
|
99
|
+
|
100
|
+
print(f'\nFile "{target_filename}" has been successfully created.')
|
101
|
+
|
102
|
+
|
103
|
+
# Example usage
|
104
|
+
if __name__ == "__main__":
|
105
|
+
files_to_merge = ["file1.nc", "file2.nc", "file3.nc"]
|
106
|
+
output_path = "merged_output.nc"
|
107
|
+
merge_nc(files_to_merge, var_name=None, dim_name="time", target_filename=output_path)
|