cli-anything-drms 0.1.0__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.
Files changed (24) hide show
  1. cli_anything_drms-0.1.0/PKG-INFO +10 -0
  2. cli_anything_drms-0.1.0/cli_anything/drms/__main__.py +4 -0
  3. cli_anything_drms-0.1.0/cli_anything/drms/core/case_notes.py +27 -0
  4. cli_anything_drms-0.1.0/cli_anything/drms/core/client.py +45 -0
  5. cli_anything_drms-0.1.0/cli_anything/drms/core/lab_reports.py +16 -0
  6. cli_anything_drms-0.1.0/cli_anything/drms/core/medication.py +20 -0
  7. cli_anything_drms-0.1.0/cli_anything/drms/core/patients.py +20 -0
  8. cli_anything_drms-0.1.0/cli_anything/drms/core/staff.py +12 -0
  9. cli_anything_drms-0.1.0/cli_anything/drms/core/stats.py +40 -0
  10. cli_anything_drms-0.1.0/cli_anything/drms/drms_cli.py +516 -0
  11. cli_anything_drms-0.1.0/cli_anything/drms/drms_feature/discovery.py +57 -0
  12. cli_anything_drms-0.1.0/cli_anything/drms/drms_feature/member.py +29 -0
  13. cli_anything_drms-0.1.0/cli_anything/drms/drms_feature/recommend.py +14 -0
  14. cli_anything_drms-0.1.0/cli_anything/drms/utils/auth.py +42 -0
  15. cli_anything_drms-0.1.0/cli_anything/drms/utils/repl_skin.py +68 -0
  16. cli_anything_drms-0.1.0/cli_anything/drms/utils/session.py +108 -0
  17. cli_anything_drms-0.1.0/cli_anything_drms.egg-info/PKG-INFO +10 -0
  18. cli_anything_drms-0.1.0/cli_anything_drms.egg-info/SOURCES.txt +22 -0
  19. cli_anything_drms-0.1.0/cli_anything_drms.egg-info/dependency_links.txt +1 -0
  20. cli_anything_drms-0.1.0/cli_anything_drms.egg-info/entry_points.txt +3 -0
  21. cli_anything_drms-0.1.0/cli_anything_drms.egg-info/requires.txt +2 -0
  22. cli_anything_drms-0.1.0/cli_anything_drms.egg-info/top_level.txt +1 -0
  23. cli_anything_drms-0.1.0/setup.cfg +4 -0
  24. cli_anything_drms-0.1.0/setup.py +19 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-anything-drms
3
+ Version: 0.1.0
4
+ Summary: CLI-Anything harness for the DRMS / HTM FastAPI backend
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: requests>=2.28
8
+ Dynamic: requires-dist
9
+ Dynamic: requires-python
10
+ Dynamic: summary
@@ -0,0 +1,4 @@
1
+ from cli_anything.drms.drms_cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
@@ -0,0 +1,27 @@
1
+ """Case notes domain operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def list_case_notes(client: DrmsClient, patient_uuid: str) -> Any:
8
+ return client.get(f"/patient/case_notes/v2/{patient_uuid}")
9
+
10
+
11
+ def get_case_note(client: DrmsClient, note_uuid: str) -> Any:
12
+ return client.get(f"/patient/case_notes/v2/note/{note_uuid}")
13
+
14
+
15
+ def generate_case_note_report(client: DrmsClient, patient_id: str, design_type: str = "new") -> Any:
16
+ return client.post(
17
+ f"/patient/case_notes/v2/report/{patient_id}",
18
+ data={"designtype": design_type},
19
+ )
20
+
21
+
22
+ def list_case_note_categories(client: DrmsClient) -> Any:
23
+ return client.get("/patient/case_notes/v2/categories")
24
+
25
+
26
+ def list_case_note_templates(client: DrmsClient) -> Any:
27
+ return client.get("/patient/case_notes/v2/templates")
@@ -0,0 +1,45 @@
1
+ """HTTP client wrapper for the DRMS / HTM FastAPI backend."""
2
+
3
+ import json
4
+ from typing import Any, Optional
5
+ import requests
6
+ from cli_anything.drms.utils.session import require_token
7
+
8
+
9
+ class DrmsClient:
10
+ def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
11
+ if base_url and token:
12
+ self.base_url = base_url.rstrip("/")
13
+ self.token = token
14
+ else:
15
+ self.base_url, self.token = require_token()
16
+
17
+ def _headers(self) -> dict:
18
+ return {"Authorization": f"Bearer {self.token}"}
19
+
20
+ def get(self, path: str, params: Optional[dict] = None) -> Any:
21
+ url = f"{self.base_url}{path}"
22
+ r = requests.get(url, headers=self._headers(), params=params, timeout=30)
23
+ r.raise_for_status()
24
+ return r.json()
25
+
26
+ def post(self, path: str, data: Any = None, files: Optional[dict] = None) -> Any:
27
+ url = f"{self.base_url}{path}"
28
+ if files:
29
+ r = requests.post(url, headers=self._headers(), data=data, files=files, timeout=60)
30
+ else:
31
+ r = requests.post(url, headers=self._headers(), json=data, timeout=30)
32
+ r.raise_for_status()
33
+ return r.json()
34
+
35
+ def put(self, path: str, data: Any = None) -> Any:
36
+ url = f"{self.base_url}{path}"
37
+ r = requests.put(url, headers=self._headers(), json=data, timeout=30)
38
+ r.raise_for_status()
39
+ return r.json()
40
+
41
+ def delete(self, path: str) -> Any:
42
+ url = f"{self.base_url}{path}"
43
+ r = requests.delete(url, headers=self._headers(), timeout=30)
44
+ r.raise_for_status()
45
+ return r.json()
@@ -0,0 +1,16 @@
1
+ """Lab report domain operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def get_lab_reports(client: DrmsClient, patient_uuid: str) -> Any:
8
+ return client.get(f"/health_info/lab_report/patient/{patient_uuid}")
9
+
10
+
11
+ def get_lab_report(client: DrmsClient, report_uuid: str) -> Any:
12
+ return client.get(f"/health_info/lab_report/get/{report_uuid}")
13
+
14
+
15
+ def get_lab_trends(client: DrmsClient, patient_uuid: str) -> Any:
16
+ return client.get(f"/health_info/lab_report/patient/{patient_uuid}/trends")
@@ -0,0 +1,20 @@
1
+ """Medication domain operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def get_medications(client: DrmsClient, patient_id: str) -> Any:
8
+ return client.get(f"/patient/v3/medication/{patient_id}")
9
+
10
+
11
+ def get_daily_medication(client: DrmsClient, patient_id: str) -> Any:
12
+ return client.get(f"/daily-medication/{patient_id}")
13
+
14
+
15
+ def get_medication_monitoring(client: DrmsClient, patient_id: str) -> Any:
16
+ return client.get(f"/medication-monitoring/{patient_id}")
17
+
18
+
19
+ def get_health_summary(client: DrmsClient, patient_id: str) -> Any:
20
+ return client.get(f"/patient/v5/health-summary/{patient_id}")
@@ -0,0 +1,20 @@
1
+ """Patient domain operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def search_patients(client: DrmsClient, term: str) -> Any:
8
+ return client.get(f"/patient/v2/search/{term}")
9
+
10
+
11
+ def list_patients(client: DrmsClient, page: int = 1) -> Any:
12
+ return client.get(f"/patient/v2/all/{page}")
13
+
14
+
15
+ def get_patient(client: DrmsClient, patient_id: str) -> Any:
16
+ return client.get(f"/patient/v2/get/{patient_id}")
17
+
18
+
19
+ def list_patients_by_condition(client: DrmsClient, condition: str, page: int = 1) -> Any:
20
+ return client.get("/patient-list-by-conditions/by-condition", params={"condition": condition, "page": page})
@@ -0,0 +1,12 @@
1
+ """Staff/organisation domain operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def list_staff(client: DrmsClient) -> Any:
8
+ return client.get("/admin/staffs")
9
+
10
+
11
+ def get_staff(client: DrmsClient, firebase_uid: str) -> Any:
12
+ return client.get(f"/admin/staff/{firebase_uid}")
@@ -0,0 +1,40 @@
1
+ """Organisation statistics — action logs, health conditions, CPD analytics."""
2
+
3
+ from typing import Any, List, Optional
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def get_action_stats(client: DrmsClient, start_date: Optional[str] = None, end_date: Optional[str] = None) -> Any:
8
+ params = {}
9
+ if start_date:
10
+ params["start_date"] = start_date
11
+ if end_date:
12
+ params["end_date"] = end_date
13
+ return client.get("/admin/action_stats_v2", params=params or None)
14
+
15
+
16
+ def get_condition_stats(client: DrmsClient, start_date: Optional[str] = None, end_date: Optional[str] = None) -> Any:
17
+ params = {}
18
+ if start_date:
19
+ params["start_date"] = start_date
20
+ if end_date:
21
+ params["end_date"] = end_date
22
+ return client.get("/admin/condition_stats", params=params or None)
23
+
24
+
25
+ def get_cpd_analytics(
26
+ client: DrmsClient,
27
+ start_date: Optional[str] = None,
28
+ end_date: Optional[str] = None,
29
+ premise_ids: Optional[List[str]] = None,
30
+ include_inactive: bool = False,
31
+ top_n: int = 100,
32
+ ) -> Any:
33
+ params: dict = {"top_n": top_n, "include_inactive_staff": str(include_inactive).lower()}
34
+ if start_date:
35
+ params["start_date"] = start_date
36
+ if end_date:
37
+ params["end_date"] = end_date
38
+ if premise_ids:
39
+ params["premise_ids"] = premise_ids
40
+ return client.get("/admin/cpd_analytics", params=params)
@@ -0,0 +1,516 @@
1
+ """DRMS / HTM CLI — member search, discovery, recommendations, and patient data."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import Optional
6
+
7
+ import click
8
+ import requests
9
+
10
+ from cli_anything.drms.core.client import DrmsClient
11
+ from cli_anything.drms.core import patients, lab_reports, case_notes, medication, staff, stats
12
+ from cli_anything.drms.drms_feature import member, discovery, recommend
13
+ from cli_anything.drms.utils.session import save_token, save_session, clear_session, get_session
14
+ from cli_anything.drms.utils import auth as _auth
15
+ from cli_anything.drms.utils.repl_skin import ReplSkin
16
+
17
+
18
+ # ── Helpers ───────────────────────────────────────────────────────────────────
19
+
20
+ def _client(ctx: click.Context) -> DrmsClient:
21
+ return DrmsClient()
22
+
23
+
24
+ def _out(ctx: click.Context, data) -> None:
25
+ if ctx.obj.get("json"):
26
+ click.echo(json.dumps(data, indent=2, default=str))
27
+ else:
28
+ if isinstance(data, (dict, list)):
29
+ click.echo(json.dumps(data, indent=2, default=str))
30
+ else:
31
+ click.echo(str(data))
32
+
33
+
34
+ def _handle_error(e: Exception) -> None:
35
+ if isinstance(e, requests.HTTPError):
36
+ try:
37
+ detail = e.response.json().get("detail", str(e))
38
+ except Exception:
39
+ detail = str(e)
40
+ click.echo(f"API error {e.response.status_code}: {detail}", err=True)
41
+ else:
42
+ click.echo(f"Error: {e}", err=True)
43
+ sys.exit(1)
44
+
45
+
46
+ # ── Root group ────────────────────────────────────────────────────────────────
47
+
48
+ @click.group()
49
+ @click.option("--json", "json_mode", is_flag=True, default=False, help="Output raw JSON.")
50
+ @click.pass_context
51
+ def cli(ctx: click.Context, json_mode: bool) -> None:
52
+ """DRMS / HTM CLI — member search, discovery, drug recommendations, and patient data."""
53
+ ctx.ensure_object(dict)
54
+ ctx.obj["json"] = json_mode
55
+
56
+
57
+ # ── Auth ──────────────────────────────────────────────────────────────────────
58
+
59
+ @cli.command()
60
+ @click.option("--url", default="https://htm.aipharm.xyz", show_default=True, help="HTM API base URL.")
61
+ @click.option("--token", default=None, hidden=True, help="Pre-issued token (CI/agent use only).")
62
+ def login(url: str, token: str) -> None:
63
+ """Log in to DRMS / HTM. Prompts for email and password."""
64
+ if token:
65
+ save_token(url, token)
66
+ click.echo(f"Session saved (token mode). Base URL: {url}")
67
+ return
68
+
69
+ email = click.prompt("Email")
70
+ password = click.prompt("Password", hide_input=True)
71
+
72
+ click.echo("Authenticating…")
73
+ try:
74
+ resp = _auth.login(url, email, password)
75
+ except ValueError as e:
76
+ click.echo(str(e), err=True)
77
+ sys.exit(1)
78
+
79
+ id_token = resp["firebase_token"]
80
+ local_id = resp["user_id"]
81
+
82
+ save_session(url, id_token, resp["refresh_token"], local_id, email)
83
+ click.echo(f"Logged in as {email}")
84
+
85
+
86
+ @cli.command()
87
+ def logout() -> None:
88
+ """Log out and revoke tokens server-side."""
89
+ data = get_session()
90
+ if data.get("token") and data.get("base_url"):
91
+ _auth.logout_server(data["base_url"], data["token"])
92
+ clear_session()
93
+ click.echo("Logged out.")
94
+
95
+
96
+ @cli.command()
97
+ def whoami() -> None:
98
+ """Show current session info."""
99
+ data = get_session()
100
+ if not data or not data.get("token"):
101
+ click.echo("Not logged in.")
102
+ return
103
+ click.echo(f"URL : {data.get('base_url', 'n/a')}")
104
+ click.echo(f"Email: {data.get('email', '(token mode)')}")
105
+ issued = data.get("issued_at")
106
+ if issued:
107
+ import time
108
+ age_min = int((time.time() - issued) / 60)
109
+ click.echo(f"Token: {age_min} min old (auto-refreshes at 55 min)")
110
+ else:
111
+ click.echo(f"Token: {data.get('token', '')[:24]}… (manual token mode)")
112
+
113
+
114
+ # ── Patient commands ──────────────────────────────────────────────────────────
115
+
116
+ @cli.group()
117
+ def patient() -> None:
118
+ """Patient search and lookup."""
119
+
120
+
121
+ @patient.command("search")
122
+ @click.argument("term")
123
+ @click.pass_context
124
+ def patient_search(ctx, term: str) -> None:
125
+ """Search patients by name, IC, or ref ID."""
126
+ try:
127
+ _out(ctx, patients.search_patients(_client(ctx), term))
128
+ except Exception as e:
129
+ _handle_error(e)
130
+
131
+
132
+ @patient.command("list")
133
+ @click.option("--page", default=1, show_default=True)
134
+ @click.pass_context
135
+ def patient_list(ctx, page: int) -> None:
136
+ """List patients (paginated)."""
137
+ try:
138
+ _out(ctx, patients.list_patients(_client(ctx), page))
139
+ except Exception as e:
140
+ _handle_error(e)
141
+
142
+
143
+ @patient.command("get")
144
+ @click.argument("patient_id")
145
+ @click.pass_context
146
+ def patient_get(ctx, patient_id: str) -> None:
147
+ """Get a patient by ID."""
148
+ try:
149
+ _out(ctx, patients.get_patient(_client(ctx), patient_id))
150
+ except Exception as e:
151
+ _handle_error(e)
152
+
153
+
154
+ @patient.command("by-condition")
155
+ @click.argument("condition")
156
+ @click.option("--page", default=1, show_default=True)
157
+ @click.pass_context
158
+ def patient_by_condition(ctx, condition: str, page: int) -> None:
159
+ """List patients by medical condition tag."""
160
+ try:
161
+ _out(ctx, patients.list_patients_by_condition(_client(ctx), condition, page))
162
+ except Exception as e:
163
+ _handle_error(e)
164
+
165
+
166
+ # ── Lab report commands ───────────────────────────────────────────────────────
167
+
168
+ @cli.group()
169
+ def lab() -> None:
170
+ """Lab report data and trends."""
171
+
172
+
173
+ @lab.command("list")
174
+ @click.argument("patient_uuid")
175
+ @click.pass_context
176
+ def lab_list(ctx, patient_uuid: str) -> None:
177
+ """List all lab reports for a patient."""
178
+ try:
179
+ _out(ctx, lab_reports.get_lab_reports(_client(ctx), patient_uuid))
180
+ except Exception as e:
181
+ _handle_error(e)
182
+
183
+
184
+ @lab.command("get")
185
+ @click.argument("report_uuid")
186
+ @click.pass_context
187
+ def lab_get(ctx, report_uuid: str) -> None:
188
+ """Get a single lab report with all results."""
189
+ try:
190
+ _out(ctx, lab_reports.get_lab_report(_client(ctx), report_uuid))
191
+ except Exception as e:
192
+ _handle_error(e)
193
+
194
+
195
+ @lab.command("trends")
196
+ @click.argument("patient_uuid")
197
+ @click.pass_context
198
+ def lab_trends(ctx, patient_uuid: str) -> None:
199
+ """Show lab result trends for a patient."""
200
+ try:
201
+ _out(ctx, lab_reports.get_lab_trends(_client(ctx), patient_uuid))
202
+ except Exception as e:
203
+ _handle_error(e)
204
+
205
+
206
+ # ── Case note commands ────────────────────────────────────────────────────────
207
+
208
+ @cli.group()
209
+ def note() -> None:
210
+ """Case note lookup and reference data."""
211
+
212
+
213
+ @note.command("list")
214
+ @click.argument("patient_uuid")
215
+ @click.pass_context
216
+ def note_list(ctx, patient_uuid: str) -> None:
217
+ """List all case notes for a patient."""
218
+ try:
219
+ _out(ctx, case_notes.list_case_notes(_client(ctx), patient_uuid))
220
+ except Exception as e:
221
+ _handle_error(e)
222
+
223
+
224
+ @note.command("get")
225
+ @click.argument("note_uuid")
226
+ @click.pass_context
227
+ def note_get(ctx, note_uuid: str) -> None:
228
+ """Get a single case note."""
229
+ try:
230
+ _out(ctx, case_notes.get_case_note(_client(ctx), note_uuid))
231
+ except Exception as e:
232
+ _handle_error(e)
233
+
234
+
235
+ @note.command("report")
236
+ @click.argument("patient_id")
237
+ @click.option("--design", default="new", type=click.Choice(["old", "new", "markdown"]), show_default=True)
238
+ @click.pass_context
239
+ def note_report(ctx, patient_id: str, design: str) -> None:
240
+ """Generate a PDF case note report for a patient."""
241
+ try:
242
+ _out(ctx, case_notes.generate_case_note_report(_client(ctx), patient_id, design))
243
+ except Exception as e:
244
+ _handle_error(e)
245
+
246
+
247
+ # ── Medication commands ───────────────────────────────────────────────────────
248
+
249
+ @cli.group()
250
+ def med() -> None:
251
+ """Medication and health summary."""
252
+
253
+
254
+ @med.command("list")
255
+ @click.argument("patient_id")
256
+ @click.pass_context
257
+ def med_list(ctx, patient_id: str) -> None:
258
+ """List medications for a patient."""
259
+ try:
260
+ _out(ctx, medication.get_medications(_client(ctx), patient_id))
261
+ except Exception as e:
262
+ _handle_error(e)
263
+
264
+
265
+ @med.command("daily")
266
+ @click.argument("patient_id")
267
+ @click.pass_context
268
+ def med_daily(ctx, patient_id: str) -> None:
269
+ """Show daily medication schedule for a patient."""
270
+ try:
271
+ _out(ctx, medication.get_daily_medication(_client(ctx), patient_id))
272
+ except Exception as e:
273
+ _handle_error(e)
274
+
275
+
276
+ @med.command("summary")
277
+ @click.argument("patient_id")
278
+ @click.pass_context
279
+ def med_summary(ctx, patient_id: str) -> None:
280
+ """Show health summary for a patient."""
281
+ try:
282
+ _out(ctx, medication.get_health_summary(_client(ctx), patient_id))
283
+ except Exception as e:
284
+ _handle_error(e)
285
+
286
+
287
+ # ── Staff commands ────────────────────────────────────────────────────────────
288
+
289
+ @cli.group("staff")
290
+ def staff_cmd() -> None:
291
+ """Staff and organisation lookup."""
292
+
293
+
294
+ @staff_cmd.command("list")
295
+ @click.pass_context
296
+ def staff_list(ctx) -> None:
297
+ """List all staff in the organisation."""
298
+ try:
299
+ _out(ctx, staff.list_staff(_client(ctx)))
300
+ except Exception as e:
301
+ _handle_error(e)
302
+
303
+
304
+ @staff_cmd.command("get")
305
+ @click.argument("firebase_uid")
306
+ @click.pass_context
307
+ def staff_get(ctx, firebase_uid: str) -> None:
308
+ """Get a staff member by Firebase UID."""
309
+ try:
310
+ _out(ctx, staff.get_staff(_client(ctx), firebase_uid))
311
+ except Exception as e:
312
+ _handle_error(e)
313
+
314
+
315
+ # ── Stats commands ────────────────────────────────────────────────────────────
316
+
317
+ @cli.group("stats")
318
+ def stats_group() -> None:
319
+ """Organisation statistics (admin / branch manager only)."""
320
+
321
+
322
+ @stats_group.command("actions")
323
+ @click.option("--start", default=None, help="Start date ISO-8601 (default: 7 days ago).")
324
+ @click.option("--end", default=None, help="End date ISO-8601 (default: now).")
325
+ @click.pass_context
326
+ def stats_actions(ctx, start, end) -> None:
327
+ """Action-log analytics: staff performance, branch activeness, behaviour insights."""
328
+ try:
329
+ _out(ctx, stats.get_action_stats(_client(ctx), start, end))
330
+ except Exception as e:
331
+ _handle_error(e)
332
+
333
+
334
+ @stats_group.command("conditions")
335
+ @click.option("--start", default=None, help="Start date ISO-8601 (default: 7 days ago).")
336
+ @click.option("--end", default=None, help="End date ISO-8601 (default: now).")
337
+ @click.pass_context
338
+ def stats_conditions(ctx, start, end) -> None:
339
+ """Health condition frequency: counts by condition, branch, age group, and gender."""
340
+ try:
341
+ _out(ctx, stats.get_condition_stats(_client(ctx), start, end))
342
+ except Exception as e:
343
+ _handle_error(e)
344
+
345
+
346
+ @stats_group.command("cpd")
347
+ @click.option("--start", default=None)
348
+ @click.option("--end", default=None)
349
+ @click.option("--premise", "premise_ids", multiple=True)
350
+ @click.option("--include-inactive", is_flag=True, default=False)
351
+ @click.option("--top-n", default=100, show_default=True)
352
+ @click.pass_context
353
+ def stats_cpd(ctx, start, end, premise_ids, include_inactive, top_n) -> None:
354
+ """CPD analytics: enrolments, completions, leaderboard, at-risk cohort."""
355
+ try:
356
+ _out(ctx, stats.get_cpd_analytics(_client(ctx), start, end, list(premise_ids) or None, include_inactive, top_n))
357
+ except Exception as e:
358
+ _handle_error(e)
359
+
360
+
361
+ # ── HTM Member commands ───────────────────────────────────────────────────────
362
+
363
+ @cli.group("member")
364
+ def member_group() -> None:
365
+ """HTM member search and purchase history."""
366
+
367
+
368
+ @member_group.command("search")
369
+ @click.argument("query")
370
+ @click.option("--by-name", is_flag=True, default=False, help="Search by name instead of IC.")
371
+ @click.pass_context
372
+ def member_search(ctx, query: str, by_name: bool) -> None:
373
+ """Search HTM members by IC number (or name with --by-name)."""
374
+ try:
375
+ _out(ctx, member.search_members(_client(ctx), query, by_name))
376
+ except Exception as e:
377
+ _handle_error(e)
378
+
379
+
380
+ @member_group.command("search-card")
381
+ @click.argument("query")
382
+ @click.option("--by-name", is_flag=True, default=False)
383
+ @click.pass_context
384
+ def member_search_card(ctx, query: str, by_name: bool) -> None:
385
+ """Search HTM members by loyalty card number."""
386
+ try:
387
+ _out(ctx, member.search_members_by_card(_client(ctx), query, by_name))
388
+ except Exception as e:
389
+ _handle_error(e)
390
+
391
+
392
+ @member_group.command("purchases")
393
+ @click.argument("member_id")
394
+ @click.pass_context
395
+ def member_purchases(ctx, member_id: str) -> None:
396
+ """Show purchase history for a member."""
397
+ try:
398
+ _out(ctx, member.get_purchases(_client(ctx), member_id))
399
+ except Exception as e:
400
+ _handle_error(e)
401
+
402
+
403
+ @member_group.command("labs")
404
+ @click.argument("patient_id")
405
+ @click.pass_context
406
+ def member_labs(ctx, patient_id: str) -> None:
407
+ """Show HTM lab reports for a patient."""
408
+ try:
409
+ _out(ctx, member.get_lab_reports(_client(ctx), patient_id))
410
+ except Exception as e:
411
+ _handle_error(e)
412
+
413
+
414
+ @member_group.command("labs-all")
415
+ @click.pass_context
416
+ def member_labs_all(ctx) -> None:
417
+ """List all HTM lab reports for the organisation."""
418
+ try:
419
+ _out(ctx, member.list_lab_reports(_client(ctx)))
420
+ except Exception as e:
421
+ _handle_error(e)
422
+
423
+
424
+ # ── Discovery commands ────────────────────────────────────────────────────────
425
+
426
+ @cli.group("discovery")
427
+ def discovery_group() -> None:
428
+ """DRMS illness discovery — symptoms, illness search, checker, analyses."""
429
+
430
+
431
+ @discovery_group.command("complaints")
432
+ @click.option("--invalidate", is_flag=True, default=False, help="Bust the server-side cache.")
433
+ @click.pass_context
434
+ def discovery_complaints(ctx, invalidate: bool) -> None:
435
+ """List all symptoms and risk factors."""
436
+ try:
437
+ _out(ctx, discovery.get_complaints(_client(ctx), invalidate))
438
+ except Exception as e:
439
+ _handle_error(e)
440
+
441
+
442
+ @discovery_group.command("illnesses")
443
+ @click.option("--q", default=None, help="Keyword search.")
444
+ @click.option("--category", default=None, help="Filter by category.")
445
+ @click.option("--limit", default=20, show_default=True)
446
+ @click.option("--offset", default=0, show_default=True)
447
+ @click.pass_context
448
+ def discovery_illnesses(ctx, q, category, limit, offset) -> None:
449
+ """Search illnesses by keyword or category."""
450
+ try:
451
+ _out(ctx, discovery.search_illnesses(_client(ctx), q, category, limit, offset))
452
+ except Exception as e:
453
+ _handle_error(e)
454
+
455
+
456
+ @discovery_group.command("recommendations")
457
+ @click.argument("illness_id", type=int)
458
+ @click.pass_context
459
+ def discovery_recommendations(ctx, illness_id: int) -> None:
460
+ """Show health recommendations for a specific illness ID."""
461
+ try:
462
+ _out(ctx, discovery.get_illness_recommendations(_client(ctx), illness_id))
463
+ except Exception as e:
464
+ _handle_error(e)
465
+
466
+
467
+ @discovery_group.command("analyses")
468
+ @click.argument("patient_id")
469
+ @click.pass_context
470
+ def discovery_analyses(ctx, patient_id: str) -> None:
471
+ """Show all saved discovery analyses for a patient."""
472
+ try:
473
+ _out(ctx, discovery.get_patient_analyses(_client(ctx), patient_id))
474
+ except Exception as e:
475
+ _handle_error(e)
476
+
477
+
478
+ @discovery_group.command("check")
479
+ @click.argument("symptom_ids", nargs=-1, type=int, required=True)
480
+ @click.pass_context
481
+ def discovery_check(ctx, symptom_ids) -> None:
482
+ """Run illness checker for the given symptom IDs (space-separated)."""
483
+ try:
484
+ _out(ctx, discovery.check_illnesses(_client(ctx), list(symptom_ids)))
485
+ except Exception as e:
486
+ _handle_error(e)
487
+
488
+
489
+ # ── Recommend commands ────────────────────────────────────────────────────────
490
+
491
+ @cli.group("recommend")
492
+ def recommend_group() -> None:
493
+ """HTM ML-based drug/product recommendations."""
494
+
495
+
496
+ @recommend_group.command("get")
497
+ @click.argument("query_items")
498
+ @click.option("--top-n", default=3, show_default=True, help="Recommendations per item.")
499
+ @click.pass_context
500
+ def recommend_get(ctx, query_items: str, top_n: int) -> None:
501
+ """Get ML recommendations for comma-separated product/drug names."""
502
+ try:
503
+ _out(ctx, recommend.get_recommendations(_client(ctx), query_items, top_n))
504
+ except Exception as e:
505
+ _handle_error(e)
506
+
507
+
508
+ # ── REPL ──────────────────────────────────────────────────────────────────────
509
+
510
+ @cli.command("repl")
511
+ @click.option("--json", "json_mode", is_flag=True, default=False)
512
+ @click.pass_context
513
+ def repl(ctx, json_mode: bool) -> None:
514
+ """Start interactive REPL session."""
515
+ skin = ReplSkin(cli, json_mode=json_mode or ctx.obj.get("json", False))
516
+ skin.run()
@@ -0,0 +1,57 @@
1
+ """DRMS Discovery v2 domain operations."""
2
+
3
+ from typing import Any, List, Optional
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def get_complaints(client: DrmsClient, invalidate: bool = False) -> Any:
8
+ """GET /discovery/v2/complaints — all symptoms and risk factors."""
9
+ return client.get("/discovery/v2/complaints", params={"invalidate": invalidate})
10
+
11
+
12
+ def get_screening_data(client: DrmsClient) -> Any:
13
+ """GET /discovery/v2/screening — full screening dataset (illnesses, symptoms, risk factors)."""
14
+ return client.get("/discovery/v2/screening")
15
+
16
+
17
+ def search_illnesses(
18
+ client: DrmsClient,
19
+ q: Optional[str] = None,
20
+ category: Optional[str] = None,
21
+ limit: int = 20,
22
+ offset: int = 0,
23
+ ) -> Any:
24
+ """GET /discovery/v2/possible_illnesses_list — search illnesses by keyword or category."""
25
+ params: dict = {"limit": limit, "offset": offset}
26
+ if q:
27
+ params["q"] = q
28
+ if category:
29
+ params["category"] = category
30
+ return client.get("/discovery/v2/possible_illnesses_list", params=params)
31
+
32
+
33
+ def get_illness_recommendations(client: DrmsClient, illness_id: int) -> Any:
34
+ """GET /discovery/v2/illness_recommendation/{illness_id} — health recommendations for an illness."""
35
+ return client.get(f"/discovery/v2/illness_recommendation/{illness_id}")
36
+
37
+
38
+ def get_patient_analyses(client: DrmsClient, patient_id: str) -> Any:
39
+ """GET /discovery/v2/get_all_analyses/{patient_id} — all saved analyses for a patient."""
40
+ return client.get(f"/discovery/v2/get_all_analyses/{patient_id}")
41
+
42
+
43
+ def get_analysis(client: DrmsClient, patient_id: str, analyzed_id: str) -> Any:
44
+ """GET /discovery/v2/get_analysis/{patient_id}/{analyzed_id} — single analysis record."""
45
+ return client.get(f"/discovery/v2/get_analysis/{patient_id}/{analyzed_id}")
46
+
47
+
48
+ def check_illnesses(
49
+ client: DrmsClient,
50
+ symptom_ids: List[int],
51
+ known_illness_ids: Optional[List[int]] = None,
52
+ ) -> Any:
53
+ """POST /discovery/v2/checker — match illnesses from selected symptoms."""
54
+ return client.post("/discovery/v2/checker", data={
55
+ "main_sympt_ids": symptom_ids,
56
+ "known_illness_ids": known_illness_ids or [],
57
+ })
@@ -0,0 +1,29 @@
1
+ """HTM member search and purchase history operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def search_members(client: DrmsClient, query: str, by_name: bool = False) -> Any:
8
+ """GET /htm/search_members_v2 — search HTM members by IC or name."""
9
+ return client.get("/htm/search_members_v2", params={"query": query, "searchname": by_name})
10
+
11
+
12
+ def search_members_by_card(client: DrmsClient, query: str, by_name: bool = False) -> Any:
13
+ """GET /htm/search_members_by_card_v2 — search members by loyalty card."""
14
+ return client.get("/htm/search_members_by_card_v2", params={"query": query, "searchname": by_name})
15
+
16
+
17
+ def get_purchases(client: DrmsClient, member_id: str) -> Any:
18
+ """GET /htm/v3/search_purchases — purchase history for a member (v3 with remarks)."""
19
+ return client.get("/htm/v3/search_purchases", params={"member_id": member_id})
20
+
21
+
22
+ def get_lab_reports(client: DrmsClient, patient_id: str) -> Any:
23
+ """GET /htm/htm_labreports/by_patient_id — HTM lab reports for a patient."""
24
+ return client.get("/htm/htm_labreports/by_patient_id", params={"PatientID": patient_id})
25
+
26
+
27
+ def list_lab_reports(client: DrmsClient) -> Any:
28
+ """GET /htm/htm_labreports — all uploaded lab reports for the organisation."""
29
+ return client.get("/htm/htm_labreports")
@@ -0,0 +1,14 @@
1
+ """HTM ML-based drug/product recommendation operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.drms.core.client import DrmsClient
5
+
6
+
7
+ def get_recommendations(client: DrmsClient, query_items: str, top_n: int = 3) -> Any:
8
+ """GET /htm/recommendations — ML-based product/drug recommendations.
9
+
10
+ Args:
11
+ query_items: Comma-separated product/drug names or codes.
12
+ top_n: Number of recommendations to return per item.
13
+ """
14
+ return client.get("/htm/recommendations", params={"query_items": query_items, "top_n": top_n})
@@ -0,0 +1,42 @@
1
+ """Backend auth — calls /v1/auth/* endpoints instead of Firebase directly."""
2
+
3
+ import requests
4
+
5
+
6
+ def login(base_url: str, email: str, password: str) -> dict:
7
+ """POST /v1/auth/login → {firebase_token, refresh_token, user_id, email, display_name}"""
8
+ resp = requests.post(
9
+ f"{base_url}/v1/auth/login",
10
+ json={"email": email, "password": password, "origin": "drms-cli"},
11
+ timeout=15,
12
+ )
13
+ if resp.status_code == 401:
14
+ detail = resp.json().get("detail", "Authentication failed.")
15
+ raise ValueError(detail)
16
+ resp.raise_for_status()
17
+ return resp.json()
18
+
19
+
20
+ def refresh(base_url: str, refresh_token: str) -> dict:
21
+ """POST /v1/auth/refresh → {firebase_token, refresh_token}"""
22
+ resp = requests.post(
23
+ f"{base_url}/v1/auth/refresh",
24
+ json={"refresh_token": refresh_token},
25
+ timeout=15,
26
+ )
27
+ if resp.status_code in (400, 401):
28
+ raise ValueError("Refresh token expired — please log in again.")
29
+ resp.raise_for_status()
30
+ return resp.json()
31
+
32
+
33
+ def logout_server(base_url: str, token: str) -> None:
34
+ """POST /v1/auth/logout — revokes all refresh tokens server-side."""
35
+ try:
36
+ requests.post(
37
+ f"{base_url}/v1/auth/logout",
38
+ headers={"Authorization": f"Bearer {token}"},
39
+ timeout=10,
40
+ )
41
+ except Exception:
42
+ pass
@@ -0,0 +1,68 @@
1
+ """Unified REPL skin for the DRMS CLI harness."""
2
+
3
+ from typing import Optional
4
+ import click
5
+
6
+
7
+ BANNER = """\
8
+ ╔══════════════════════════════════════════╗
9
+ ║ DRMS / HTM CLI • type 'help' ║
10
+ ╚══════════════════════════════════════════╝
11
+ """
12
+
13
+ PROMPT = "drms> "
14
+
15
+
16
+ class ReplSkin:
17
+ """Interactive REPL that delegates lines to a Click group."""
18
+
19
+ def __init__(self, cli_group: click.Group, json_mode: bool = False):
20
+ self.cli = cli_group
21
+ self.json_mode = json_mode
22
+ self._history: list[str] = []
23
+
24
+ def push_history(self, line: str) -> None:
25
+ self._history.append(line)
26
+
27
+ def undo(self) -> Optional[str]:
28
+ return self._history.pop() if self._history else None
29
+
30
+ def _dispatch(self, line: str) -> None:
31
+ args = line.strip().split()
32
+ if not args:
33
+ return
34
+ if self.json_mode and "--json" not in args:
35
+ args.append("--json")
36
+ try:
37
+ self.cli.main(args=args, standalone_mode=False)
38
+ except click.UsageError as e:
39
+ click.echo(f"Error: {e}", err=True)
40
+ except SystemExit:
41
+ pass
42
+ except Exception as e:
43
+ click.echo(f"Error: {e}", err=True)
44
+
45
+ def run(self) -> None:
46
+ click.echo(BANNER)
47
+ while True:
48
+ try:
49
+ line = input(PROMPT).strip()
50
+ except (EOFError, KeyboardInterrupt):
51
+ click.echo("\nBye.")
52
+ break
53
+
54
+ if not line:
55
+ continue
56
+ if line in ("exit", "quit", "q"):
57
+ click.echo("Bye.")
58
+ break
59
+ if line == "undo":
60
+ last = self.undo()
61
+ click.echo(f"Removed from history: {last}" if last else "Nothing to undo.")
62
+ continue
63
+ if line in ("help", "?"):
64
+ self._dispatch("--help")
65
+ continue
66
+
67
+ self.push_history(line)
68
+ self._dispatch(line)
@@ -0,0 +1,108 @@
1
+ """
2
+ Session management: persist credentials to ~/.drms_session.json.
3
+ Locked writes prevent concurrent JSON corruption.
4
+ Auto-refreshes the token when it is older than 55 minutes
5
+ by calling the backend /v1/auth/refresh endpoint.
6
+ """
7
+
8
+ import json
9
+ import fcntl
10
+ import os
11
+ import time
12
+ from pathlib import Path
13
+
14
+ SESSION_FILE = Path.home() / ".drms_session.json"
15
+
16
+ _TOKEN_TTL = 55 * 60 # refresh 5 min before Firebase's 1-hour expiry
17
+
18
+
19
+ def _load_raw() -> dict:
20
+ if not SESSION_FILE.exists():
21
+ return {}
22
+ try:
23
+ with open(SESSION_FILE, "r") as f:
24
+ return json.load(f)
25
+ except (json.JSONDecodeError, OSError):
26
+ return {}
27
+
28
+
29
+ def _save_raw(data: dict) -> None:
30
+ tmp = SESSION_FILE.with_suffix(".tmp")
31
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
32
+ with os.fdopen(fd, "w") as f:
33
+ fcntl.flock(f, fcntl.LOCK_EX)
34
+ json.dump(data, f, indent=2)
35
+ fcntl.flock(f, fcntl.LOCK_UN)
36
+ tmp.replace(SESSION_FILE)
37
+
38
+
39
+ def get_session() -> dict:
40
+ return _load_raw()
41
+
42
+
43
+ def save_session(
44
+ base_url: str,
45
+ token: str,
46
+ refresh_token: str,
47
+ local_id: str,
48
+ email: str,
49
+ ) -> None:
50
+ data = _load_raw()
51
+ data.update(
52
+ base_url=base_url.rstrip("/"),
53
+ token=token,
54
+ refresh_token=refresh_token,
55
+ local_id=local_id,
56
+ email=email,
57
+ issued_at=time.time(),
58
+ )
59
+ _save_raw(data)
60
+
61
+
62
+ def save_token(base_url: str, token: str) -> None:
63
+ """Agent / CI mode: store only base_url + token, no auto-refresh."""
64
+ data = _load_raw()
65
+ data["base_url"] = base_url.rstrip("/")
66
+ data["token"] = token
67
+ data.pop("issued_at", None)
68
+ _save_raw(data)
69
+
70
+
71
+ def clear_session() -> None:
72
+ if SESSION_FILE.exists():
73
+ SESSION_FILE.unlink()
74
+
75
+
76
+ def require_token() -> tuple[str, str]:
77
+ """Return (base_url, token). Auto-refreshes via backend if token is near expiry."""
78
+ data = _load_raw()
79
+ base_url = data.get("base_url")
80
+ token = data.get("token")
81
+
82
+ if not base_url or not token:
83
+ raise RuntimeError("Not logged in. Run: drms-agent login")
84
+
85
+ issued_at = data.get("issued_at")
86
+ refresh_token = data.get("refresh_token")
87
+
88
+ if issued_at and refresh_token and (time.time() - issued_at) > _TOKEN_TTL:
89
+ try:
90
+ from cli_anything.drms.utils.auth import refresh as backend_refresh
91
+ new = backend_refresh(base_url, refresh_token)
92
+ token = new["firebase_token"]
93
+ data["token"] = token
94
+ data["refresh_token"] = new["refresh_token"]
95
+ data["issued_at"] = time.time()
96
+ _save_raw(data)
97
+ except Exception:
98
+ raise RuntimeError(
99
+ "Session expired and token refresh failed. Run: drms-agent login"
100
+ )
101
+
102
+ return base_url, token
103
+
104
+
105
+ def update_session_key(key: str, value) -> None:
106
+ data = _load_raw()
107
+ data[key] = value
108
+ _save_raw(data)
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-anything-drms
3
+ Version: 0.1.0
4
+ Summary: CLI-Anything harness for the DRMS / HTM FastAPI backend
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: requests>=2.28
8
+ Dynamic: requires-dist
9
+ Dynamic: requires-python
10
+ Dynamic: summary
@@ -0,0 +1,22 @@
1
+ setup.py
2
+ cli_anything/drms/__main__.py
3
+ cli_anything/drms/drms_cli.py
4
+ cli_anything/drms/core/case_notes.py
5
+ cli_anything/drms/core/client.py
6
+ cli_anything/drms/core/lab_reports.py
7
+ cli_anything/drms/core/medication.py
8
+ cli_anything/drms/core/patients.py
9
+ cli_anything/drms/core/staff.py
10
+ cli_anything/drms/core/stats.py
11
+ cli_anything/drms/drms_feature/discovery.py
12
+ cli_anything/drms/drms_feature/member.py
13
+ cli_anything/drms/drms_feature/recommend.py
14
+ cli_anything/drms/utils/auth.py
15
+ cli_anything/drms/utils/repl_skin.py
16
+ cli_anything/drms/utils/session.py
17
+ cli_anything_drms.egg-info/PKG-INFO
18
+ cli_anything_drms.egg-info/SOURCES.txt
19
+ cli_anything_drms.egg-info/dependency_links.txt
20
+ cli_anything_drms.egg-info/entry_points.txt
21
+ cli_anything_drms.egg-info/requires.txt
22
+ cli_anything_drms.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ drms = cli_anything.drms.drms_cli:cli
3
+ drms-agent = cli_anything.drms.drms_cli:cli
@@ -0,0 +1,2 @@
1
+ click>=8.0
2
+ requests>=2.28
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from setuptools import setup, find_namespace_packages
2
+
3
+ setup(
4
+ name="cli-anything-drms",
5
+ version="0.1.0",
6
+ description="CLI-Anything harness for the DRMS / HTM FastAPI backend",
7
+ packages=find_namespace_packages(include=["cli_anything.*"]),
8
+ install_requires=[
9
+ "click>=8.0",
10
+ "requests>=2.28",
11
+ ],
12
+ entry_points={
13
+ "console_scripts": [
14
+ "drms-agent=cli_anything.drms.drms_cli:cli",
15
+ "drms=cli_anything.drms.drms_cli:cli",
16
+ ]
17
+ },
18
+ python_requires=">=3.10",
19
+ )