mdrtreadtool 2.0.0__py3-none-win_amd64.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.
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ """mdrtreadtool —— 医疗器械警戒趋势分析工具(编译发行版)。
3
+
4
+ 本包在 PyPI 上分发的是一个 **已编译** 的扩展模块 ``chart_tool.pyd``
5
+ (Cython + CPython 稳定 ABI / abi3),不包含任何 ``.py`` 形式的业务源码。
6
+
7
+ 命令行入口::
8
+
9
+ mdrtreadtool 启动图形界面
10
+ mdrtreadtool --selftest 环境自检(弹窗 + 写出报告)
11
+ mdrtreadtool --selftest --quiet
12
+ mdrtreadtool --info 只打印模块信息并做一次纯函数自检
13
+ mdrtreadtool --version 打印版本号
14
+
15
+ 在代码中使用::
16
+
17
+ import mdrtreadtool
18
+ print(mdrtreadtool.__version__)
19
+
20
+ from mdrtreadtool import load_module # 只加载、不启动界面
21
+ mod = load_module()
22
+ den, ev = mod.make_sample_data()
23
+ """
24
+
25
+ __version__ = "2.0.0"
26
+ __author__ = "sysucai"
27
+ __license__ = "Proprietary"
28
+ __all__ = ["__version__", "__author__", "__license__", "load_module", "resource_path"]
29
+
30
+
31
+ def load_module():
32
+ """加载并返回已编译的主程序模块(不启动界面)。
33
+
34
+ 等价于 ``from mdrtreadtool import _loader; _loader.load_module()``;
35
+ 放在这里是为了让 ``import mdrtreadtool`` 保持轻量(不在导入期碰 tkinter)。
36
+ """
37
+ from ._loader import load_module as _load
38
+ return _load()
39
+
40
+
41
+ def resource_path(name):
42
+ """返回包内随附资源(如 chart_tool.ico)的绝对路径;不存在返回 None。"""
43
+ from ._loader import resource_path as _res
44
+ return _res(name)
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ """支持 ``python -m mdrtreadtool`` 方式启动。"""
3
+ import sys
4
+
5
+ from ._loader import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """mdrtreadtool 的编译模块加载器。
4
+
5
+ 包内随附的 ``chart_tool.pyd`` 是主程序经 Cython 编译、以 CPython 稳定 ABI
6
+ (abi3,``Py_LIMITED_API = 0x03080000``)生成的扩展模块。同一个文件可以直接被
7
+ CPython 3.8 / 3.9 / 3.10 / 3.11 / 3.12 / 3.13 / 3.14(Windows x64)加载,
8
+ 不必为每个版本重新编译。
9
+
10
+ 加载器**不 import 任何第三方库**——即便 numpy / pandas / matplotlib 缺失,
11
+ 也能先把 ``mdrtreadtool --info`` 跑起来,一眼看清是「模块加载失败」还是
12
+ 「运行依赖缺失」,而不是毫无提示地退出。
13
+
14
+ 命令行
15
+ ------
16
+ mdrtreadtool 启动图形界面
17
+ mdrtreadtool --selftest 环境自检(弹窗 + 写出报告)
18
+ mdrtreadtool --selftest --quiet 只写报告、不弹窗
19
+ mdrtreadtool --info 不启动界面,只打印模块信息并做一次纯函数自检
20
+ mdrtreadtool --version 打印版本号
21
+ mdrtreadtool --no-update 本次启动不做自动更新
22
+
23
+ 除 ``--info`` / ``--version`` 外,其余参数原样交给主程序处理。
24
+
25
+ 自动更新
26
+ --------
27
+ 启动时(``--info`` / ``--version`` 除外)会先尝试通过 pip 自动更新,见 ``_update`` 模块。
28
+ **只对 pip 安装的发行版生效**:单文件 exe(``sys.frozen``)环境会直接跳过——exe 的升级
29
+ 方式是替换新 exe,不该由 pip 覆盖。
30
+ 关键点是**时机**:更新必须在 import ``chart_tool.pyd`` **之前**完成——该文件一旦被加载
31
+ 就被 Windows 锁定、无法覆盖;而在加载前替换掉,本次启动用到的就是新版本。
32
+
33
+ 三步(查询 / 下载 / 安装)各带超时,任何一步超时或失败都只打印一行提示并继续启动;
34
+ 正常情况下**完全静默**。用 ``MDRTREADTOOL_UPDATE_VERBOSE=1`` 可看到每一步的结果,
35
+ 用 ``MDRTREADTOOL_NO_UPDATE=1`` 或 ``--no-update`` 可关闭。
36
+ """
37
+ import importlib.machinery
38
+ import importlib.util
39
+ import os
40
+ import sys
41
+
42
+ PACKAGE = "mdrtreadtool"
43
+ HERE = os.path.dirname(os.path.abspath(__file__))
44
+
45
+ # 优先按此顺序找模块文件;找不到再扫描本包目录下任意扩展模块
46
+ PREFERRED = ("chart_tool", "警戒图表工具")
47
+ EXT_SUFFIXES = (".pyd", ".so", ".dylib")
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # 模块定位与加载
52
+ # ---------------------------------------------------------------------------
53
+ def _module_dirs():
54
+ """按优先级返回「模块搜索目录」列表。
55
+
56
+ 正常安装时模块就在本包目录里;额外兼容两种非 pip 场景:
57
+ · PyInstaller 单文件 exe —— 解包目录 sys._MEIPASS
58
+ · 绿色目录 —— 可执行文件所在目录
59
+ """
60
+ cands = []
61
+ meipass = getattr(sys, "_MEIPASS", None)
62
+ if meipass:
63
+ cands.append(meipass)
64
+ if getattr(sys, "frozen", False):
65
+ try:
66
+ cands.append(os.path.dirname(os.path.abspath(sys.executable)))
67
+ except Exception: # noqa: BLE001
68
+ pass
69
+ cands.append(HERE)
70
+ out, seen = [], set()
71
+ for d in cands:
72
+ d = os.path.abspath(d)
73
+ if d not in seen:
74
+ seen.add(d)
75
+ out.append(d)
76
+ return out
77
+
78
+
79
+ def _pick_module():
80
+ """返回 (模块文件绝对路径, 模块短名)。
81
+
82
+ 短名必须取自**文件名**第一个点之前的部分:扩展模块的初始化函数名
83
+ (``PyInit_<短名>``)在编译时已固定,加载时给的短名与它不一致就会导入失败。
84
+ """
85
+ found = [] # [(目录, 文件绝对路径), ...]
86
+ for d in _module_dirs():
87
+ try:
88
+ names = sorted(os.listdir(d))
89
+ except OSError:
90
+ continue
91
+ for fn in names:
92
+ if fn.lower().endswith(EXT_SUFFIXES) and not fn.startswith("__"):
93
+ found.append((d, os.path.join(d, fn)))
94
+ for want in PREFERRED: # 先按首选名精确匹配
95
+ for _d, p in found:
96
+ if os.path.basename(p).split(".")[0].lower() == want.lower():
97
+ return p, os.path.basename(p).split(".")[0]
98
+ # 未命中首选名时,只有在「未打包」的情况下才退回到任意扩展模块——打包后的
99
+ # 解包目录里还躺着 numpy / pandas 等一大批 .pyd,盲目取第一个会加载错对象。
100
+ if found and not getattr(sys, "frozen", False):
101
+ p = found[0][1]
102
+ return p, os.path.basename(p).split(".")[0]
103
+ return None, None
104
+
105
+
106
+ def _load_module(path, shortname):
107
+ """按显式模块名加载扩展模块(不用 __import__,以免依赖 sys.path 与文件命名约定)。"""
108
+ fullname = "%s.%s" % (PACKAGE, shortname)
109
+ loader = importlib.machinery.ExtensionFileLoader(fullname, path)
110
+ spec = importlib.util.spec_from_file_location(fullname, path, loader=loader)
111
+ if spec is None or spec.loader is None:
112
+ raise ImportError("无法为 %s 创建加载器。" % path)
113
+ mod = importlib.util.module_from_spec(spec)
114
+ sys.modules.setdefault(fullname, mod) # 先登记,便于模块内部自引用
115
+ try:
116
+ spec.loader.exec_module(mod)
117
+ except BaseException:
118
+ sys.modules.pop(fullname, None)
119
+ raise
120
+ return mod
121
+
122
+
123
+ def resource_path(name):
124
+ """返回包内随附资源(如 chart_tool.ico)的绝对路径;不存在返回 None。"""
125
+ for d in _module_dirs():
126
+ p = os.path.join(d, name)
127
+ try:
128
+ if os.path.isfile(p):
129
+ return p
130
+ except Exception: # noqa: BLE001
131
+ pass
132
+ return None
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # 失败提示
137
+ # ---------------------------------------------------------------------------
138
+ def _notify(title, text):
139
+ """无控制台时用弹窗提示致命错误。
140
+
141
+ 有控制台就不打扰:信息已经写到 stderr。若用 pythonw / gui 方式启动,
142
+ ``sys.stderr`` 为 None,不做这一步用户只会看到「启动了但没反应」。
143
+ """
144
+ if sys.stderr is not None:
145
+ return
146
+ try:
147
+ import tkinter as tk
148
+ from tkinter import messagebox
149
+ root = tk.Tk()
150
+ root.withdraw()
151
+ messagebox.showerror(title, text)
152
+ root.destroy()
153
+ except Exception: # noqa: BLE001
154
+ pass
155
+
156
+
157
+ def _fail(msg, hint=""):
158
+ text = "\n[%s] 无法启动:%s\n" % (PACKAGE, msg)
159
+ if hint:
160
+ text += "\n%s\n" % hint
161
+ try:
162
+ sys.stderr.write(text)
163
+ except Exception: # noqa: BLE001
164
+ pass
165
+ _notify("无法启动", ("%s\n\n%s" % (msg, hint)).strip())
166
+ return 2
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # 对外接口
171
+ # ---------------------------------------------------------------------------
172
+ def load_module():
173
+ """加载并返回已编译的主程序模块(不启动界面)。
174
+
175
+ 失败时抛出 ImportError / ModuleNotFoundError,供调用方自行处理;
176
+ 命令行路径请用 main(),它会给出中文提示而不是抛栈。
177
+ """
178
+ path, shortname = _pick_module()
179
+ if path is None:
180
+ raise ImportError(
181
+ "在 %s 中没有找到扩展模块(%s)。请重新安装:"
182
+ "python -m pip install --force-reinstall mdrtreadtool"
183
+ % (HERE, " / ".join(EXT_SUFFIXES)))
184
+ return _load_module(path, shortname)
185
+
186
+
187
+ def _auto_update(argv):
188
+ """启动时尝试通过 pip 自动更新;任何异常都不影响启动。
189
+
190
+ 只在真正启动程序(以及 ``--selftest``)时调用;``--info`` / ``--version`` 这类
191
+ 诊断用法不会触发联网。``--no-update`` 或 ``MDRTREADTOOL_NO_UPDATE=1`` 可关闭。
192
+
193
+ **单文件 exe 环境不自动更新**:exe 自带解释器与依赖,升级方式是替换新 exe,
194
+ 不应由 pip 覆盖。此处与 ``_update.maybe_update()`` 各设一道守卫。
195
+ """
196
+ if getattr(sys, "frozen", False):
197
+ return None
198
+ if "--no-update" in argv or "-no-update" in argv:
199
+ return None
200
+ try:
201
+ from . import _update
202
+ except Exception: # noqa: BLE001
203
+ return None
204
+ try:
205
+ return _update.maybe_update(say=_say)
206
+ except Exception: # noqa: BLE001
207
+ return None
208
+
209
+
210
+ def _say(text):
211
+ """往控制台写一行;无 stdout 时安静放弃(不打断启动)。"""
212
+ try:
213
+ sys.stdout.write(text + "\n")
214
+ sys.stdout.flush()
215
+ except Exception: # noqa: BLE001
216
+ pass
217
+
218
+
219
+ def _print_info(mod, path, name):
220
+ print("=" * 72)
221
+ print(" mdrtreadtool 模块信息")
222
+ print("=" * 72)
223
+ try:
224
+ from . import __version__ as _v
225
+ except Exception: # noqa: BLE001
226
+ _v = "?"
227
+ print(" 版本 : %s" % _v)
228
+ print(" Python : %s (%d 位)"
229
+ % (sys.version.split()[0], 64 if sys.maxsize > 2 ** 32 else 32))
230
+ print(" 解释器 : %s" % sys.executable)
231
+ print(" 模块文件 : %s" % getattr(mod, "__file__", path))
232
+ print(" 模块名 : %s" % getattr(mod, "__name__", name))
233
+ print(" 程序目录 : %s" % (mod._app_dir() if hasattr(mod, "_app_dir") else "?"))
234
+ print(" 窗口图标 : %s" % (resource_path("chart_tool.ico") or "(未随包提供,将使用默认图标)"))
235
+ try:
236
+ from . import _update
237
+ print(" 自动更新 : %s" % _update.summary())
238
+ except Exception: # noqa: BLE001
239
+ print(" 自动更新 : (不可用)")
240
+ print(" 可调用入口 : main=%s run_selftest=%s"
241
+ % (callable(getattr(mod, "main", None)),
242
+ callable(getattr(mod, "run_selftest", None))))
243
+ try:
244
+ den, ev = mod.make_sample_data()
245
+ df, meta = mod.compute_pareto(ev, risk_dim="器械故障表现", gran="Q")
246
+ print(" 纯函数自检 : 示例数据 %d 期分母 / %d 例事件;帕累托 %s→%s 共 %d 类"
247
+ % (len(den), len(ev), meta["period_prev"], meta["period_curr"], len(df)))
248
+ print(" 依据 : Python %s(稳定 ABI / abi3,支持 3.8~3.14)"
249
+ % sys.version.split()[0])
250
+ print("=" * 72)
251
+ print(" 结果:模块加载与统计函数均正常。执行 mdrtreadtool 启动界面。")
252
+ except Exception as exc: # noqa: BLE001
253
+ print(" [警告] 纯函数自检失败:%s: %s" % (type(exc).__name__, exc))
254
+ print("=" * 72)
255
+ return 0
256
+
257
+
258
+ def main(argv=None):
259
+ """命令行入口:定位、加载编译模块,然后按参数启动界面或自检。"""
260
+ argv = list(sys.argv[1:] if argv is None else argv)
261
+
262
+ if "--version" in argv or "-V" in argv:
263
+ try:
264
+ from . import __version__ as _v
265
+ except Exception: # noqa: BLE001
266
+ _v = "?"
267
+ print("%s %s" % (PACKAGE, _v))
268
+ return 0
269
+
270
+ # 自动更新:必须在 import chart_tool.pyd 之前完成——该文件一旦被加载就被 Windows
271
+ # 锁定、无法覆盖;在加载前替换掉,本次启动用到的就是新版本。
272
+ # --info 属于纯诊断用法,不触发联网。
273
+ if "--info" not in argv:
274
+ _auto_update(argv)
275
+
276
+ path, name = _pick_module()
277
+ if path is None:
278
+ return _fail(
279
+ "本包中没有找到扩展模块(%s)。" % " / ".join(EXT_SUFFIXES),
280
+ "安装似乎不完整,请重装:\n %s -m pip install --force-reinstall mdrtreadtool"
281
+ % os.path.basename(sys.executable))
282
+ try:
283
+ mod = _load_module(path, name)
284
+ except ModuleNotFoundError as exc:
285
+ miss = getattr(exc, "name", "") or str(exc)
286
+ return _fail(
287
+ "加载 %s 时缺少运行依赖:%s" % (os.path.basename(path), miss),
288
+ "请先安装依赖:\n %s -m pip install numpy pandas matplotlib openpyxl\n"
289
+ "(本 .pyd 只需要 Python 运行时依赖,不需要编译器或 Cython。)"
290
+ % os.path.basename(sys.executable))
291
+ except ImportError as exc:
292
+ return _fail("加载 %s 失败:%s" % (os.path.basename(path), exc),
293
+ "常见原因:\n"
294
+ " · .pyd 与当前 Python 版本/架构不匹配(本 .pyd 支持 CPython 3.8~3.14、64 位);\n"
295
+ " · 安装过程中文件损坏,请重装:pip install --force-reinstall mdrtreadtool")
296
+ except OSError as exc:
297
+ return _fail("加载 %s 失败(%s)" % (os.path.basename(path), exc),
298
+ "若提示「不是有效的 Win32 应用程序」,说明位数不匹配:"
299
+ "本 .pyd 为 64 位,请使用 64 位 Python。")
300
+
301
+ if "--info" in argv:
302
+ return _print_info(mod, path, name)
303
+
304
+ entry = getattr(mod, "main", None)
305
+ if not callable(entry):
306
+ return _fail("%s 里没有 main() 入口,可能编译的不是主程序源码。"
307
+ % os.path.basename(path))
308
+ entry() # 主程序内部负责界面生命周期与进程退出
309
+ return 0
310
+
311
+
312
+ if __name__ == "__main__":
313
+ sys.exit(main())
@@ -0,0 +1,389 @@
1
+ # -*- coding: utf-8 -*-
2
+ """mdrtreadtool 的自动更新(仅对 pip 安装的发行版生效)。
3
+
4
+ 设计要点
5
+ --------
6
+ 1. **只服务 pip 安装版**:本模块由 ``_loader.py`` 调用,而 ``_loader.py`` 只存在于
7
+ PyPI 分发的 wheel 里;源码运行(``python gen.py``)与单文件 exe 都不走这条路,
8
+ 因此不受影响。为保险起见,``maybe_update()`` 还会显式检查 ``sys.frozen``,
9
+ 一旦处于 PyInstaller 打包环境就立即跳过——exe 有自己的升级途径(换新 exe),
10
+ 绝不该被 pip 覆盖。
11
+ 2. **在加载编译模块之前执行**:``chart_tool.pyd`` 一旦被 import 就会被 Windows 锁定,
12
+ 无法覆盖。加载器在 import 之前完成更新,随后 import 到的就是新版本,本次启动即生效。
13
+ 3. **节流**:两次联网检查之间至少间隔 ``CHECK_INTERVAL``,避免每次启动都等网络。
14
+ 4. **超时即跳过**:查询 / 下载 / 安装三步各带超时,任何一步超时或失败都只记一笔日志,
15
+ 绝不影响程序正常启动(所有异常都被吞掉)。
16
+ 5. **只升不降**:仅当 PyPI 上的版本高于本机已装版本时才更新。
17
+
18
+ 可用的环境变量
19
+ --------------
20
+ MDRTREADTOOL_NO_UPDATE=1 关闭自动更新
21
+ MDRTREADTOOL_UPDATE_FORCE=1 忽略节流,本次强制联网检查
22
+ MDRTREADTOOL_UPDATE_VERBOSE=1 打印每一步的结果(正常、已最新、超时等)
23
+ MDRTREADTOOL_UPDATE_DIR=<目录> 覆盖状态文件与下载目录(默认 ~/treadtools/updates)
24
+ MDRTREADTOOL_PYPI_JSON=<URL> 覆盖 PyPI JSON 地址(便于离线自测 / 内网镜像)
25
+ """
26
+ import json
27
+ import os
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ import sys
32
+ import time
33
+ import urllib.request
34
+
35
+ PROJECT = "mdrtreadtool"
36
+ USER_AGENT = "mdrtreadtool-updater/2.0"
37
+
38
+ # 两次联网检查之间的最小间隔(秒)。到点才联网,平时启动不等网络。
39
+ CHECK_INTERVAL = 6 * 3600
40
+ # 查询 / 下载 / 安装 三步各自的超时(秒)。
41
+ CHECK_TIMEOUT = 5.0
42
+ DOWNLOAD_TIMEOUT = 45.0
43
+ INSTALL_TIMEOUT = 180.0
44
+
45
+ # 本机是 Windows x64,包里是 abi3 通用轮子;按顺序挑第一个匹配的。
46
+ WHEEL_TAGS = ("py3-none-win_amd64",)
47
+
48
+ ENV_PYPI_JSON = "MDRTREADTOOL_PYPI_JSON"
49
+ ENV_UPDATE_DIR = "MDRTREADTOOL_UPDATE_DIR"
50
+ ENV_NO_UPDATE = "MDRTREADTOOL_NO_UPDATE"
51
+ ENV_FORCE = "MDRTREADTOOL_UPDATE_FORCE"
52
+ ENV_VERBOSE = "MDRTREADTOOL_UPDATE_VERBOSE"
53
+
54
+ DEFAULT_PYPI_JSON = "https://pypi.org/pypi/%s/json" % PROJECT
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # 小工具
59
+ # ---------------------------------------------------------------------------
60
+ def _truthy(name):
61
+ return os.environ.get(name, "").strip().lower() in ("1", "yes", "true", "on")
62
+
63
+
64
+ def _default_say(text):
65
+ """往控制台写一行;无 stdout(如 pythonw)时安静地放弃。"""
66
+ try:
67
+ sys.stdout.write(text + "\n")
68
+ sys.stdout.flush()
69
+ except Exception: # noqa: BLE001
70
+ pass
71
+
72
+
73
+ def _log(say, verbose, text):
74
+ if verbose:
75
+ say(text)
76
+
77
+
78
+ def _short(exc):
79
+ """把异常压成一句短的中文原因。"""
80
+ s = str(exc) or type(exc).__name__
81
+ low = s.lower()
82
+ if isinstance(exc, TimeoutError) or "timed out" in low or "timeout" in low:
83
+ return "超时"
84
+ if ("refused" in low or "unreachable" in low or "getaddrinfo" in low
85
+ or "name or service" in low or "temporary failure" in low):
86
+ return "无法连接"
87
+ return s[:80]
88
+
89
+
90
+ def _parse_version(text):
91
+ """把版本号压成可比较的数字元组;取不到数字段时返回空元组。"""
92
+ m = re.match(r"^(\d+(?:\.\d+)*)", str(text or "").strip())
93
+ return tuple(int(x) for x in m.group(1).split(".")) if m else ()
94
+
95
+
96
+ def _newer(candidate, current):
97
+ return _parse_version(candidate) > _parse_version(current)
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # 状态文件
102
+ # ---------------------------------------------------------------------------
103
+ def update_dir():
104
+ """状态文件与下载缓存所在目录。"""
105
+ d = os.environ.get(ENV_UPDATE_DIR)
106
+ if not d:
107
+ d = os.path.join(os.path.expanduser("~"), "treadtools", "updates")
108
+ return d
109
+
110
+
111
+ def _state_path():
112
+ return os.path.join(update_dir(), "state.json")
113
+
114
+
115
+ def _load_state():
116
+ try:
117
+ with open(_state_path(), "r", encoding="utf-8") as f:
118
+ data = json.load(f)
119
+ return data if isinstance(data, dict) else {}
120
+ except Exception: # noqa: BLE001
121
+ return {}
122
+
123
+
124
+ def _save_state(state):
125
+ try:
126
+ os.makedirs(update_dir(), exist_ok=True)
127
+ tmp = _state_path() + ".tmp"
128
+ with open(tmp, "w", encoding="utf-8") as f:
129
+ json.dump(state, f, ensure_ascii=False, indent=2)
130
+ os.replace(tmp, _state_path())
131
+ except Exception: # noqa: BLE001
132
+ pass
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # 版本探测
137
+ # ---------------------------------------------------------------------------
138
+ def installed_version():
139
+ """返回本机已安装的发行版版本号;不是 pip 安装(或取不到)时返回 None。"""
140
+ try:
141
+ from importlib.metadata import version as _version # Python 3.8+
142
+ return _version(PROJECT)
143
+ except Exception: # noqa: BLE001
144
+ pass
145
+ try:
146
+ from . import __version__
147
+ return __version__
148
+ except Exception: # noqa: BLE001
149
+ return None
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # 联网
154
+ # ---------------------------------------------------------------------------
155
+ def _http_get(url, timeout):
156
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
157
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
158
+ return resp.read()
159
+
160
+
161
+ def _download(url, dest, timeout):
162
+ """流式下载到 dest;先写 .part 再改名,中途中断不会留下半个可用文件。"""
163
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
164
+ deadline = time.time() + timeout
165
+ tmp = dest + ".part"
166
+ try:
167
+ with urllib.request.urlopen(req, timeout=min(15.0, timeout)) as resp, \
168
+ open(tmp, "wb") as fh:
169
+ while True:
170
+ if time.time() > deadline:
171
+ raise TimeoutError("download timed out")
172
+ chunk = resp.read(65536)
173
+ if not chunk:
174
+ break
175
+ fh.write(chunk)
176
+ os.replace(tmp, dest)
177
+ except BaseException:
178
+ try:
179
+ os.remove(tmp)
180
+ except Exception: # noqa: BLE001
181
+ pass
182
+ raise
183
+ return dest
184
+
185
+
186
+ def _pick_wheel(info, version):
187
+ """从 PyPI JSON 里挑出适用于本机的轮子,返回 (文件名, 下载地址)。"""
188
+ cands = []
189
+ for item in (info.get("urls") or []):
190
+ name = item.get("filename") or ""
191
+ if item.get("packagetype") and item["packagetype"] != "bdist_wheel":
192
+ continue
193
+ if name.endswith(".whl") and item.get("url"):
194
+ cands.append((name, item["url"]))
195
+ if not cands:
196
+ for item in ((info.get("releases") or {}).get(version) or []):
197
+ name = item.get("filename") or ""
198
+ if name.endswith(".whl") and item.get("url"):
199
+ cands.append((name, item["url"]))
200
+ for tag in WHEEL_TAGS:
201
+ for name, url in cands:
202
+ if tag in name:
203
+ return name, url
204
+ return (cands[0] if cands else (None, None))
205
+
206
+
207
+ def _check(current):
208
+ """查询 PyPI;有新版本返回 dict,否则返回 None。网络异常直接抛出,由调用方兜住。"""
209
+ url = os.environ.get(ENV_PYPI_JSON) or DEFAULT_PYPI_JSON
210
+ info = json.loads(_http_get(url, CHECK_TIMEOUT).decode("utf-8"))
211
+ latest = str(((info.get("info") or {}).get("version")) or "").strip()
212
+ if not latest or not _newer(latest, current):
213
+ return None
214
+ name, wheel_url = _pick_wheel(info, latest)
215
+ return {"version": latest, "wheel": name, "url": wheel_url}
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # 安装
220
+ # ---------------------------------------------------------------------------
221
+ def _pip_install(path, timeout):
222
+ """用 pip 装本地轮子,返回 (返回码, 合并后的输出文本)。
223
+
224
+ 用 --no-index 是因为轮子已经在本地:既不必再访问索引,也避免取到别的版本。
225
+ --force-reinstall 保证即使版本号相同也能覆盖。
226
+ """
227
+ exe = sys.executable or shutil.which("python") or "python"
228
+ cmd = [exe, "-m", "pip", "install",
229
+ "--upgrade", "--force-reinstall", "--no-deps", "--no-index",
230
+ "--disable-pip-version-check", "--no-input", path]
231
+ kwargs = {}
232
+ if os.name == "nt":
233
+ kwargs["creationflags"] = 0x08000000 # CREATE_NO_WINDOW
234
+ try:
235
+ proc = subprocess.run(cmd, stdin=subprocess.DEVNULL,
236
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
237
+ timeout=timeout, **kwargs)
238
+ return proc.returncode, (proc.stdout or b"").decode("utf-8", "replace")
239
+ except subprocess.TimeoutExpired:
240
+ return -1, "pip 安装超时"
241
+ except Exception as exc: # noqa: BLE001
242
+ return -2, "%s: %s" % (type(exc).__name__, exc)
243
+
244
+
245
+ # ---------------------------------------------------------------------------
246
+ # 主流程
247
+ # ---------------------------------------------------------------------------
248
+ def summary():
249
+ """给 ``--info`` 用的一行状态说明。"""
250
+ if getattr(sys, "frozen", False):
251
+ return "单文件 exe 运行环境,不自动更新(exe 靠替换新版本升级)"
252
+ current = installed_version() or "未知"
253
+ state = _load_state()
254
+ last = state.get("last_check")
255
+ if last:
256
+ try:
257
+ when = time.strftime("%Y-%m-%d %H:%M", time.localtime(float(last)))
258
+ except Exception: # noqa: BLE001
259
+ when = "?"
260
+ else:
261
+ when = "从未"
262
+ text = "当前 %s;上次联网检查 %s" % (current, when)
263
+ err = state.get("last_error")
264
+ if err:
265
+ text += ";上次异常 %s" % str(err)[:60]
266
+ return text
267
+
268
+
269
+ def maybe_update(say=None, force=False):
270
+ """启动时调用一次。
271
+
272
+ 永远不抛异常。返回状态 dict:
273
+ status ∈ updated / available / uptodate / throttled / offline
274
+ / disabled / frozen / unknown
275
+ current 本机原版本号
276
+ version 本次发现的可用新版本号(若有)
277
+ detail 失败原因(若有)
278
+ """
279
+ say = say or _default_say
280
+ verbose = _truthy(ENV_VERBOSE) or force
281
+ st = {"status": "unknown", "current": None, "version": None, "detail": ""}
282
+
283
+ # 单文件 exe(PyInstaller)环境一律不自动更新:exe 自带 Python 运行时与依赖,
284
+ # 用 pip 去升级既没有意义,也可能把环境搞乱。exe 的升级方式就是换一个新的 exe。
285
+ if getattr(sys, "frozen", False):
286
+ st["status"] = "frozen"
287
+ st["detail"] = "单文件 exe 运行环境"
288
+ _log(say, verbose, "[更新] 当前是单文件 exe 运行环境,自动更新只对 pip 安装版生效,已跳过。")
289
+ return st
290
+
291
+ if _truthy(ENV_NO_UPDATE):
292
+ st["status"] = "disabled"
293
+ _log(say, verbose, "[更新] 已关闭自动更新。")
294
+ return st
295
+
296
+ current = installed_version()
297
+ st["current"] = current
298
+ if not current:
299
+ # 不是 pip 安装(例如手工拷贝的目录),不擅自升级
300
+ _log(say, verbose, "[更新] 未检测到已安装的发行版信息,跳过自动更新。")
301
+ return st
302
+
303
+ state = _load_state()
304
+ now = time.time()
305
+ if not (force or _truthy(ENV_FORCE)) and \
306
+ (now - float(state.get("last_check") or 0)) < CHECK_INTERVAL:
307
+ st["status"] = "throttled"
308
+ _log(say, verbose, "[更新] 距上次检查不足 %d 小时,本次跳过。"
309
+ % (CHECK_INTERVAL // 3600))
310
+ return st
311
+
312
+ try:
313
+ found = _check(current)
314
+ except Exception as exc: # noqa: BLE001
315
+ state["last_check"] = now
316
+ state["last_error"] = "%s: %s" % (type(exc).__name__, exc)
317
+ _save_state(state)
318
+ st["status"] = "offline"
319
+ st["detail"] = state["last_error"]
320
+ _log(say, verbose, "[更新] 检查更新失败(%s),已跳过,本次用 %s 启动。"
321
+ % (_short(exc), current))
322
+ return st
323
+
324
+ state["last_check"] = now
325
+ state.pop("last_error", None)
326
+ _save_state(state)
327
+
328
+ if not found:
329
+ st["status"] = "uptodate"
330
+ _log(say, verbose, "[更新] 已是最新版本 %s。" % current)
331
+ return st
332
+
333
+ latest = found["version"]
334
+ st["version"] = latest
335
+
336
+ if not found.get("url"):
337
+ st["status"] = "available"
338
+ st["detail"] = "PyPI 上没有适用于本机的轮子"
339
+ state["last_error"] = st["detail"]
340
+ _save_state(state)
341
+ say("[更新] 发现新版本 %s,但 PyPI 上没有适用于本机的轮子,已跳过。" % latest)
342
+ return st
343
+
344
+ say("[更新] 发现新版本 %s(当前 %s),正在通过 pip 更新 ..." % (latest, current))
345
+
346
+ dest = os.path.join(update_dir(), found["wheel"])
347
+ try:
348
+ os.makedirs(update_dir(), exist_ok=True)
349
+ _download(found["url"], dest, DOWNLOAD_TIMEOUT)
350
+ except Exception as exc: # noqa: BLE001
351
+ st["status"] = "available"
352
+ st["detail"] = "%s: %s" % (type(exc).__name__, exc)
353
+ state["last_error"] = st["detail"]
354
+ _save_state(state)
355
+ say("[更新] 新版本 %s 下载失败(%s),已跳过,本次仍用 %s 启动。"
356
+ % (latest, _short(exc), current))
357
+ return st
358
+
359
+ rc, out = _pip_install(dest, INSTALL_TIMEOUT)
360
+ if rc != 0:
361
+ st["status"] = "available"
362
+ st["detail"] = (out or "rc=%s" % rc)[-400:]
363
+ state["last_error"] = st["detail"]
364
+ _save_state(state)
365
+ say("[更新] 新版本 %s 安装失败,已跳过。可手动执行:"
366
+ "pip install --upgrade %s" % (latest, PROJECT))
367
+ _log(say, verbose, "[更新] pip 输出:%s" % (out or "").strip())
368
+ return st
369
+
370
+ import importlib
371
+ importlib.invalidate_caches()
372
+ after = installed_version()
373
+ st["installed"] = after
374
+ try:
375
+ os.remove(dest) # 装完即清,避免下次重复占用
376
+ except Exception: # noqa: BLE001
377
+ pass
378
+
379
+ if after and _parse_version(after) >= _parse_version(latest):
380
+ st["status"] = "updated"
381
+ say("[更新] 更新完成:%s → %s,本次启动即为新版本。" % (current, after))
382
+ else:
383
+ st["status"] = "available"
384
+ st["detail"] = "安装完成但版本未变(当前仍报 %s)" % after
385
+ state["last_error"] = st["detail"]
386
+ _save_state(state)
387
+ say("[更新] 更新似乎未生效(当前仍为 %s)。可手动执行:"
388
+ "pip install --upgrade %s" % (after, PROJECT))
389
+ return st
Binary file
Binary file
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdrtreadtool
3
+ Version: 2.0.0
4
+ Summary: 医疗器械警戒趋势分析工具:把分母表与不良事件表一键生成控制图 / 直方图 / 帕累托图(编译发行版,Windows x64 / Python 3.8+)
5
+ Author: sysucai
6
+ Maintainer: sysucai
7
+ License-Expression: LicenseRef-Proprietary
8
+ Keywords: medical-device,vigilance,SPC,control-chart,p-chart,pareto,histogram,trend-analysis,医疗器械,警戒
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Win32 (MS Windows)
11
+ Classifier: Intended Audience :: Healthcare Industry
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Natural Language :: Chinese (Simplified)
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: Implementation :: CPython
24
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
25
+ Classifier: Topic :: Scientific/Engineering :: Visualization
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy
30
+ Requires-Dist: pandas
31
+ Requires-Dist: matplotlib
32
+ Requires-Dist: openpyxl
33
+ Provides-Extra: image
34
+ Requires-Dist: Pillow>=9.0; extra == "image"
35
+ Provides-Extra: xls
36
+ Requires-Dist: xlrd>=2.0; extra == "xls"
37
+ Provides-Extra: all
38
+ Requires-Dist: Pillow>=9.0; extra == "all"
39
+ Requires-Dist: xlrd>=2.0; extra == "all"
40
+ Dynamic: license-file
41
+
42
+ # mdrtreadtool · 医疗器械警戒趋势分析工具
43
+
44
+ 把「**分母表**(使用基数表)」与「**不良事件表**」两张 Excel 表,一键生成医疗器械警戒趋势报告所需的**三类统计图表**——控制图、直方图、帕累托图。
45
+
46
+ 面向医疗器械注册人 / 不良事件监测人员,依据《医疗器械趋势报告撰写指南》(试行)编制:控制图支持 P / NP / U / C 四种常规控制图与 G 图(稀有事件几何控制图),直方图按「型号比较」口径拆解故障表现 / 伤害表现,帕累托图支持两个时期对比。
47
+
48
+ 本发行版在 PyPI 上提供的是**已编译**的扩展模块(Cython + CPython 稳定 ABI / abi3),**不含 `.py` 业务源码**,与官方 exe 版行为完全一致。
49
+
50
+ ---
51
+
52
+ ## 系统要求
53
+
54
+ | 项目 | 要求 |
55
+ |---|---|
56
+ | 操作系统 | Windows 10 / 11 / Server(**仅 64 位**) |
57
+ | Python | **3.8 及以上**(CPython 3.8 ~ 3.14 均可,同一个轮子通用) |
58
+ | 运行依赖 | `numpy`、`pandas`、`matplotlib`、`openpyxl`(`pip` 会自动安装) |
59
+
60
+ > 轮子的文件名是 `mdrtreadtool-2.0.0-py3-none-win_amd64.whl`。因为随附的 `.pyd` 是以 **CPython 稳定 ABI(abi3)** 编译的,只依赖系统自带的 `python3.dll`,所以一个轮子可在 3.8 ~ 3.14 上直接加载,不必按版本分别编译或分别下载。
61
+
62
+ ---
63
+
64
+ ## 安装
65
+
66
+ ```bash
67
+ python -m pip install mdrtreadtool
68
+ ```
69
+
70
+ 升级到最新版:
71
+
72
+ ```bash
73
+ python -m pip install --upgrade mdrtreadtool
74
+ ```
75
+
76
+ 可选组件(不装也能用,程序会自动降级):
77
+
78
+ ```bash
79
+ python -m pip install "mdrtreadtool[image]" # Pillow:抗锯齿圆角按钮 + 帮助插图
80
+ python -m pip install "mdrtreadtool[xls]" # xlrd:读取 Excel 97-2003 的 .xls 旧格式
81
+ python -m pip install "mdrtreadtool[all]" # 以上全部
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 启动
87
+
88
+ ```bash
89
+ mdrtreadtool
90
+ ```
91
+
92
+ 图形界面会直接打开。也可以在 Python 里以模块方式启动:
93
+
94
+ ```bash
95
+ python -m mdrtreadtool
96
+ ```
97
+
98
+ ### 命令行参数
99
+
100
+ | 命令 | 作用 |
101
+ |---|---|
102
+ | `mdrtreadtool` | 启动图形界面 |
103
+ | `mdrtreadtool --selftest` | 环境自检:检查 Python / 依赖 / 中文字体 / 绘图内核,弹窗显示并写出报告 |
104
+ | `mdrtreadtool --selftest --quiet` | 同上,只写报告、不弹窗(适合脚本批量检查) |
105
+ | `mdrtreadtool --info` | 不启动界面,只打印模块信息并跑一次纯函数自检(排查问题时最先用这个) |
106
+ | `mdrtreadtool --version` | 打印版本号 |
107
+ | `mdrtreadtool --no-update` | 本次启动不做自动更新 |
108
+
109
+ > 程序首次启动时会弹出一个「用户登记」窗口。**直接关闭即可**——不登记不影响任何功能;登记后仅用于接收版本更新通知。
110
+
111
+ ### 自动更新
112
+
113
+ 程序启动时会自动检查 PyPI 上有没有新版本,有的话**通过 pip 自动升级,本次启动即用上新版本**。
114
+ 整个过程带超时保护:**查询、下载、安装三步各自的超时一到就放弃**,绝不会卡住或影响启动。
115
+
116
+ - 正常情况**完全静默**;只有真的发生了更新(或更新失败需要你手动处理)才会打印一行提示。
117
+ - 为减少等待,联网检查**最长每 6 小时一次**;其余启动不再联网。
118
+ - 想看看它到底干了什么:`set MDRTREADTOOL_UPDATE_VERBOSE=1`,或直接 `mdrtreadtool --info`(末行会显示上次检查时间)。
119
+ - 想临时/永久关掉:加参数 `--no-update`,或设环境变量 `MDRTREADTOOL_NO_UPDATE=1`。
120
+ - 内网镜像 / 离线自测:`set MDRTREADTOOL_PYPI_JSON=<你的 JSON 地址>`。
121
+
122
+ > **注意**:自动更新**只对 `pip install` 安装的版本生效**。
123
+ > 源码运行(`python gen.py`)不涉及;单文件 exe 版**完全不做自动更新**——exe 自带
124
+ > Python 运行时与依赖,升级方式就是替换一个新的 exe,程序不会去执行 pip。
125
+ > 换句话说,只有在 pip 环境里跑 `mdrtreadtool` 的用户会被自动更新。
126
+
127
+ ---
128
+
129
+ ## 数据准备
130
+
131
+ 导入一个 `.xlsx` 文件,内含两张工作表:**第一张 = 不良事件表**,**第二张 = 分母表**。
132
+ (工作表名含「事件 / 不良」或「分母 / 基数」时按名字匹配,否则按上述顺序取;只有一张表时按列判断归属。没有分母表时,「发生率」类指标不可用。)
133
+
134
+ ### 分母表(使用基数表)—— 固定 6 列
135
+
136
+ | 注册证编号/曾用注册证编号 | 型号 | 规格 | 产品批号 | 月份 | 使用基数 |
137
+ |---|---|---|---|---|---|
138
+
139
+ - 「月份」支持 `2026-01` / `2026/01` / `2026-01-05`,会自动取所属月;
140
+ - 一行 = 某注册证下某型号 / 规格 / 批号在某月的使用基数。
141
+
142
+ ### 不良事件表 —— 至少包含
143
+
144
+ | 报告编码 | 事件发生日期 | 注册证编号/曾用注册证编号 |
145
+ |---|---|---|
146
+
147
+ 可选列:`型号`、`规格`、`产品批号`、`器械故障表现`、`伤害表现`(用于直方图与帕累托图的分组维度)。
148
+
149
+ > 界面上点「下载示例数据」可导出一份符合上述约定的样例文件,照着填即可。
150
+
151
+ **分母对不齐时**:选「发生率」出图时,若某些取值在分母表中找不到对应记录,程序会先弹窗告诉你具体缺哪些维度、涉及多少条事件,由你决定「忽略这些事件继续生成」还是「取消本次生成」。
152
+
153
+ ---
154
+
155
+ ## 三类图表
156
+
157
+ ### 1. 控制图
158
+
159
+ 按月份 / 季度观察风险表现是否超出控制限。
160
+
161
+ | 图型 | 适用数据 | 控制限 |
162
+ |---|---|---|
163
+ | **P 图** | 不良率(二项) | `p̄ ± 3·√( p̄(1-p̄) / nᵢ )` |
164
+ | **NP 图** | 不良数(二项) | 同上 × `nᵢ` |
165
+ | **U 图** | 单位缺陷数(泊松) | `ū ± 3·√( ū / nᵢ )` |
166
+ | **C 图** | 缺陷数(泊松) | 同上 × `nᵢ` |
167
+ | **G 图** | 稀有事件·事件间隔(几何分布) | `CL = ln2·ḡ`、`UCL = ḡ + 3·√(ḡ(ḡ+1))`、`LCL ≡ 0` |
168
+
169
+ - 中心线 `p̄ = Σ风险表现数 / Σ使用基数`(加权汇集,GB/T 17989.2—2020 口径),图下脚注给出与「简单平均」口径的实际差异;
170
+ - 控制限随各期分母 `nᵢ` 变化呈锯齿状;仅当分母固定时退化为水平直线;
171
+ - G 图方向与 P 图相反:间隔越短 = 事件越密 = 风险升高;配合 1.5×IQR 参考线(探索性离群参考线,**不是**控制限)。
172
+
173
+ ### 2. 直方图(型号比较)
174
+
175
+ 按型号 / 注册证编号分组,拆解故障表现 / 伤害表现作对比(依据《指南》附录 1「直方图法·型号比较」)。
176
+
177
+ - 指标支持「发生率(需分母表)」「数量」「构成比」,默认发生率;
178
+ - 发生率 = 风险表现数 ÷ 使用基数 × 100%;构成比 = 该组该类风险表现数 ÷ 该组全部风险表现数 × 100%(分母不因「展示前 N 组」截断)。
179
+
180
+ ### 3. 帕累托图
181
+
182
+ 按月份 / 季度(或故障 / 伤害表现)排序并叠加累计占比,默认对比「上期 → 本期」两期。
183
+
184
+ - 期次可按粒度选择,也可用「指定时间段」自定两段日期区间(如 `20240103–20240630` vs `20240701–20241231`);
185
+ - 支持一键导出帕累托数据表(.xlsx)。
186
+
187
+ ### 附带能力
188
+
189
+ - **参数自动记忆**:所有界面参数保存在 `~/treadtools/setting.json`,下次启动原样回填;
190
+ - **数值显示统一口径**:可在「总体设置」里切换百分数 / 小数、自动适配有效数字或手动指定位数,改动即时重绘;
191
+ - **数据校验报告**:导入时给出 6 节校验(概况 / 必要列完整性 / 分子表覆盖范围 / 分母表覆盖范围 / 数据质量 / 数据剔除),可导出为 Markdown;
192
+ - **统计表与图完全一致**:控制图 / 直方图可「显示表格」「导出表格」,表格口径与图中参考序列严格对齐;
193
+ - **容错**:自动绕过 openpyxl 3.1.0~3.1.2 解析 `autoFilter` 的已知缺陷,坏文件里的正常数据照样读得出来。
194
+
195
+ ---
196
+
197
+ ## 常见问题
198
+
199
+ **Q:装完执行 `mdrtreadtool` 报「不是有效的 Win32 应用程序」?**
200
+ A:位数不匹配。本包只提供 64 位版本,请使用 **64 位** Python。
201
+
202
+ **Q:提示缺少 numpy / pandas?**
203
+ A:`pip` 一般会自动装。若公司网络无法直连 PyPI,请配置内网镜像后执行
204
+ `python -m pip install numpy pandas matplotlib openpyxl`,或改用 exe 单文件版(免 Python 环境)。
205
+
206
+ **Q:图里的中文变成方框?**
207
+ A:执行 `mdrtreadtool --selftest`,报告里会列出检测到的中文字体;若一个都没检测到,请安装「微软雅黑 / 黑体」等常见中文字体。
208
+
209
+ **Q:能否拿到源码做二次开发?**
210
+ A:不能。本包在 PyPI 上分发的是**编译后的二进制模块**,不包含业务源码,授权为专有授权(见下)。有二次开发或定制需求请联系作者。
211
+
212
+ **Q:`~/treadtools/setting.json` 是什么?**
213
+ A:界面参数(期次、粒度、字段映射、显示口径等)的自动保存文件,删掉即可恢复默认,不影响数据。
214
+
215
+ ---
216
+
217
+ ## 授权
218
+
219
+ **专有授权(Proprietary License)—— 非开源。**
220
+
221
+ Copyright (C) 2026 sysucai. All rights reserved.
222
+
223
+ 允许:在授权范围内**免费使用**本软件(含内部业务使用)。
224
+ 禁止:反向工程、反编译、拆解二进制、再分发、转售、以本软件或其衍生品提供商业服务,以及移除任何版权或授权声明。
225
+
226
+ 完整条款见随包附带的 [`LICENSE`](LICENSE)。下载、安装或使用本软件即视为已阅读并同意该协议。
227
+
228
+ ---
229
+
230
+ ## 说明
231
+
232
+ - 医疗器械警戒判定涉及法规责任,本工具输出的图表与统计量仅供参考,**最终结论应由具备相应资质的专业人员判断**。
233
+ - 报告中所有方法学口径(控制图原理、基线口径、判异规则)均写在程序「帮助文件」页与图下脚注中,可逐条核对。
@@ -0,0 +1,12 @@
1
+ mdrtreadtool/__init__.py,sha256=z_uBGWwOzZ5T_5COSgu96tW3BbW_BzX_Zoz3oBQm-0E,1532
2
+ mdrtreadtool/__main__.py,sha256=yse7Auhq1aXWGT5Vu5zgq7p698aXMFx0HGpe2uBSD0I,167
3
+ mdrtreadtool/_loader.py,sha256=7U9KcoE7L9HsZ5NLk00xBFfw_2zv-u5MzooIKz4PX10,13423
4
+ mdrtreadtool/_update.py,sha256=i9OpN2PrEbFPn8OXM0tdBUSRI8gKE79RKRqom9p134c,15382
5
+ mdrtreadtool/chart_tool.ico,sha256=QSkGjDbY2BIoFsA45Akc0aIoesObVkKJltvD46HfVQU,381038
6
+ mdrtreadtool/chart_tool.pyd,sha256=MyM_GGGTspoG61vy3nSWbDzqz5WL6zDPhA3mu3C76D0,2580992
7
+ mdrtreadtool-2.0.0.dist-info/licenses/LICENSE,sha256=cWhR4QS0Pm_q2sPM9IIuIyIBhQu64mA3g65W0gXGG4w,7957
8
+ mdrtreadtool-2.0.0.dist-info/METADATA,sha256=K-nAmiydJQ9188cKoXX-8_Yb8VKOwJd9TSn_IMzhDQU,11776
9
+ mdrtreadtool-2.0.0.dist-info/WHEEL,sha256=hVx9elvUDfBjRmbl8JwIcXXikst35RZTZK9nspfI_28,98
10
+ mdrtreadtool-2.0.0.dist-info/entry_points.txt,sha256=YnX_BzkubLaip8X9T6zPmHeLV8cME8kHP2lZhRJ5kas,59
11
+ mdrtreadtool-2.0.0.dist-info/top_level.txt,sha256=teSwJorMMlStOWQUPY7nfpfgH105hq1BijqrcdOTwxc,13
12
+ mdrtreadtool-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mdrtreadtool = mdrtreadtool._loader:main
@@ -0,0 +1,156 @@
1
+ 专有软件许可协议
2
+ PROPRIETARY SOFTWARE LICENSE AGREEMENT
3
+
4
+ 软件名称:mdrtreadtool —— 医疗器械警戒趋势分析工具
5
+ 版本:2.0.0
6
+ 著作权人(许可方):sysucai
7
+ 发布日期:2026-10-12
8
+
9
+ 本协议同时提供中文与英文文本。中、英文文本如有歧义,以中文文本为准。
10
+ This Agreement is provided in both Chinese and English. In case of any
11
+ discrepancy, the Chinese text shall prevail.
12
+
13
+
14
+ 一、定义
15
+
16
+ 1.1 「本软件」指 mdrtreadtool 软件包及其全部组成文件,包括但不限于已编译的扩展
17
+ 模块(chart_tool.pyd)、图标资源、文档、示例数据与随附脚本。
18
+ 1.2 「许可方」指本软件的著作权人 sysucai。
19
+ 1.3 「被许可方」指通过合法途径获取、安装或使用本软件的自然人或法人。
20
+
21
+
22
+ 二、授权范围
23
+
24
+ 2.1 在遵守本协议全部条款的前提下,许可方授予被许可方一项**非独占、不可转让、
25
+ 不可再许可**的普通使用许可,允许被许可方:
26
+ (a) 在其自身业务(含企业内部业务)中安装、运行本软件;
27
+ (b) 使用本软件生成的图表、统计数据与报告,用于内部质量分析、法规报告编制
28
+ 等正当用途。
29
+
30
+ 2.2 被许可方**无需**为上述使用支付许可费用。
31
+
32
+
33
+ 三、限制
34
+
35
+ 被许可方**不得**实施下列行为,亦不得允许或协助第三方实施:
36
+
37
+ 3.1 对本软件进行反向工程、反编译、反汇编、解密,或以其他方式试图获取其源代码、
38
+ 内部算法或未公开的实现细节;
39
+ 3.2 复制、发布、分发、出租、出借、出售、转让本软件或其任何部分,或将本软件
40
+ 上传至任何公开的软件仓库、网盘、论坛或其他信息分享平台;
41
+ 3.3 移除、遮蔽、篡改本软件中的任何著作权声明、许可声明、版本标识或作者署名;
42
+ 3.4 将本软件或其修改版本用于提供商业服务(包括但不限于 SaaS、代做报告、
43
+ 二次销售),无论是否直接收费;
44
+ 3.5 将本软件用于任何违反中华人民共和国法律法规的用途;
45
+ 3.6 以本软件为基础开发实质功能相同或近似的竞争性产品。
46
+
47
+
48
+ 四、权利保留
49
+
50
+ 4.1 本软件系授权使用,而非出售。本协议未明示授予的一切权利,均由许可方保留。
51
+ 4.2 本软件的全部知识产权(包括但不限于著作权、商标权、商业秘密)均归许可方
52
+ 所有,不因本协议的签署或履行而发生转移。
53
+
54
+
55
+ 五、第三方组件
56
+
57
+ 5.1 本软件在运行过程中依赖若干第三方开源组件(例如 NumPy、pandas、matplotlib、
58
+ openpyxl、Pillow、xlrd 等)。这些组件由各自的著作权人依照其自身的开源许可
59
+ 协议(如 BSD、MIT、PSF 等)授权,**不适用**本协议的专有条款。
60
+ 5.2 被许可方如分发本软件所在的 Python 环境,应自行遵守上述第三方组件的许可要求。
61
+
62
+
63
+ 六、无担保
64
+
65
+ 6.1 本软件按「现状」提供,不附带任何形式的明示或默示担保,包括但不限于对
66
+ 适销性、特定用途适用性及不侵权的担保。
67
+ 6.2 许可方不保证本软件运行无中断、无错误,亦不保证其输出的统计结果、图表或
68
+ 结论满足任何特定监管要求。
69
+
70
+
71
+ 七、责任限制
72
+
73
+ 7.1 在适用法律允许的最大范围内,许可方不对因使用或无法使用本软件而产生的
74
+ 任何直接、间接、附带、特殊、惩罚性或后果性损害(包括但不限于数据丢失、
75
+ 业务中断、利润损失、监管处罚)承担赔偿责任,无论其是否已被告知该等
76
+ 损害的可能性。
77
+ 7.2 本软件输出的图表与统计量仅供参考。医疗器械警戒判定涉及法规责任,最终
78
+ 结论应由具备相应资质的专业人员独立作出。
79
+
80
+
81
+ 八、协议的终止
82
+
83
+ 8.1 被许可方违反本协议任一条款时,本协议项下的授权**自动终止**,无需许可方
84
+ 另行通知。
85
+ 8.2 授权终止后,被许可方应立即停止使用并销毁本软件的全部副本。
86
+
87
+
88
+ 九、适用法律与争议解决
89
+
90
+ 9.1 本协议的订立、效力、解释、履行及争议解决,均适用中华人民共和国法律。
91
+ 9.2 因本协议产生的争议,双方应友好协商解决;协商不成的,任何一方均可向许可方
92
+ 住所地有管辖权的人民法院提起诉讼。
93
+
94
+
95
+ 十、其他
96
+
97
+ 10.1 下载、安装、复制或以任何方式使用本软件,即视为被许可方已阅读、理解并
98
+ 同意接受本协议全部条款的约束。
99
+ 10.2 若被许可方不同意本协议的任何条款,请立即停止安装与使用,并删除本软件的
100
+ 全部副本。
101
+
102
+
103
+ ────────────────────────────────────────────────────────────────────────────
104
+ ENGLISH TEXT
105
+
106
+ Software: mdrtreadtool — Medical Device Vigilance Trend Analysis Tool
107
+ Copyright (C) 2026 sysucai. All rights reserved.
108
+
109
+ 1. GRANT. Subject to the terms of this Agreement, the Licensor grants you a
110
+ non-exclusive, non-transferable, non-sublicensable license to install and
111
+ use this software for your own internal business purposes, free of charge.
112
+
113
+ 2. RESTRICTIONS. You may NOT: (a) reverse engineer, decompile, disassemble or
114
+ otherwise attempt to derive the source code, internal algorithms or
115
+ non-public implementation details of the software; (b) copy, publish,
116
+ distribute, rent, lease, sell, transfer or upload the software or any part
117
+ of it to any public repository, file-sharing service, forum or other
118
+ information-sharing platform; (c) remove, obscure or alter any copyright,
119
+ license, version or attribution notice; (d) use the software or any
120
+ derivative thereof to provide commercial services, including but not
121
+ limited to SaaS offerings, report-writing services or resale, whether or
122
+ not a fee is charged; (e) use the software for any purpose prohibited by
123
+ the laws of the People's Republic of China; or (f) develop a competing
124
+ product with substantially similar functionality based on the software.
125
+
126
+ 3. RESERVATION. The software is licensed, not sold. All rights not expressly
127
+ granted are reserved by the Licensor. All intellectual property rights in
128
+ the software remain with the Licensor and are not transferred.
129
+
130
+ 4. THIRD-PARTY COMPONENTS. The software depends on third-party open-source
131
+ components (e.g. NumPy, pandas, matplotlib, openpyxl, Pillow, xlrd), which
132
+ are licensed under their own respective licenses and are NOT governed by
133
+ this Agreement.
134
+
135
+ 5. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
136
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
137
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
138
+
139
+ 6. LIMITATION OF LIABILITY. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW,
140
+ THE LICENSOR SHALL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
141
+ SPECIAL, PUNITIVE OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATING TO
142
+ THE USE OF OR INABILITY TO USE THE SOFTWARE.
143
+
144
+ 7. TERMINATION. Your rights under this Agreement terminate automatically
145
+ without notice if you breach any of its terms.
146
+
147
+ 8. GOVERNING LAW. This Agreement shall be governed by and construed in
148
+ accordance with the laws of the People's Republic of China.
149
+
150
+ 9. ACCEPTANCE. By downloading, installing, copying or otherwise using this
151
+ software, you acknowledge that you have read, understood and agree to be
152
+ bound by this Agreement. If you do not agree, you must stop using the
153
+ software and delete all copies immediately.
154
+
155
+ ────────────────────────────────────────────────────────────────────────────
156
+ SPDX-License-Identifier: LicenseRef-Proprietary
@@ -0,0 +1 @@
1
+ mdrtreadtool