clinical-scope 0.4.2__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.
- clinical_scope/__init__.py +15 -0
- clinical_scope/build_info/assemble_bundle.py +92 -0
- clinical_scope/build_info/generate_third_party_licenses.py +471 -0
- clinical_scope/config/__init__.py +1 -0
- clinical_scope/config/parsing.py +84 -0
- clinical_scope/constants.py +178 -0
- clinical_scope/dash_api/__init__.py +0 -0
- clinical_scope/dash_api/annotations/__init__.py +1 -0
- clinical_scope/dash_api/annotations/io.py +106 -0
- clinical_scope/dash_api/annotations/model.py +143 -0
- clinical_scope/dash_api/annotations/renderer.py +353 -0
- clinical_scope/dash_api/callbacks/__init__.py +79 -0
- clinical_scope/dash_api/callbacks/annotation_callbacks.py +1429 -0
- clinical_scope/dash_api/callbacks/data_callbacks.py +1215 -0
- clinical_scope/dash_api/callbacks/loop_callbacks.py +85 -0
- clinical_scope/dash_api/core_api.py +674 -0
- clinical_scope/dash_api/helper_api.py +52 -0
- clinical_scope/dash_api/io.py +39 -0
- clinical_scope/dash_api/styles.py +267 -0
- clinical_scope/dash_api/ui_components.py +154 -0
- clinical_scope/dash_api/validation.py +151 -0
- clinical_scope/database_options_parser.py +216 -0
- clinical_scope/database_options_xlsx.py +394 -0
- clinical_scope/datasource/__init__.py +20 -0
- clinical_scope/datasource/base.py +593 -0
- clinical_scope/datasource/formatting/__init__.py +1 -0
- clinical_scope/datasource/formatting/timezone.py +278 -0
- clinical_scope/datasource/inspection.py +225 -0
- clinical_scope/datasource/registry.py +222 -0
- clinical_scope/datasource/sources/eit/__init__.py +0 -0
- clinical_scope/datasource/sources/eit/find_load_format.py +355 -0
- clinical_scope/datasource/sources/eit/options.py +68 -0
- clinical_scope/datasource/sources/fluxmed_parameters/__init__.py +0 -0
- clinical_scope/datasource/sources/fluxmed_parameters/find_load_format.py +110 -0
- clinical_scope/datasource/sources/fluxmed_parameters/options.py +47 -0
- clinical_scope/datasource/sources/fluxmed_signals/__init__.py +0 -0
- clinical_scope/datasource/sources/fluxmed_signals/find_load_format.py +103 -0
- clinical_scope/datasource/sources/fluxmed_signals/options.py +31 -0
- clinical_scope/datasource/sources/mindray_respi_numerics/__init__.py +1 -0
- clinical_scope/datasource/sources/mindray_respi_numerics/find_load_format.py +84 -0
- clinical_scope/datasource/sources/mindray_respi_numerics/options.py +51 -0
- clinical_scope/datasource/sources/mindray_respi_waves/__init__.py +1 -0
- clinical_scope/datasource/sources/mindray_respi_waves/find_load_format.py +139 -0
- clinical_scope/datasource/sources/mindray_respi_waves/options.py +51 -0
- clinical_scope/datasource/sources/mindray_scope/__init__.py +1 -0
- clinical_scope/datasource/sources/mindray_scope/find_load_format.py +292 -0
- clinical_scope/datasource/sources/mindray_scope/options.py +27 -0
- clinical_scope/datasource/sources/other/__init__.py +0 -0
- clinical_scope/datasource/sources/other/find_load_format.py +391 -0
- clinical_scope/datasource/sources/other/options.py +45 -0
- clinical_scope/datasource/sources/philips_numerics/__init__.py +0 -0
- clinical_scope/datasource/sources/philips_numerics/find_load_format.py +48 -0
- clinical_scope/datasource/sources/philips_numerics/options.py +43 -0
- clinical_scope/datasource/sources/philips_waves/__init__.py +0 -0
- clinical_scope/datasource/sources/philips_waves/find_load_format.py +55 -0
- clinical_scope/datasource/sources/philips_waves/options.py +31 -0
- clinical_scope/datasource/sources/servo_u/__init__.py +0 -0
- clinical_scope/datasource/sources/servo_u/find_load_format.py +185 -0
- clinical_scope/datasource/sources/servo_u/options.py +31 -0
- clinical_scope/datasource/sources/syringe/__init__.py +0 -0
- clinical_scope/datasource/sources/syringe/find_load_format.py +68 -0
- clinical_scope/datasource/sources/syringe/options.py +42 -0
- clinical_scope/datasource/timing.py +37 -0
- clinical_scope/hover_formatters.py +101 -0
- clinical_scope/io/__init__.py +1 -0
- clinical_scope/io/file_utils.py +230 -0
- clinical_scope/io/paths.py +42 -0
- clinical_scope/logger_config.py +111 -0
- clinical_scope/signal_container.py +902 -0
- clinical_scope/wrapper.py +659 -0
- clinical_scope-0.4.2.dist-info/METADATA +243 -0
- clinical_scope-0.4.2.dist-info/RECORD +76 -0
- clinical_scope-0.4.2.dist-info/WHEEL +5 -0
- clinical_scope-0.4.2.dist-info/entry_points.txt +2 -0
- clinical_scope-0.4.2.dist-info/licenses/LICENSE +203 -0
- clinical_scope-0.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from clinical_scope.wrapper import (
|
|
2
|
+
batch_extract,
|
|
3
|
+
extract_datasource,
|
|
4
|
+
extract_patient,
|
|
5
|
+
load_annotations,
|
|
6
|
+
load_database_annotations,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"batch_extract",
|
|
11
|
+
"extract_datasource",
|
|
12
|
+
"extract_patient",
|
|
13
|
+
"load_annotations",
|
|
14
|
+
"load_database_annotations",
|
|
15
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Finish a built ClinicalScope bundle: copy static assets in, write license notices.
|
|
4
|
+
|
|
5
|
+
PyInstaller produces the raw bundle (the executable + ``_internal/``); this script
|
|
6
|
+
takes over from there and is **shared by both build entry points** -- ``build.sh``
|
|
7
|
+
(local) and the GitHub Actions build workflow -- so the asset manifest and the
|
|
8
|
+
license step live in exactly one place instead of drifting between a bash script
|
|
9
|
+
and a YAML file. Each caller still runs PyInstaller itself, because their output
|
|
10
|
+
paths differ; they converge here.
|
|
11
|
+
|
|
12
|
+
Steps:
|
|
13
|
+
|
|
14
|
+
1. Copy the static assets listed in ``ASSETS`` into the bundle root.
|
|
15
|
+
2. Regenerate ``THIRD_PARTY_LICENSES.txt`` via ``generate_third_party_licenses``.
|
|
16
|
+
|
|
17
|
+
Exit code: ``0`` when every asset copied and attribution is complete; non-zero
|
|
18
|
+
when an asset was missing or the license notice has unresolved entries. Callers
|
|
19
|
+
treat a non-zero exit as a *warning*, not a hard failure -- see
|
|
20
|
+
``docs/adr/0002-warn-only-on-unresolved-license-attribution.md``.
|
|
21
|
+
|
|
22
|
+
Usage::
|
|
23
|
+
|
|
24
|
+
python assemble_bundle.py --bundle-root <dir-with-_internal>
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import shutil
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from clinical_scope.build_info import generate_third_party_licenses as licenses
|
|
34
|
+
|
|
35
|
+
# Repo root: this file is at src/clinical_scope/build_info/assemble_bundle.py.
|
|
36
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
|
37
|
+
|
|
38
|
+
# (path relative to the repo root, "file" | "tree"). Trees drop the per-run cache.
|
|
39
|
+
ASSETS: list[tuple[str, str]] = [
|
|
40
|
+
("docs/user_guide/ClinicalScope_UserGuide.pdf", "file"),
|
|
41
|
+
("LICENSE", "file"),
|
|
42
|
+
("DISCLAIMER.txt", "file"),
|
|
43
|
+
("example/template_patient_data_structure", "tree"),
|
|
44
|
+
("example/demo_database", "tree"),
|
|
45
|
+
]
|
|
46
|
+
_TREE_IGNORE = shutil.ignore_patterns("clinical_scope_output")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def copy_assets(bundle_root: Path) -> list[str]:
|
|
50
|
+
"""Copy each asset into ``bundle_root``; return the ones that were missing."""
|
|
51
|
+
missing: list[str] = []
|
|
52
|
+
for rel, kind in ASSETS:
|
|
53
|
+
src = PROJECT_ROOT / rel
|
|
54
|
+
if not src.exists():
|
|
55
|
+
print(f" WARNING: asset not found, skipped: {rel}")
|
|
56
|
+
missing.append(rel)
|
|
57
|
+
continue
|
|
58
|
+
dest = bundle_root / src.name
|
|
59
|
+
if kind == "tree":
|
|
60
|
+
shutil.copytree(src, dest, ignore=_TREE_IGNORE, dirs_exist_ok=True)
|
|
61
|
+
else:
|
|
62
|
+
shutil.copy(src, dest)
|
|
63
|
+
print(f" copied {rel}")
|
|
64
|
+
return missing
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv: list[str] | None = None) -> int:
|
|
68
|
+
"""Copy assets and write license notices into the bundle; return an exit code."""
|
|
69
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
70
|
+
ap.add_argument(
|
|
71
|
+
"--bundle-root",
|
|
72
|
+
required=True,
|
|
73
|
+
type=Path,
|
|
74
|
+
help="Built bundle directory (contains the executable and _internal/).",
|
|
75
|
+
)
|
|
76
|
+
args = ap.parse_args(argv)
|
|
77
|
+
if not args.bundle_root.is_dir():
|
|
78
|
+
ap.error(f"bundle root does not exist: {args.bundle_root}")
|
|
79
|
+
|
|
80
|
+
print("Copying bundle assets...")
|
|
81
|
+
missing = copy_assets(args.bundle_root)
|
|
82
|
+
if missing:
|
|
83
|
+
print(f" MISSING ASSETS: {', '.join(missing)}")
|
|
84
|
+
|
|
85
|
+
print("Generating third-party license notices...")
|
|
86
|
+
lic_rc = licenses.main(["--bundle-root", str(args.bundle_root)])
|
|
87
|
+
|
|
88
|
+
return 1 if (missing or lic_rc) else 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate THIRD_PARTY_LICENSES.txt for a built ClinicalScope bundle.
|
|
4
|
+
|
|
5
|
+
The PyInstaller bundle redistributes third-party code and native libraries, all
|
|
6
|
+
under permissive / public-domain licenses whose only obligation is attribution.
|
|
7
|
+
PyInstaller does not collect their license texts, so this script assembles them:
|
|
8
|
+
|
|
9
|
+
1. Python packages -- harvested from the *running interpreter's* installed
|
|
10
|
+
distributions (run this with the same venv that produced the build, so the
|
|
11
|
+
set matches exactly). Build-only tooling is excluded.
|
|
12
|
+
2. Native libraries + the embedded interpreter -- discovered by scanning the
|
|
13
|
+
bundle's ``_internal/`` folder, then matched against a hand-maintained map.
|
|
14
|
+
Unknown native libraries are emitted as TODO entries rather than skipped.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
python generate_third_party_licenses.py --bundle-root <dir-with-_internal>
|
|
19
|
+
|
|
20
|
+
The output is written to ``<bundle-root>/THIRD_PARTY_LICENSES.txt``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import importlib.metadata as im
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# Build-only tooling that is NOT redistributed in the bundle -> no obligation.
|
|
31
|
+
# (PyInstaller's own bootloader ships under its separate distribution exception.)
|
|
32
|
+
BUILD_ONLY = {
|
|
33
|
+
"pyinstaller",
|
|
34
|
+
"pyinstaller-hooks-contrib",
|
|
35
|
+
"altgraph",
|
|
36
|
+
"macholib",
|
|
37
|
+
"pip",
|
|
38
|
+
"setuptools",
|
|
39
|
+
"wheel",
|
|
40
|
+
"clinical-scope", # our own code -> covered by the top-level LICENSE
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
LIC_HINTS = ("license", "licence", "copying", "notice", "authors")
|
|
44
|
+
|
|
45
|
+
# Copyleft policy for the build warning. Attribution discharges permissive
|
|
46
|
+
# licenses and weak file-level copyleft (MPL-2.0). Strong copyleft adds source
|
|
47
|
+
# obligations an attribution-only bundle can't meet -> flagged for review.
|
|
48
|
+
STRONG_COPYLEFT = (
|
|
49
|
+
"affero",
|
|
50
|
+
"agpl",
|
|
51
|
+
"lgpl",
|
|
52
|
+
"gpl",
|
|
53
|
+
"eclipse public",
|
|
54
|
+
"epl-",
|
|
55
|
+
"cddl",
|
|
56
|
+
"sspl",
|
|
57
|
+
"common public",
|
|
58
|
+
"cpl-",
|
|
59
|
+
"share-alike",
|
|
60
|
+
"sharealike",
|
|
61
|
+
"cc-by-sa",
|
|
62
|
+
)
|
|
63
|
+
WEAK_COPYLEFT = ("mozilla public", "mpl-2", "mpl 2") # noted, not flagged
|
|
64
|
+
# Longer ``License`` values are inlined license text, not an identifier -> skip.
|
|
65
|
+
MAX_LICENSE_ID_LEN = 80
|
|
66
|
+
|
|
67
|
+
# Strong-copyleft dists a human reviewed and accepts (name -> reason); listed here
|
|
68
|
+
# silences the warning, strong-copyleft and absent is reported as unexpected.
|
|
69
|
+
ACCEPTED_COPYLEFT: dict[str, str] = {}
|
|
70
|
+
|
|
71
|
+
# Standalone native shared libraries -> (display name, license notice). Keyed by
|
|
72
|
+
# a lowercase filename-stem prefix; matched across platforms (.dylib/.so/.dll).
|
|
73
|
+
# Extend this map when a build introduces a native lib not listed here.
|
|
74
|
+
NATIVE_LICENSES: dict[str, tuple[str, str]] = {
|
|
75
|
+
"libarrow": (
|
|
76
|
+
"Apache Arrow C++",
|
|
77
|
+
"License: Apache License 2.0. The full Apache-2.0 text and Arrow's NOTICE\n"
|
|
78
|
+
'are reproduced above in the "pyarrow" section (same project).',
|
|
79
|
+
),
|
|
80
|
+
"libparquet": (
|
|
81
|
+
"Apache Parquet C++ (part of Apache Arrow)",
|
|
82
|
+
'License: Apache License 2.0. See the "pyarrow" section above.',
|
|
83
|
+
),
|
|
84
|
+
"libcrypto": (
|
|
85
|
+
"OpenSSL",
|
|
86
|
+
"License: Apache License 2.0 (OpenSSL 3.x).\n"
|
|
87
|
+
"Copyright (c) The OpenSSL Project Authors. https://www.openssl.org",
|
|
88
|
+
),
|
|
89
|
+
"libssl": (
|
|
90
|
+
"OpenSSL",
|
|
91
|
+
"License: Apache License 2.0 (OpenSSL 3.x).\n"
|
|
92
|
+
"Copyright (c) The OpenSSL Project Authors. https://www.openssl.org",
|
|
93
|
+
),
|
|
94
|
+
"libsqlite3": (
|
|
95
|
+
"SQLite",
|
|
96
|
+
"SQLite is in the public domain. https://www.sqlite.org/copyright.html",
|
|
97
|
+
),
|
|
98
|
+
"liblzma": (
|
|
99
|
+
"liblzma / xz",
|
|
100
|
+
"The liblzma core is public domain (0BSD). https://tukaani.org/xz/",
|
|
101
|
+
),
|
|
102
|
+
"libmpdec": (
|
|
103
|
+
"libmpdec",
|
|
104
|
+
"License: BSD 2-Clause. Copyright (c) Stefan Krah. Used by Python decimal.",
|
|
105
|
+
),
|
|
106
|
+
# Common cross-platform extras (may appear on Linux/Windows builds):
|
|
107
|
+
"libz": ("zlib", "License: zlib license. https://zlib.net"),
|
|
108
|
+
"libbz2": ("bzip2", "License: BSD-style. https://sourceware.org/bzip2/"),
|
|
109
|
+
"libffi": ("libffi", "License: MIT. https://github.com/libffi/libffi"),
|
|
110
|
+
"libtcl": ("Tcl", "License: Tcl/Tk BSD-style license. https://www.tcl.tk"),
|
|
111
|
+
"libtk": ("Tk", "License: Tcl/Tk BSD-style license. https://www.tcl.tk"),
|
|
112
|
+
# OpenBLAS, vendored into NumPy/SciPy wheels (same file stem on every OS):
|
|
113
|
+
"libscipy_openblas": (
|
|
114
|
+
"OpenBLAS",
|
|
115
|
+
"License: BSD 3-Clause. Copyright (c) 2011-present, The OpenBLAS contributors.\n"
|
|
116
|
+
"https://github.com/OpenMathLib/OpenBLAS/blob/develop/LICENSE",
|
|
117
|
+
),
|
|
118
|
+
# Windows drops the `lib` prefix, so these mirror the lib* keys above.
|
|
119
|
+
"arrow": (
|
|
120
|
+
"Apache Arrow C++",
|
|
121
|
+
"License: Apache License 2.0. The full Apache-2.0 text and Arrow's NOTICE\n"
|
|
122
|
+
'are reproduced above in the "pyarrow" section (same project).',
|
|
123
|
+
),
|
|
124
|
+
"parquet": (
|
|
125
|
+
"Apache Parquet C++ (part of Apache Arrow)",
|
|
126
|
+
'License: Apache License 2.0. See the "pyarrow" section above.',
|
|
127
|
+
),
|
|
128
|
+
"sqlite3": (
|
|
129
|
+
"SQLite",
|
|
130
|
+
"SQLite is in the public domain. https://www.sqlite.org/copyright.html",
|
|
131
|
+
),
|
|
132
|
+
"pywintypes": (
|
|
133
|
+
"pywin32 (pywintypes)",
|
|
134
|
+
"License: PSF License (BSD-style). Copyright (c) Mark Hammond and contributors.\n"
|
|
135
|
+
"https://github.com/mhammond/pywin32",
|
|
136
|
+
),
|
|
137
|
+
# Linux GCC runtime libs; the GCC Runtime Library Exception permits non-GPL use.
|
|
138
|
+
"libgcc": (
|
|
139
|
+
"GCC runtime libraries",
|
|
140
|
+
"License: GPL-3.0-or-later WITH GCC-exception-3.1 (GCC Runtime Library\n"
|
|
141
|
+
"Exception), which permits distribution with independent software.\n"
|
|
142
|
+
"Copyright (c) Free Software Foundation, Inc. https://gcc.gnu.org",
|
|
143
|
+
),
|
|
144
|
+
"libstdc++": (
|
|
145
|
+
"GCC runtime libraries",
|
|
146
|
+
"License: GPL-3.0-or-later WITH GCC-exception-3.1 (GCC Runtime Library\n"
|
|
147
|
+
"Exception), which permits distribution with independent software.\n"
|
|
148
|
+
"Copyright (c) Free Software Foundation, Inc. https://gcc.gnu.org",
|
|
149
|
+
),
|
|
150
|
+
"libgfortran": (
|
|
151
|
+
"GCC runtime libraries",
|
|
152
|
+
"License: GPL-3.0-or-later WITH GCC-exception-3.1 (GCC Runtime Library\n"
|
|
153
|
+
"Exception), which permits distribution with independent software.\n"
|
|
154
|
+
"Copyright (c) Free Software Foundation, Inc. https://gcc.gnu.org",
|
|
155
|
+
),
|
|
156
|
+
"libquadmath": (
|
|
157
|
+
"GCC libquadmath",
|
|
158
|
+
"License: LGPL-2.1-or-later. Copyright (c) Free Software Foundation, Inc.\n"
|
|
159
|
+
"https://gcc.gnu.org/onlinedocs/libquadmath/",
|
|
160
|
+
),
|
|
161
|
+
"libtinfo": (
|
|
162
|
+
"ncurses (libtinfo)",
|
|
163
|
+
"License: MIT-style (ncurses license). Copyright (c) Free Software\n"
|
|
164
|
+
"Foundation, Inc. https://invisible-island.net/ncurses/",
|
|
165
|
+
),
|
|
166
|
+
"libuuid": (
|
|
167
|
+
"util-linux libuuid",
|
|
168
|
+
"License: BSD 3-Clause. Copyright (c) Theodore Ts'o.\n"
|
|
169
|
+
"https://github.com/util-linux/util-linux",
|
|
170
|
+
),
|
|
171
|
+
# GNU Readline is deliberately absent: GPL-3, excluded at build time (see the
|
|
172
|
+
# spec's `readline` exclude). Its absence makes any re-bundling surface as an
|
|
173
|
+
# UNRECOGNISED warning instead of being silently attributed.
|
|
174
|
+
# Windows Microsoft C/C++ runtimes, redistributable under the VS / SDK terms.
|
|
175
|
+
"vcruntime": (
|
|
176
|
+
"Microsoft Visual C++ Runtime",
|
|
177
|
+
"License: Microsoft Visual C++ Redistributable (distributable code).\n"
|
|
178
|
+
"Copyright (c) Microsoft Corporation. https://visualstudio.microsoft.com",
|
|
179
|
+
),
|
|
180
|
+
"msvcp140": (
|
|
181
|
+
"Microsoft Visual C++ Runtime",
|
|
182
|
+
"License: Microsoft Visual C++ Redistributable (distributable code).\n"
|
|
183
|
+
"Copyright (c) Microsoft Corporation. https://visualstudio.microsoft.com",
|
|
184
|
+
),
|
|
185
|
+
"api-ms-win": (
|
|
186
|
+
"Microsoft Universal C Runtime (UCRT)",
|
|
187
|
+
"License: Microsoft Windows SDK distributable code (Universal CRT).\n"
|
|
188
|
+
"Copyright (c) Microsoft Corporation. https://learn.microsoft.com",
|
|
189
|
+
),
|
|
190
|
+
"ucrtbase": (
|
|
191
|
+
"Microsoft Universal C Runtime (UCRT)",
|
|
192
|
+
"License: Microsoft Windows SDK distributable code (Universal CRT).\n"
|
|
193
|
+
"Copyright (c) Microsoft Corporation. https://learn.microsoft.com",
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Packages that ship no license file upstream -> supply a notice by hand here.
|
|
198
|
+
MANUAL_PKG: dict[str, str] = {}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def norm(name: str) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Lowercase a distribution name.
|
|
204
|
+
|
|
205
|
+
Hyphens and underscores are unified so lookups are spelling-insensitive.
|
|
206
|
+
"""
|
|
207
|
+
return (name or "").lower().replace("_", "-")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def classify_license(dist: im.Distribution) -> tuple[str, str] | None:
|
|
211
|
+
"""
|
|
212
|
+
Classify a distribution's license as copyleft, if it is.
|
|
213
|
+
|
|
214
|
+
Returns ``(severity, label)`` (severity ``"strong"``/``"weak"``) or ``None``
|
|
215
|
+
for permissive/unknown. Reads the standardized ``License ::`` trove
|
|
216
|
+
classifiers plus a short ``License``/``License-Expression`` field; a long
|
|
217
|
+
``License`` value (inlined license text) is skipped so it can't false-match.
|
|
218
|
+
"""
|
|
219
|
+
md = dist.metadata
|
|
220
|
+
parts = [
|
|
221
|
+
v
|
|
222
|
+
for k in ("License-Expression", "License")
|
|
223
|
+
if (v := md.get(k)) and len(v) < MAX_LICENSE_ID_LEN
|
|
224
|
+
]
|
|
225
|
+
parts += [c for c in md.get_all("Classifier") or [] if c.startswith("License")]
|
|
226
|
+
blob = " ; ".join(parts).lower()
|
|
227
|
+
label = "; ".join(parts) or "?"
|
|
228
|
+
if any(m in blob for m in STRONG_COPYLEFT):
|
|
229
|
+
return "strong", label
|
|
230
|
+
if any(m in blob for m in WEAK_COPYLEFT):
|
|
231
|
+
return "weak", label
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def harvest_python_packages() -> tuple[list[str], list[str], list[str], list[tuple[str, str, str]]]:
|
|
236
|
+
"""
|
|
237
|
+
Collect license sections for the running interpreter's distributions.
|
|
238
|
+
|
|
239
|
+
Returns ``(sections, covered, missing, copyleft)`` where ``copyleft`` lists
|
|
240
|
+
``(severity, name-version, label)`` for non-permissive components.
|
|
241
|
+
"""
|
|
242
|
+
excluded = {norm(b) for b in BUILD_ONLY}
|
|
243
|
+
seen: set[str] = set()
|
|
244
|
+
sections: list[str] = []
|
|
245
|
+
covered: list[str] = []
|
|
246
|
+
missing: list[str] = []
|
|
247
|
+
copyleft: list[tuple[str, str, str]] = []
|
|
248
|
+
|
|
249
|
+
dists = sorted(im.distributions(), key=lambda d: norm(d.metadata.get("Name") or ""))
|
|
250
|
+
for d in dists:
|
|
251
|
+
name = d.metadata.get("Name") or "<unknown>"
|
|
252
|
+
n = norm(name)
|
|
253
|
+
if n in excluded or n in seen:
|
|
254
|
+
continue
|
|
255
|
+
seen.add(n)
|
|
256
|
+
verdict = classify_license(d)
|
|
257
|
+
if verdict and not (verdict[0] == "strong" and n in ACCEPTED_COPYLEFT):
|
|
258
|
+
copyleft.append((verdict[0], f"{name} {d.version}", verdict[1]))
|
|
259
|
+
texts = _license_texts(d)
|
|
260
|
+
if not texts:
|
|
261
|
+
body = MANUAL_PKG.get(
|
|
262
|
+
n, "*** TODO: upstream ships no license file -- supply the notice. ***"
|
|
263
|
+
)
|
|
264
|
+
sections.append(_block(f"{name} {d.version} (manual notice)", [("notice", body)]))
|
|
265
|
+
missing.append(f"{name} {d.version}")
|
|
266
|
+
continue
|
|
267
|
+
sections.append(_block(f"{name} {d.version}", texts))
|
|
268
|
+
covered.append(f"{name} {d.version}")
|
|
269
|
+
return sections, covered, missing, copyleft
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _license_texts(dist: im.Distribution) -> list[tuple[str, str]]:
|
|
273
|
+
"""Return ``(relative-name, text)`` for each license file a dist ships."""
|
|
274
|
+
out: list[tuple[str, str]] = []
|
|
275
|
+
for f in dist.files or []:
|
|
276
|
+
s = str(f).replace("\\", "/").lower()
|
|
277
|
+
if ".dist-info/" not in s:
|
|
278
|
+
continue
|
|
279
|
+
base = s.rsplit("/", 1)[-1]
|
|
280
|
+
if "/licenses/" in s or "/license_files/" in s or any(h in base for h in LIC_HINTS):
|
|
281
|
+
try:
|
|
282
|
+
text = dist.locate_file(f).read_text(encoding="utf-8", errors="replace")
|
|
283
|
+
except OSError as e:
|
|
284
|
+
text = f"<<could not read {f}: {e}>>"
|
|
285
|
+
out.append((str(f).split(".dist-info/", 1)[-1], text))
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _block(title: str, parts: list[tuple[str, str]]) -> str:
|
|
290
|
+
bar = "=" * 78
|
|
291
|
+
body = "\n".join(f"--- {name} ---\n{text.rstrip()}\n" for name, text in parts)
|
|
292
|
+
return f"\n{bar}\n{title}\n{bar}\n\n{body}"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def discover_native_libs(internal: Path) -> list[Path]:
|
|
296
|
+
"""
|
|
297
|
+
Return the standalone native shared libraries bundled in ``_internal``.
|
|
298
|
+
|
|
299
|
+
Extension modules (``*.cpython-*.so``, ``*.pyd``, ``*.cpython-*.dylib``) belong
|
|
300
|
+
to a Python package and are already attributed; only standalone ``lib*`` /
|
|
301
|
+
versioned shared objects need their own notice, so those are what we return.
|
|
302
|
+
"""
|
|
303
|
+
standalone: list[Path] = []
|
|
304
|
+
ext_suffixes = (".pyd",)
|
|
305
|
+
for p in sorted(internal.rglob("*")):
|
|
306
|
+
if not p.is_file():
|
|
307
|
+
continue
|
|
308
|
+
name = p.name.lower()
|
|
309
|
+
is_shared = name.endswith((".dylib", ".dll")) or (
|
|
310
|
+
".so" in name and (name.endswith(".so") or ".so." in name)
|
|
311
|
+
)
|
|
312
|
+
if not is_shared and not name.endswith(ext_suffixes):
|
|
313
|
+
continue
|
|
314
|
+
is_extension = "cpython-" in name or name.endswith(ext_suffixes) or "abi3" in name
|
|
315
|
+
if is_shared and not is_extension:
|
|
316
|
+
standalone.append(p)
|
|
317
|
+
return standalone
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
PSF_TODO = "*** TODO: paste PSF License text. https://docs.python.org/3/license.html ***"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def find_psf_text() -> str:
|
|
324
|
+
"""Return the CPython PSF license text, or ``PSF_TODO`` if not found on disk."""
|
|
325
|
+
base = Path(sys.base_prefix)
|
|
326
|
+
ver = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
327
|
+
candidates = [
|
|
328
|
+
base / "lib" / ver / "LICENSE.txt",
|
|
329
|
+
base / "LICENSE.txt",
|
|
330
|
+
base / "LICENSE",
|
|
331
|
+
]
|
|
332
|
+
for c in candidates:
|
|
333
|
+
if c.is_file():
|
|
334
|
+
return c.read_text(encoding="utf-8", errors="replace").rstrip()
|
|
335
|
+
return PSF_TODO
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def native_section(internal: Path) -> tuple[str, list[str]]:
|
|
339
|
+
"""
|
|
340
|
+
Build the native-library + embedded-interpreter section for this bundle.
|
|
341
|
+
|
|
342
|
+
Returns ``(text, unrecognised)`` so the caller can flag unmatched native
|
|
343
|
+
libraries via the process exit code.
|
|
344
|
+
"""
|
|
345
|
+
standalone = discover_native_libs(internal)
|
|
346
|
+
lines = [
|
|
347
|
+
"\n" + "#" * 78,
|
|
348
|
+
"NATIVE LIBRARIES AND EMBEDDED INTERPRETER",
|
|
349
|
+
"#" * 78,
|
|
350
|
+
"",
|
|
351
|
+
"Bundled as compiled shared libraries in `_internal/`; no Python metadata.",
|
|
352
|
+
"Discovered for THIS platform's build (re-run per platform).",
|
|
353
|
+
"",
|
|
354
|
+
]
|
|
355
|
+
# The interpreter's own shared lib (libpython*.so / python3*.dll) is covered
|
|
356
|
+
# by the PSF block below, so route it there instead of flagging it.
|
|
357
|
+
interpreter_prefixes = ("libpython", "python3")
|
|
358
|
+
matched: dict[str, tuple[str, list[str]]] = {}
|
|
359
|
+
interpreter_libs: list[str] = []
|
|
360
|
+
unknown: list[str] = []
|
|
361
|
+
for p in standalone:
|
|
362
|
+
stem = p.name.lower()
|
|
363
|
+
if stem.startswith(interpreter_prefixes):
|
|
364
|
+
interpreter_libs.append(p.name)
|
|
365
|
+
continue
|
|
366
|
+
hit = next((k for k in NATIVE_LICENSES if stem.startswith(k)), None)
|
|
367
|
+
if hit:
|
|
368
|
+
disp, notice = NATIVE_LICENSES[hit]
|
|
369
|
+
matched.setdefault(disp, (notice, []))[1].append(p.name)
|
|
370
|
+
else:
|
|
371
|
+
unknown.append(p.name)
|
|
372
|
+
|
|
373
|
+
for disp, (notice, files) in sorted(matched.items()):
|
|
374
|
+
lines += ["-" * 78, f"{disp} ({', '.join(sorted(set(files)))})", "-" * 78, notice, ""]
|
|
375
|
+
|
|
376
|
+
# Embedded interpreter (PSF) -- always present in a PyInstaller bundle.
|
|
377
|
+
psf_text = find_psf_text()
|
|
378
|
+
if psf_text == PSF_TODO:
|
|
379
|
+
unknown.append("CPython PSF license text (interpreter)")
|
|
380
|
+
interp_files = f" ({', '.join(sorted(set(interpreter_libs)))})" if interpreter_libs else ""
|
|
381
|
+
lines += [
|
|
382
|
+
"-" * 78,
|
|
383
|
+
f"CPython {sys.version_info.major}.{sys.version_info.minor} interpreter "
|
|
384
|
+
f"(Python, base_library.zip, interpreter shared library){interp_files}",
|
|
385
|
+
"-" * 78,
|
|
386
|
+
"License: Python Software Foundation License Version 2 (PSF-2.0).",
|
|
387
|
+
"Copyright (c) 2001-present Python Software Foundation. All Rights Reserved.",
|
|
388
|
+
"",
|
|
389
|
+
psf_text,
|
|
390
|
+
"",
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
if unknown:
|
|
394
|
+
lines += [
|
|
395
|
+
"-" * 78,
|
|
396
|
+
"UNRECOGNISED NATIVE LIBRARIES -- ACTION REQUIRED",
|
|
397
|
+
"-" * 78,
|
|
398
|
+
"Add these to NATIVE_LICENSES in generate_third_party_licenses.py:",
|
|
399
|
+
*[f" *** TODO: {u}" for u in sorted(unknown)],
|
|
400
|
+
"",
|
|
401
|
+
]
|
|
402
|
+
return "\n".join(lines), unknown
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def build_header(covered: int, missing: int) -> str:
|
|
406
|
+
"""Return the file preamble summarising counts and provenance."""
|
|
407
|
+
return f"""\
|
|
408
|
+
THIRD-PARTY SOFTWARE NOTICES AND LICENSES
|
|
409
|
+
=========================================
|
|
410
|
+
|
|
411
|
+
ClinicalScope is distributed as a self-contained executable produced with
|
|
412
|
+
PyInstaller. The bundle (the `_internal/` folder next to the executable)
|
|
413
|
+
redistributes the third-party components listed below, each under its own
|
|
414
|
+
license. The full and unmodified text of every license follows; refer to each
|
|
415
|
+
notice for its specific terms.
|
|
416
|
+
|
|
417
|
+
Generated from the build interpreter: {sys.executable}
|
|
418
|
+
|
|
419
|
+
Python packages with reproduced license text: {covered}
|
|
420
|
+
Python packages needing a manual notice (listed inline below): {missing}
|
|
421
|
+
Native libraries + interpreter: see "NATIVE LIBRARIES" at the end.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def main(argv: list[str] | None = None) -> int:
|
|
426
|
+
"""Parse arguments, assemble the file, and write it to the bundle root."""
|
|
427
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
428
|
+
ap.add_argument(
|
|
429
|
+
"--bundle-root",
|
|
430
|
+
required=True,
|
|
431
|
+
type=Path,
|
|
432
|
+
help="Bundle directory containing the executable and _internal/.",
|
|
433
|
+
)
|
|
434
|
+
ap.add_argument("--output", type=Path, default=None, help="Override output path.")
|
|
435
|
+
args = ap.parse_args(argv)
|
|
436
|
+
|
|
437
|
+
internal = args.bundle_root / "_internal"
|
|
438
|
+
if not internal.is_dir():
|
|
439
|
+
ap.error(f"no _internal/ under {args.bundle_root} -- is this a built bundle?")
|
|
440
|
+
out = args.output or (args.bundle_root / "THIRD_PARTY_LICENSES.txt")
|
|
441
|
+
|
|
442
|
+
sections, covered, missing, copyleft = harvest_python_packages()
|
|
443
|
+
native_text, unrecognised = native_section(internal)
|
|
444
|
+
out.write_text(
|
|
445
|
+
build_header(len(covered), len(missing)) + "".join(sections) + native_text,
|
|
446
|
+
encoding="utf-8",
|
|
447
|
+
)
|
|
448
|
+
print(f"wrote {out} ({out.stat().st_size:,} bytes)")
|
|
449
|
+
print(f" python packages: {len(covered)} with text, {len(missing)} manual")
|
|
450
|
+
if missing:
|
|
451
|
+
print(f" MANUAL NEEDED: {', '.join(missing)}")
|
|
452
|
+
if unrecognised:
|
|
453
|
+
print(f" UNRECOGNISED NATIVE LIBS: {', '.join(sorted(unrecognised))}")
|
|
454
|
+
|
|
455
|
+
strong = [c for c in copyleft if c[0] == "strong"]
|
|
456
|
+
weak = [c for c in copyleft if c[0] == "weak"]
|
|
457
|
+
if weak:
|
|
458
|
+
print(f" weak copyleft (OK to ship unmodified): {', '.join(c[1] for c in weak)}")
|
|
459
|
+
if strong:
|
|
460
|
+
print(" UNEXPECTED STRONG-COPYLEFT PACKAGES -- review before publishing:")
|
|
461
|
+
for _, nv, label in strong:
|
|
462
|
+
print(f" {nv} [{label}]")
|
|
463
|
+
|
|
464
|
+
# Warn-only by design (see build_info/README.md): unresolved attribution or a
|
|
465
|
+
# new strong-copyleft dep must not pass silently, so signal via a non-zero exit
|
|
466
|
+
# code that build.sh surfaces -- without blocking an iterative build.
|
|
467
|
+
return 1 if (missing or unrecognised or strong) else 0
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if __name__ == "__main__":
|
|
471
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# config package — configuration parsing utilities.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration file parsing utilities.
|
|
3
|
+
|
|
4
|
+
This module provides functions for loading JSON and XLSX configuration files,
|
|
5
|
+
including database options and patient options.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from clinical_scope.database_options_parser import validate_database_options
|
|
13
|
+
from clinical_scope.database_options_xlsx import xlsx_to_database_options
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ==================================================================================================
|
|
19
|
+
def load_options(path: Path | None) -> dict:
|
|
20
|
+
"""Load JSON options from a file if the path exists."""
|
|
21
|
+
if path and path.exists():
|
|
22
|
+
with path.open(encoding="utf-8") as file:
|
|
23
|
+
return json.load(file)
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ==================================================================================================
|
|
28
|
+
def build_patient_options(
|
|
29
|
+
patient_folder: str | Path,
|
|
30
|
+
path_patient_options: str | Path | None = None,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Build a patient_options dict from a folder path and an optional JSON file.
|
|
34
|
+
|
|
35
|
+
``data_folder`` is always set from *patient_folder*.
|
|
36
|
+
Any other keys present in the JSON file are preserved.
|
|
37
|
+
"""
|
|
38
|
+
opts = load_options(Path(path_patient_options)) if path_patient_options else {}
|
|
39
|
+
opts["data_folder"] = str(patient_folder)
|
|
40
|
+
return opts
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ==================================================================================================
|
|
44
|
+
def load_database_options_from_path(path: Path) -> dict:
|
|
45
|
+
"""
|
|
46
|
+
Load database options from a JSON or XLSX file.
|
|
47
|
+
|
|
48
|
+
This is the canonical entry point for loading database options from a file
|
|
49
|
+
path, supporting both formats accepted by the Dash UI file upload.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: Path to a ``.json`` or ``.xlsx`` database options file.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Parsed database options dictionary.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If the file extension is not supported.
|
|
59
|
+
FileNotFoundError: If the path does not exist.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if not path.exists():
|
|
64
|
+
msg = f"Database options file not found: {path}"
|
|
65
|
+
raise FileNotFoundError(msg)
|
|
66
|
+
|
|
67
|
+
suffix = path.suffix.lower()
|
|
68
|
+
if suffix == ".json":
|
|
69
|
+
db_options = load_options(path)
|
|
70
|
+
elif suffix == ".xlsx":
|
|
71
|
+
db_options = xlsx_to_database_options(path)
|
|
72
|
+
else:
|
|
73
|
+
msg = f"Unsupported file extension '{suffix}'. Expected .json or .xlsx."
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
|
|
76
|
+
for issue in validate_database_options(db_options):
|
|
77
|
+
if issue.severity == "error":
|
|
78
|
+
logger.error("database_options [%s]: %s", issue.path, issue.message)
|
|
79
|
+
elif issue.severity == "warning":
|
|
80
|
+
logger.warning("database_options [%s]: %s", issue.path, issue.message)
|
|
81
|
+
else:
|
|
82
|
+
logger.info("database_options [%s]: %s", issue.path, issue.message)
|
|
83
|
+
|
|
84
|
+
return db_options
|