iints-sdk-python35 1.5.4__py3-none-any.whl → 1.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. iints/__init__.py +7 -3
  2. iints/api/registry.py +280 -0
  3. iints/cli/cli.py +1127 -45
  4. iints/core/devices/__init__.py +2 -2
  5. iints/core/devices/models.py +178 -5
  6. iints/core/patient/bergman_model.py +28 -13
  7. iints/core/physiology_variation.py +119 -0
  8. iints/core/simulator.py +43 -8
  9. iints/data/__init__.py +48 -1
  10. iints/data/datasets.json +62 -5
  11. iints/data/demo/demo_cgm.csv +288 -288
  12. iints/data/physiology_residual_profiles.json +13953 -0
  13. iints/data/realism_dashboard.py +390 -0
  14. iints/data/realism_reference.py +229 -0
  15. iints/data/realism_references.json +56 -0
  16. iints/data/realism_validator.py +813 -0
  17. iints/data/synthetic_mirror.py +183 -10
  18. iints/data/tidepool.py +239 -14
  19. iints/data/virtual_patients/reference_azt1d_t1d.yaml +12 -0
  20. iints/data/virtual_patients/reference_free_living_t1d.yaml +12 -0
  21. iints/data/virtual_patients/reference_hupa_ucm_t1d.yaml +12 -0
  22. iints/highlevel.py +38 -25
  23. iints/jetson/__init__.py +25 -0
  24. iints/jetson/endurance.py +949 -0
  25. iints/live_patient/api.py +6 -0
  26. iints/live_patient/edge_ops.py +204 -0
  27. iints/live_patient/runtime.py +15 -14
  28. iints/presets/evidence_sources.yaml +9 -1
  29. iints/presets/presets.json +380 -18
  30. iints/research/__init__.py +11 -1
  31. iints/research/evaluation.py +216 -1
  32. iints/scenarios/study_pack.py +31 -18
  33. iints/utils/run_io.py +11 -0
  34. iints_sdk_python35-1.5.5.dist-info/METADATA +131 -0
  35. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/RECORD +41 -30
  36. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/entry_points.txt +1 -1
  37. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/licenses/LICENSE +2 -2
  38. iints_sdk_python35-1.5.4.dist-info/METADATA +0 -200
  39. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/WHEEL +0 -0
  40. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/licenses/LICENSE-MIT-IINTS-LEGACY +0 -0
  41. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/licenses/NOTICE +0 -0
  42. {iints_sdk_python35-1.5.4.dist-info → iints_sdk_python35-1.5.5.dist-info}/top_level.txt +0 -0
iints/__init__.py CHANGED
@@ -11,7 +11,7 @@ except ImportError: # pragma: no cover - Python < 3.8 fallback
11
11
  try:
12
12
  __version__ = version("iints-sdk-python35")
13
13
  except PackageNotFoundError: # pragma: no cover - source tree fallback
14
- __version__ = "1.5.4"
14
+ __version__ = "1.5.5"
15
15
 
16
16
  # Note to developers: this SDK is currently maintained by a single author.
17
17
  # Please report bugs via GitHub issues and feel free to contribute fixes via PRs.
@@ -47,7 +47,7 @@ except Exception: # pragma: no cover - fallback if torch/device manager import
47
47
  def get_device(self):
48
48
  return self._device
49
49
  from .core.safety import SafetyConfig, SafetySupervisor
50
- from .core.devices.models import SensorModel, PumpModel
50
+ from .core.devices.models import SENSOR_PROFILES, SensorModel, PumpModel, create_sensor_model
51
51
  from .core.algorithms.standard_pump_algo import StandardPumpAlgorithm
52
52
  from .core.algorithms.mock_algorithms import (
53
53
  ConstantDoseAlgorithm,
@@ -74,7 +74,7 @@ from .data.importer import (
74
74
  summarize_carelink_csv,
75
75
  )
76
76
  from .data.nightscout import NightscoutConfig, import_nightscout
77
- from .data.tidepool import TidepoolClient, load_openapi_spec
77
+ from .data.tidepool import TidepoolClient, TidepoolConfig, import_tidepool, load_openapi_spec
78
78
  from .data.guardians import mdmp_gate, MDMPGateError
79
79
  from .data.synthetic_mirror import generate_synthetic_mirror, SyntheticMirrorArtifact
80
80
  from .data.study_corruption import AVAILABLE_STUDY_CORRUPTIONS, apply_study_corruptions, write_corrupted_study_csv
@@ -224,6 +224,8 @@ __all__ = [
224
224
  "SafetyConfig",
225
225
  "SensorModel",
226
226
  "PumpModel",
227
+ "SENSOR_PROFILES",
228
+ "create_sensor_model",
227
229
  "StandardPumpAlgorithm",
228
230
  "ConstantDoseAlgorithm",
229
231
  "RandomDoseAlgorithm",
@@ -247,6 +249,8 @@ __all__ = [
247
249
  "NightscoutConfig",
248
250
  "import_nightscout",
249
251
  "TidepoolClient",
252
+ "TidepoolConfig",
253
+ "import_tidepool",
250
254
  "load_openapi_spec",
251
255
  "mdmp_gate",
252
256
  "MDMPGateError",
iints/api/registry.py CHANGED
@@ -1,8 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ import hashlib
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import re
10
+ import shutil
11
+ import sys
12
+ import time
4
13
  from typing import List, Optional, Mapping, Sequence, Iterable, Any, cast
5
14
  import importlib
15
+ import importlib.util
6
16
 
7
17
  try:
8
18
  from importlib import metadata as importlib_metadata
@@ -12,6 +22,10 @@ except Exception: # pragma: no cover
12
22
  from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmMetadata
13
23
  from iints.core.algorithms.discovery import discover_algorithms
14
24
 
25
+ PLUGIN_REGISTRY_SCHEMA_VERSION = "1.0"
26
+ IINTS_PLUGIN_HOME_ENV = "IINTS_PLUGIN_HOME"
27
+ SUPPORTED_LOCAL_PLUGIN_KINDS = {"algorithm", "patient_model", "data_source", "validator"}
28
+
15
29
 
16
30
  @dataclass
17
31
  class AlgorithmListing:
@@ -21,6 +35,231 @@ class AlgorithmListing:
21
35
  metadata: Optional[AlgorithmMetadata]
22
36
  status: str = "available"
23
37
  error: Optional[str] = None
38
+ plugin_path: Optional[str] = None
39
+
40
+
41
+ @dataclass
42
+ class LocalPluginRecord:
43
+ kind: str
44
+ name: str
45
+ installed_path: str
46
+ source_path: str
47
+ module_stem: str
48
+ class_name: Optional[str] = None
49
+ registered_at_utc: str = ""
50
+
51
+ @classmethod
52
+ def from_dict(cls, payload: Mapping[str, Any]) -> "LocalPluginRecord":
53
+ return cls(
54
+ kind=str(payload.get("kind", "")),
55
+ name=str(payload.get("name", "")),
56
+ installed_path=str(payload.get("installed_path", "")),
57
+ source_path=str(payload.get("source_path", "")),
58
+ module_stem=str(payload.get("module_stem", "")),
59
+ class_name=str(payload["class_name"]) if payload.get("class_name") else None,
60
+ registered_at_utc=str(payload.get("registered_at_utc", "")),
61
+ )
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return {
65
+ "kind": self.kind,
66
+ "name": self.name,
67
+ "installed_path": self.installed_path,
68
+ "source_path": self.source_path,
69
+ "module_stem": self.module_stem,
70
+ "class_name": self.class_name,
71
+ "registered_at_utc": self.registered_at_utc,
72
+ }
73
+
74
+
75
+ def get_plugin_home() -> Path:
76
+ """Return the writable local plugin home used by CLI installs."""
77
+ override = os.getenv(IINTS_PLUGIN_HOME_ENV)
78
+ if override:
79
+ return Path(override).expanduser().resolve()
80
+ return (Path.home() / ".iints" / "plugins").resolve()
81
+
82
+
83
+ def get_plugin_registry_path() -> Path:
84
+ return get_plugin_home() / "registry.json"
85
+
86
+
87
+ def _empty_registry() -> dict[str, Any]:
88
+ return {"schema_version": PLUGIN_REGISTRY_SCHEMA_VERSION, "plugins": []}
89
+
90
+
91
+ def _read_local_plugin_registry() -> dict[str, Any]:
92
+ path = get_plugin_registry_path()
93
+ if not path.exists():
94
+ return _empty_registry()
95
+ payload = json.loads(path.read_text(encoding="utf-8"))
96
+ if not isinstance(payload, dict):
97
+ raise ValueError(f"Plugin registry must be a JSON object: {path}")
98
+ plugins = payload.get("plugins", [])
99
+ if not isinstance(plugins, list):
100
+ raise ValueError(f"Plugin registry 'plugins' must be a list: {path}")
101
+ return {
102
+ "schema_version": str(payload.get("schema_version") or PLUGIN_REGISTRY_SCHEMA_VERSION),
103
+ "plugins": plugins,
104
+ }
105
+
106
+
107
+ def _write_local_plugin_registry(payload: Mapping[str, Any]) -> Path:
108
+ path = get_plugin_registry_path()
109
+ path.parent.mkdir(parents=True, exist_ok=True)
110
+ safe_payload = {
111
+ "schema_version": PLUGIN_REGISTRY_SCHEMA_VERSION,
112
+ "plugins": list(payload.get("plugins", [])),
113
+ }
114
+ path.write_text(json.dumps(safe_payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
115
+ return path
116
+
117
+
118
+ def _slugify_plugin_name(name: str) -> str:
119
+ slug = re.sub(r"[^a-zA-Z0-9._-]+", "-", name.strip().lower()).strip("-._")
120
+ return slug or "plugin"
121
+
122
+
123
+ def _kind_directory(kind: str) -> str:
124
+ return {
125
+ "algorithm": "algorithms",
126
+ "patient_model": "patient_models",
127
+ "data_source": "data_sources",
128
+ "validator": "validators",
129
+ }[kind]
130
+
131
+
132
+ def _module_name_for_path(path: Path) -> str:
133
+ digest = hashlib.sha256(str(path).encode("utf-8")).hexdigest()[:12]
134
+ return f"_iints_local_plugin_{path.stem}_{digest}_{time.monotonic_ns()}"
135
+
136
+
137
+ def _load_algorithm_class_from_path(path: Path) -> type[InsulinAlgorithm]:
138
+ resolved = path.expanduser().resolve()
139
+ if not resolved.is_file():
140
+ raise FileNotFoundError(f"Plugin file not found: {resolved}")
141
+ module_name = _module_name_for_path(resolved)
142
+ spec = importlib.util.spec_from_file_location(module_name, resolved)
143
+ if spec is None or spec.loader is None:
144
+ raise ImportError(f"Could not load plugin module spec: {resolved}")
145
+ module = importlib.util.module_from_spec(spec)
146
+ module.__dict__.setdefault("iints", importlib.import_module("iints"))
147
+ previous_module = sys.modules.get(module_name)
148
+ sys.modules[module_name] = module
149
+ try:
150
+ spec.loader.exec_module(module)
151
+ finally:
152
+ if previous_module is None:
153
+ sys.modules.pop(module_name, None)
154
+ else:
155
+ sys.modules[module_name] = previous_module
156
+ for _, obj in module.__dict__.items():
157
+ if isinstance(obj, type) and issubclass(obj, InsulinAlgorithm) and obj is not InsulinAlgorithm:
158
+ return obj
159
+ raise ImportError(f"No InsulinAlgorithm subclass found in plugin file: {resolved}")
160
+
161
+
162
+ def _store_local_plugin_record(record: LocalPluginRecord) -> Path:
163
+ payload = _read_local_plugin_registry()
164
+ records = [
165
+ item
166
+ for item in payload.get("plugins", [])
167
+ if not (
168
+ isinstance(item, dict)
169
+ and str(item.get("kind")) == record.kind
170
+ and str(item.get("name")).lower() == record.name.lower()
171
+ )
172
+ ]
173
+ records.append(record.to_dict())
174
+ payload["plugins"] = records
175
+ return _write_local_plugin_registry(payload)
176
+
177
+
178
+ def list_local_plugin_records(kind: str | None = None) -> list[LocalPluginRecord]:
179
+ payload = _read_local_plugin_registry()
180
+ records: list[LocalPluginRecord] = []
181
+ for item in payload.get("plugins", []):
182
+ if not isinstance(item, dict):
183
+ continue
184
+ record = LocalPluginRecord.from_dict(item)
185
+ if kind is None or record.kind == kind:
186
+ records.append(record)
187
+ return sorted(records, key=lambda record: (record.kind, record.name.lower()))
188
+
189
+
190
+ def install_file_plugin(kind: str, source_path: str | Path, name: str | None = None) -> LocalPluginRecord:
191
+ """Install a local extension file into the user plugin home."""
192
+ normalized_kind = kind.strip().lower().replace("-", "_")
193
+ if normalized_kind not in SUPPORTED_LOCAL_PLUGIN_KINDS:
194
+ supported = ", ".join(sorted(SUPPORTED_LOCAL_PLUGIN_KINDS))
195
+ raise ValueError(f"Unsupported plugin kind '{kind}'. Supported kinds: {supported}")
196
+
197
+ source = Path(source_path).expanduser().resolve()
198
+ if not source.is_file():
199
+ raise FileNotFoundError(f"Plugin file not found: {source}")
200
+ if source.suffix != ".py":
201
+ raise ValueError("Local plugins must be Python .py files.")
202
+
203
+ display_name = (name or source.stem).strip()
204
+ if not display_name:
205
+ raise ValueError("Plugin name cannot be empty.")
206
+ slug = _slugify_plugin_name(display_name)
207
+ target_dir = get_plugin_home() / _kind_directory(normalized_kind)
208
+ target_dir.mkdir(parents=True, exist_ok=True)
209
+ target = target_dir / f"{slug}.py"
210
+ if source != target:
211
+ shutil.copy2(source, target)
212
+
213
+ record = LocalPluginRecord(
214
+ kind=normalized_kind,
215
+ name=display_name,
216
+ installed_path=str(target),
217
+ source_path=str(source),
218
+ module_stem=target.stem,
219
+ class_name=None,
220
+ registered_at_utc=datetime.now(timezone.utc).isoformat(),
221
+ )
222
+ _store_local_plugin_record(record)
223
+ return record
224
+
225
+
226
+ def install_algorithm_plugin(source_path: str | Path, name: str | None = None) -> LocalPluginRecord:
227
+ """Validate and install a local InsulinAlgorithm plugin file."""
228
+ source = Path(source_path).expanduser().resolve()
229
+ algorithm_class = _load_algorithm_class_from_path(source)
230
+ instance = algorithm_class()
231
+ metadata = instance.get_algorithm_metadata()
232
+ display_name = (name or metadata.name or source.stem).strip()
233
+ record = install_file_plugin("algorithm", source, display_name)
234
+ record.class_name = algorithm_class.__name__
235
+ _store_local_plugin_record(record)
236
+ return record
237
+
238
+
239
+ def uninstall_local_plugin(name: str, kind: str | None = None, *, remove_file: bool = False) -> bool:
240
+ payload = _read_local_plugin_registry()
241
+ normalized_kind = kind.strip().lower().replace("-", "_") if kind else None
242
+ kept: list[dict[str, Any]] = []
243
+ removed = False
244
+ for item in payload.get("plugins", []):
245
+ if not isinstance(item, dict):
246
+ continue
247
+ record = LocalPluginRecord.from_dict(item)
248
+ matches_name = record.name.lower() == name.lower()
249
+ matches_kind = normalized_kind is None or record.kind == normalized_kind
250
+ if matches_name and matches_kind:
251
+ removed = True
252
+ if remove_file:
253
+ try:
254
+ Path(record.installed_path).unlink(missing_ok=True)
255
+ except Exception:
256
+ pass
257
+ continue
258
+ kept.append(record.to_dict())
259
+ if removed:
260
+ payload["plugins"] = kept
261
+ _write_local_plugin_registry(payload)
262
+ return removed
24
263
 
25
264
 
26
265
  def _load_entry_point(ep) -> AlgorithmListing:
@@ -100,4 +339,45 @@ def list_algorithm_plugins() -> List[AlgorithmListing]:
100
339
  except Exception:
101
340
  pass
102
341
 
342
+ # Local user-installed plugins
343
+ try:
344
+ for record in list_local_plugin_records("algorithm"):
345
+ plugin_path = Path(record.installed_path)
346
+ try:
347
+ algorithm_class = _load_algorithm_class_from_path(plugin_path)
348
+ instance = algorithm_class()
349
+ meta = instance.get_algorithm_metadata()
350
+ listings.append(
351
+ AlgorithmListing(
352
+ name=record.name or meta.name,
353
+ class_path=f"{plugin_path}:{algorithm_class.__name__}",
354
+ source="local",
355
+ metadata=meta,
356
+ plugin_path=str(plugin_path),
357
+ )
358
+ )
359
+ except Exception as exc:
360
+ listings.append(
361
+ AlgorithmListing(
362
+ name=record.name,
363
+ class_path=f"{plugin_path}:{record.class_name or ''}",
364
+ source="local",
365
+ metadata=None,
366
+ status="unavailable",
367
+ error=str(exc),
368
+ plugin_path=str(plugin_path),
369
+ )
370
+ )
371
+ except Exception as exc:
372
+ listings.append(
373
+ AlgorithmListing(
374
+ name="local plugin registry",
375
+ class_path=str(get_plugin_registry_path()),
376
+ source="local",
377
+ metadata=None,
378
+ status="unavailable",
379
+ error=str(exc),
380
+ )
381
+ )
382
+
103
383
  return listings