cli-anything-medcare 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 (25) hide show
  1. cli_anything_medcare-0.1.0/PKG-INFO +10 -0
  2. cli_anything_medcare-0.1.0/cli_anything/medcare/__init__.py +0 -0
  3. cli_anything_medcare-0.1.0/cli_anything/medcare/__main__.py +4 -0
  4. cli_anything_medcare-0.1.0/cli_anything/medcare/core/__init__.py +0 -0
  5. cli_anything_medcare-0.1.0/cli_anything/medcare/core/case_notes.py +63 -0
  6. cli_anything_medcare-0.1.0/cli_anything/medcare/core/client.py +45 -0
  7. cli_anything_medcare-0.1.0/cli_anything/medcare/core/lab_reports.py +46 -0
  8. cli_anything_medcare-0.1.0/cli_anything/medcare/core/medication.py +20 -0
  9. cli_anything_medcare-0.1.0/cli_anything/medcare/core/patients.py +32 -0
  10. cli_anything_medcare-0.1.0/cli_anything/medcare/core/staff.py +12 -0
  11. cli_anything_medcare-0.1.0/cli_anything/medcare/medcare_cli.py +419 -0
  12. cli_anything_medcare-0.1.0/cli_anything/medcare/tests/test_core.py +159 -0
  13. cli_anything_medcare-0.1.0/cli_anything/medcare/tests/test_full_e2e.py +182 -0
  14. cli_anything_medcare-0.1.0/cli_anything/medcare/utils/auth.py +42 -0
  15. cli_anything_medcare-0.1.0/cli_anything/medcare/utils/repl_skin.py +76 -0
  16. cli_anything_medcare-0.1.0/cli_anything/medcare/utils/session.py +106 -0
  17. cli_anything_medcare-0.1.0/cli_anything_medcare.egg-info/PKG-INFO +10 -0
  18. cli_anything_medcare-0.1.0/cli_anything_medcare.egg-info/SOURCES.txt +23 -0
  19. cli_anything_medcare-0.1.0/cli_anything_medcare.egg-info/dependency_links.txt +1 -0
  20. cli_anything_medcare-0.1.0/cli_anything_medcare.egg-info/entry_points.txt +3 -0
  21. cli_anything_medcare-0.1.0/cli_anything_medcare.egg-info/requires.txt +2 -0
  22. cli_anything_medcare-0.1.0/cli_anything_medcare.egg-info/top_level.txt +1 -0
  23. cli_anything_medcare-0.1.0/pyproject.toml +3 -0
  24. cli_anything_medcare-0.1.0/setup.cfg +4 -0
  25. cli_anything_medcare-0.1.0/setup.py +19 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-anything-medcare
3
+ Version: 0.1.0
4
+ Summary: CLI-Anything harness for the Medcare 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.medcare.medcare_cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
@@ -0,0 +1,63 @@
1
+ """Case notes domain operations."""
2
+
3
+ from typing import Any, List, Optional
4
+ from cli_anything.medcare.core.client import MedcareClient
5
+
6
+
7
+ def list_case_notes(client: MedcareClient, patient_uuid: str) -> Any:
8
+ return client.get(f"/patient/case_notes/v2/{patient_uuid}")
9
+
10
+
11
+ def get_case_note(client: MedcareClient, note_uuid: str) -> Any:
12
+ return client.get(f"/patient/case_notes/v2/note/{note_uuid}")
13
+
14
+
15
+ def create_case_note(
16
+ client: MedcareClient,
17
+ patient_uuid: str,
18
+ note_date: str,
19
+ note_time: str,
20
+ title: Optional[str] = None,
21
+ details: Optional[str] = None,
22
+ remarks: Optional[str] = None,
23
+ actions: Optional[str] = None,
24
+ is_private: bool = False,
25
+ note_categories: Optional[List[str]] = None,
26
+ ) -> Any:
27
+ payload = {
28
+ "patient_uuid": patient_uuid,
29
+ "note_date": note_date,
30
+ "note_time": note_time,
31
+ "title": title,
32
+ "details": details,
33
+ "remarks": remarks,
34
+ "actions": actions,
35
+ "is_private": is_private,
36
+ "note_categories": note_categories or [],
37
+ }
38
+ return client.post("/patient/case_notes/v2/create", data=payload)
39
+
40
+
41
+ def update_case_note(client: MedcareClient, note_uuid: str, payload: dict) -> Any:
42
+ return client.put(f"/patient/case_notes/v2/update/{note_uuid}", data=payload)
43
+
44
+
45
+ def delete_case_note(client: MedcareClient, note_uuid: str) -> Any:
46
+ return client.delete(f"/patient/case_notes/v2/delete/{note_uuid}")
47
+
48
+
49
+ def generate_case_note_report(
50
+ client: MedcareClient, patient_id: str, design_type: str = "new"
51
+ ) -> Any:
52
+ return client.post(
53
+ f"/patient/case_notes/v2/report/{patient_id}",
54
+ data={"designtype": design_type},
55
+ )
56
+
57
+
58
+ def list_case_note_categories(client: MedcareClient) -> Any:
59
+ return client.get("/patient/case_notes/v2/categories")
60
+
61
+
62
+ def list_case_note_templates(client: MedcareClient) -> Any:
63
+ return client.get("/patient/case_notes/v2/templates")
@@ -0,0 +1,45 @@
1
+ """HTTP client wrapper for the Medcare FastAPI backend."""
2
+
3
+ import json
4
+ from typing import Any, Optional
5
+ import requests
6
+ from cli_anything.medcare.utils.session import require_token
7
+
8
+
9
+ class MedcareClient:
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,46 @@
1
+ """Lab report domain operations."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Optional
5
+ from cli_anything.medcare.core.client import MedcareClient
6
+
7
+
8
+ def get_lab_reports(client: MedcareClient, patient_uuid: str) -> Any:
9
+ return client.get(f"/health_info/lab_report/patient/{patient_uuid}")
10
+
11
+
12
+ def get_lab_report(client: MedcareClient, report_uuid: str) -> Any:
13
+ return client.get(f"/health_info/lab_report/get/{report_uuid}")
14
+
15
+
16
+ def get_lab_trends(client: MedcareClient, patient_uuid: str) -> Any:
17
+ return client.get(f"/health_info/lab_report/patient/{patient_uuid}/trends")
18
+
19
+
20
+ def upload_lab_report(client: MedcareClient, patient_uuid: str, pdf_path: str) -> Any:
21
+ path = Path(pdf_path)
22
+ if not path.exists():
23
+ raise FileNotFoundError(f"PDF not found: {pdf_path}")
24
+ with open(path, "rb") as f:
25
+ return client.post(
26
+ f"/health_info/lab_report/upload/{patient_uuid}",
27
+ files={"file": (path.name, f, "application/pdf")},
28
+ )
29
+
30
+
31
+ def update_lab_report(
32
+ client: MedcareClient,
33
+ report_uuid: str,
34
+ remarks: Optional[str] = None,
35
+ is_verified: Optional[bool] = None,
36
+ ) -> Any:
37
+ payload: dict = {}
38
+ if remarks is not None:
39
+ payload["remarks"] = remarks
40
+ if is_verified is not None:
41
+ payload["is_verified"] = is_verified
42
+ return client.put(f"/health_info/lab_report/update/{report_uuid}", data=payload)
43
+
44
+
45
+ def delete_lab_report(client: MedcareClient, report_uuid: str) -> Any:
46
+ return client.delete(f"/health_info/lab_report/delete/{report_uuid}")
@@ -0,0 +1,20 @@
1
+ """Medication domain operations."""
2
+
3
+ from typing import Any
4
+ from cli_anything.medcare.core.client import MedcareClient
5
+
6
+
7
+ def get_medications(client: MedcareClient, patient_id: str) -> Any:
8
+ return client.get(f"/patient/v3/medication/{patient_id}")
9
+
10
+
11
+ def get_daily_medication(client: MedcareClient, patient_id: str) -> Any:
12
+ return client.get(f"/daily-medication/{patient_id}")
13
+
14
+
15
+ def get_medication_monitoring(client: MedcareClient, patient_id: str) -> Any:
16
+ return client.get(f"/medication-monitoring/{patient_id}")
17
+
18
+
19
+ def get_health_summary(client: MedcareClient, patient_id: str) -> Any:
20
+ return client.get(f"/patient/v5/health-summary/{patient_id}")
@@ -0,0 +1,32 @@
1
+ """Patient domain operations."""
2
+
3
+ from typing import Any, Optional
4
+ from cli_anything.medcare.core.client import MedcareClient
5
+
6
+
7
+ def search_patients(client: MedcareClient, term: str) -> Any:
8
+ return client.get(f"/patient/v2/search/{term}")
9
+
10
+
11
+ def list_patients(client: MedcareClient, page: int = 1) -> Any:
12
+ return client.get(f"/patient/v2/all/{page}")
13
+
14
+
15
+ def get_patient(client: MedcareClient, patient_id: str) -> Any:
16
+ return client.get(f"/patient/v2/get/{patient_id}")
17
+
18
+
19
+ def create_patient(client: MedcareClient, payload: dict) -> Any:
20
+ return client.post("/patient/v2/create", data=payload)
21
+
22
+
23
+ def update_patient(client: MedcareClient, patient_id: str, payload: dict) -> Any:
24
+ return client.put(f"/patient/v2/update/{patient_id}", data=payload)
25
+
26
+
27
+ def delete_patient(client: MedcareClient, patient_id: str) -> Any:
28
+ return client.delete(f"/patient/v2/delete/{patient_id}")
29
+
30
+
31
+ def list_patients_by_condition(client: MedcareClient, condition: str, page: int = 1) -> Any:
32
+ 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.medcare.core.client import MedcareClient
5
+
6
+
7
+ def list_staff(client: MedcareClient) -> Any:
8
+ return client.get("/admin/staffs")
9
+
10
+
11
+ def get_staff(client: MedcareClient, firebase_uid: str) -> Any:
12
+ return client.get(f"/admin/staff/{firebase_uid}")
@@ -0,0 +1,419 @@
1
+ """Medcare CLI — read-only data access for patients, lab reports, case notes, medication, staff."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import Optional
6
+
7
+ import click
8
+ import requests
9
+
10
+ from cli_anything.medcare.core.client import MedcareClient
11
+ from cli_anything.medcare.core import patients, lab_reports, case_notes, medication, staff
12
+ from cli_anything.medcare.utils.session import save_token, save_session, clear_session, get_session
13
+ from cli_anything.medcare.utils import auth as _auth
14
+ from cli_anything.medcare.utils.repl_skin import ReplSkin
15
+
16
+
17
+ # ── Helpers ───────────────────────────────────────────────────────────────────
18
+
19
+ def _client(ctx: click.Context) -> MedcareClient:
20
+ return MedcareClient()
21
+
22
+
23
+ def _out(ctx: click.Context, data) -> None:
24
+ if ctx.obj.get("json"):
25
+ click.echo(json.dumps(data, indent=2, default=str))
26
+ else:
27
+ if isinstance(data, (dict, list)):
28
+ click.echo(json.dumps(data, indent=2, default=str))
29
+ else:
30
+ click.echo(str(data))
31
+
32
+
33
+ def _handle_error(e: Exception) -> None:
34
+ if isinstance(e, requests.HTTPError):
35
+ try:
36
+ detail = e.response.json().get("detail", str(e))
37
+ except Exception:
38
+ detail = str(e)
39
+ click.echo(f"API error {e.response.status_code}: {detail}", err=True)
40
+ else:
41
+ click.echo(f"Error: {e}", err=True)
42
+ sys.exit(1)
43
+
44
+
45
+ # ── Root group ────────────────────────────────────────────────────────────────
46
+
47
+ @click.group()
48
+ @click.option("--json", "json_mode", is_flag=True, default=False, help="Output raw JSON.")
49
+ @click.pass_context
50
+ def cli(ctx: click.Context, json_mode: bool) -> None:
51
+ """Medcare CLI — read-only data access for the Medcare backend."""
52
+ ctx.ensure_object(dict)
53
+ ctx.obj["json"] = json_mode
54
+
55
+
56
+ # ── Auth ──────────────────────────────────────────────────────────────────────
57
+
58
+ @cli.command()
59
+ @click.option("--url", default="https://api.aipharm.xyz", show_default=True, help="Medcare API base URL.")
60
+ @click.option("--token", default=None, hidden=True, help="Pre-issued Firebase ID token (agent/CI use only — skips role check).")
61
+ def login(url: str, token: str) -> None:
62
+ """Log in to Medcare. Prompts for email and password."""
63
+ # Agent / CI mode: token provided directly, skip interactive flow.
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
+ # Role check — only branch managers and admin users may use the CLI.
83
+ click.echo("Checking access…")
84
+ try:
85
+ c = MedcareClient(base_url=url, token=id_token)
86
+ profile = staff.get_staff(c, local_id)
87
+ except Exception as e:
88
+ click.echo(f"Could not verify role: {e}", err=True)
89
+ sys.exit(1)
90
+
91
+ is_branch_manager = profile.get("isBranchManager", False)
92
+ staff_role = profile.get("StaffRole", "")
93
+
94
+ if not (is_branch_manager or staff_role == "user"):
95
+ click.echo(
96
+ f"Access denied — role '{staff_role}' is not authorised to use this CLI.\n"
97
+ "Only branch managers and admin users are allowed.",
98
+ err=True,
99
+ )
100
+ sys.exit(1)
101
+
102
+ save_session(url, id_token, resp["refresh_token"], local_id, email)
103
+ click.echo(f"Logged in as {email} (role: {staff_role}, branch manager: {is_branch_manager})")
104
+
105
+
106
+ @cli.command()
107
+ def logout() -> None:
108
+ """Log out and revoke tokens server-side."""
109
+ data = get_session()
110
+ if data.get("token") and data.get("base_url"):
111
+ _auth.logout_server(data["base_url"], data["token"])
112
+ clear_session()
113
+ click.echo("Logged out.")
114
+
115
+
116
+ @cli.command()
117
+ def whoami() -> None:
118
+ """Show current session info."""
119
+ data = get_session()
120
+ if not data or not data.get("token"):
121
+ click.echo("Not logged in.")
122
+ return
123
+ click.echo(f"URL : {data.get('base_url', 'n/a')}")
124
+ click.echo(f"Email: {data.get('email', '(token mode)')}")
125
+ issued = data.get("issued_at")
126
+ if issued:
127
+ import time
128
+ age_min = int((time.time() - issued) / 60)
129
+ click.echo(f"Token: {age_min} min old (auto-refreshes at 55 min)")
130
+ else:
131
+ click.echo(f"Token: {data.get('token', '')[:24]}… (manual token mode)")
132
+
133
+
134
+ # ── Patient commands ──────────────────────────────────────────────────────────
135
+
136
+ @cli.group()
137
+ def patient() -> None:
138
+ """Patient search and lookup."""
139
+
140
+
141
+ @patient.command("search")
142
+ @click.argument("term")
143
+ @click.pass_context
144
+ def patient_search(ctx, term: str) -> None:
145
+ """Search patients by name, IC, or ref ID."""
146
+ try:
147
+ _out(ctx, patients.search_patients(_client(ctx), term))
148
+ except Exception as e:
149
+ _handle_error(e)
150
+
151
+
152
+ @patient.command("list")
153
+ @click.option("--page", default=1, show_default=True, help="Page number.")
154
+ @click.pass_context
155
+ def patient_list(ctx, page: int) -> None:
156
+ """List patients (paginated)."""
157
+ try:
158
+ _out(ctx, patients.list_patients(_client(ctx), page))
159
+ except Exception as e:
160
+ _handle_error(e)
161
+
162
+
163
+ @patient.command("get")
164
+ @click.argument("patient_id")
165
+ @click.pass_context
166
+ def patient_get(ctx, patient_id: str) -> None:
167
+ """Get a patient by ID."""
168
+ try:
169
+ _out(ctx, patients.get_patient(_client(ctx), patient_id))
170
+ except Exception as e:
171
+ _handle_error(e)
172
+
173
+
174
+ @patient.command("by-condition")
175
+ @click.argument("condition")
176
+ @click.option("--page", default=1, show_default=True)
177
+ @click.pass_context
178
+ def patient_by_condition(ctx, condition: str, page: int) -> None:
179
+ """List patients by medical condition tag."""
180
+ try:
181
+ _out(ctx, patients.list_patients_by_condition(_client(ctx), condition, page))
182
+ except Exception as e:
183
+ _handle_error(e)
184
+
185
+
186
+ # ── Lab report commands ───────────────────────────────────────────────────────
187
+
188
+ @cli.group()
189
+ def lab() -> None:
190
+ """Lab report data and trends."""
191
+
192
+
193
+ @lab.command("list")
194
+ @click.argument("patient_uuid")
195
+ @click.pass_context
196
+ def lab_list(ctx, patient_uuid: str) -> None:
197
+ """List all lab reports for a patient."""
198
+ try:
199
+ _out(ctx, lab_reports.get_lab_reports(_client(ctx), patient_uuid))
200
+ except Exception as e:
201
+ _handle_error(e)
202
+
203
+
204
+ @lab.command("get")
205
+ @click.argument("report_uuid")
206
+ @click.pass_context
207
+ def lab_get(ctx, report_uuid: str) -> None:
208
+ """Get a single lab report with all results."""
209
+ try:
210
+ _out(ctx, lab_reports.get_lab_report(_client(ctx), report_uuid))
211
+ except Exception as e:
212
+ _handle_error(e)
213
+
214
+
215
+ @lab.command("trends")
216
+ @click.argument("patient_uuid")
217
+ @click.pass_context
218
+ def lab_trends(ctx, patient_uuid: str) -> None:
219
+ """Show lab result trends for a patient."""
220
+ try:
221
+ _out(ctx, lab_reports.get_lab_trends(_client(ctx), patient_uuid))
222
+ except Exception as e:
223
+ _handle_error(e)
224
+
225
+
226
+ # ── Case note commands ────────────────────────────────────────────────────────
227
+
228
+ @cli.group()
229
+ def note() -> None:
230
+ """Case note lookup and reference data."""
231
+
232
+
233
+ @note.command("list")
234
+ @click.argument("patient_uuid")
235
+ @click.pass_context
236
+ def note_list(ctx, patient_uuid: str) -> None:
237
+ """List all case notes for a patient."""
238
+ try:
239
+ _out(ctx, case_notes.list_case_notes(_client(ctx), patient_uuid))
240
+ except Exception as e:
241
+ _handle_error(e)
242
+
243
+
244
+ @note.command("get")
245
+ @click.argument("note_uuid")
246
+ @click.pass_context
247
+ def note_get(ctx, note_uuid: str) -> None:
248
+ """Get a single case note."""
249
+ try:
250
+ _out(ctx, case_notes.get_case_note(_client(ctx), note_uuid))
251
+ except Exception as e:
252
+ _handle_error(e)
253
+
254
+
255
+ @note.command("report")
256
+ @click.argument("patient_id")
257
+ @click.option("--design", default="new", type=click.Choice(["old", "new", "markdown"]), show_default=True)
258
+ @click.pass_context
259
+ def note_report(ctx, patient_id: str, design: str) -> None:
260
+ """Generate a PDF case note report for a patient."""
261
+ try:
262
+ _out(ctx, case_notes.generate_case_note_report(_client(ctx), patient_id, design))
263
+ except Exception as e:
264
+ _handle_error(e)
265
+
266
+
267
+ @note.command("categories")
268
+ @click.pass_context
269
+ def note_categories(ctx) -> None:
270
+ """List available case note categories."""
271
+ try:
272
+ _out(ctx, case_notes.list_case_note_categories(_client(ctx)))
273
+ except Exception as e:
274
+ _handle_error(e)
275
+
276
+
277
+ @note.command("templates")
278
+ @click.pass_context
279
+ def note_templates(ctx) -> None:
280
+ """List available case note templates."""
281
+ try:
282
+ _out(ctx, case_notes.list_case_note_templates(_client(ctx)))
283
+ except Exception as e:
284
+ _handle_error(e)
285
+
286
+
287
+ # ── Medication commands ───────────────────────────────────────────────────────
288
+
289
+ @cli.group()
290
+ def med() -> None:
291
+ """Medication and health summary."""
292
+
293
+
294
+ @med.command("list")
295
+ @click.argument("patient_id")
296
+ @click.pass_context
297
+ def med_list(ctx, patient_id: str) -> None:
298
+ """List medications for a patient."""
299
+ try:
300
+ _out(ctx, medication.get_medications(_client(ctx), patient_id))
301
+ except Exception as e:
302
+ _handle_error(e)
303
+
304
+
305
+ @med.command("daily")
306
+ @click.argument("patient_id")
307
+ @click.pass_context
308
+ def med_daily(ctx, patient_id: str) -> None:
309
+ """Show daily medication schedule for a patient."""
310
+ try:
311
+ _out(ctx, medication.get_daily_medication(_client(ctx), patient_id))
312
+ except Exception as e:
313
+ _handle_error(e)
314
+
315
+
316
+ @med.command("monitoring")
317
+ @click.argument("patient_id")
318
+ @click.pass_context
319
+ def med_monitoring(ctx, patient_id: str) -> None:
320
+ """Show medication monitoring data for a patient."""
321
+ try:
322
+ _out(ctx, medication.get_medication_monitoring(_client(ctx), patient_id))
323
+ except Exception as e:
324
+ _handle_error(e)
325
+
326
+
327
+ @med.command("summary")
328
+ @click.argument("patient_id")
329
+ @click.pass_context
330
+ def med_summary(ctx, patient_id: str) -> None:
331
+ """Show health summary for a patient."""
332
+ try:
333
+ _out(ctx, medication.get_health_summary(_client(ctx), patient_id))
334
+ except Exception as e:
335
+ _handle_error(e)
336
+
337
+
338
+ # ── Staff commands ────────────────────────────────────────────────────────────
339
+
340
+ @cli.group()
341
+ def staff_cmd() -> None:
342
+ """Staff and organisation lookup."""
343
+
344
+
345
+ staff_cmd.name = "staff"
346
+
347
+
348
+ @staff_cmd.command("list")
349
+ @click.pass_context
350
+ def staff_list(ctx) -> None:
351
+ """List all staff in the organisation."""
352
+ try:
353
+ _out(ctx, staff.list_staff(_client(ctx)))
354
+ except Exception as e:
355
+ _handle_error(e)
356
+
357
+
358
+ @staff_cmd.command("get")
359
+ @click.argument("firebase_uid")
360
+ @click.pass_context
361
+ def staff_get(ctx, firebase_uid: str) -> None:
362
+ """Get a staff member by Firebase UID."""
363
+ try:
364
+ _out(ctx, staff.get_staff(_client(ctx), firebase_uid))
365
+ except Exception as e:
366
+ _handle_error(e)
367
+
368
+
369
+ # ── Preview command ───────────────────────────────────────────────────────────
370
+
371
+ @cli.command("preview")
372
+ @click.argument("patient_id")
373
+ @click.pass_context
374
+ def preview(ctx, patient_id: str) -> None:
375
+ """Print a compact health snapshot for a patient."""
376
+ c = _client(ctx)
377
+ lines = []
378
+ try:
379
+ p = patients.get_patient(c, patient_id)
380
+ info = p.get("personal_info", p)
381
+ lines.append(f"Patient : {info.get('name', patient_id)}")
382
+ lines.append(f"IC : {info.get('ic', 'n/a')}")
383
+ lines.append(f"Gender : {info.get('gender', 'n/a')}")
384
+ lines.append(f"DOB : {info.get('date_of_birth', 'n/a')}")
385
+ except Exception:
386
+ lines.append(f"Patient : {patient_id} (fetch failed)")
387
+
388
+ try:
389
+ labs = lab_reports.get_lab_reports(c, patient_id)
390
+ count = len(labs) if isinstance(labs, list) else labs.get("total", "?")
391
+ lines.append(f"Lab Rpts: {count}")
392
+ except Exception:
393
+ lines.append("Lab Rpts: (unavailable)")
394
+
395
+ try:
396
+ notes = case_notes.list_case_notes(c, patient_id)
397
+ count = len(notes) if isinstance(notes, list) else notes.get("total", "?")
398
+ lines.append(f"Notes : {count}")
399
+ except Exception:
400
+ lines.append("Notes : (unavailable)")
401
+
402
+ if ctx.obj.get("json"):
403
+ click.echo(json.dumps({"patient_id": patient_id, "summary": lines}))
404
+ else:
405
+ click.echo("\n".join(lines))
406
+
407
+
408
+ # ── REPL entry ────────────────────────────────────────────────────────────────
409
+
410
+ @cli.command("repl")
411
+ @click.option("--json", "json_mode", is_flag=True, default=False)
412
+ @click.pass_context
413
+ def repl(ctx, json_mode: bool) -> None:
414
+ """Start interactive REPL session."""
415
+ skin = ReplSkin(cli, json_mode=json_mode or ctx.obj.get("json", False))
416
+ skin.run()
417
+
418
+
419
+ cli.add_command(staff_cmd, name="staff")
@@ -0,0 +1,159 @@
1
+ """Unit tests — no network, no real backend required."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from pathlib import Path
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ # ── Session helpers ───────────────────────────────────────────────────────────
12
+
13
+ def _patch_session_file(tmp_path):
14
+ session_path = tmp_path / ".medcare_session.json"
15
+ return patch("cli_anything.medcare.utils.session.SESSION_FILE", session_path)
16
+
17
+
18
+ def test_session_save_load(tmp_path):
19
+ with _patch_session_file(tmp_path):
20
+ from cli_anything.medcare.utils.session import save_token, get_session
21
+ save_token("http://localhost:8000", "tok123")
22
+ data = get_session()
23
+ assert data["base_url"] == "http://localhost:8000"
24
+ assert data["token"] == "tok123"
25
+
26
+
27
+ def test_session_clear(tmp_path):
28
+ with _patch_session_file(tmp_path):
29
+ from cli_anything.medcare.utils.session import save_token, clear_session, get_session
30
+ save_token("http://localhost:8000", "tok123")
31
+ clear_session()
32
+ data = get_session()
33
+ assert data == {}
34
+
35
+
36
+ def test_require_token_missing(tmp_path):
37
+ with _patch_session_file(tmp_path):
38
+ from cli_anything.medcare.utils.session import require_token
39
+ with pytest.raises(RuntimeError, match="Not logged in"):
40
+ require_token()
41
+
42
+
43
+ # ── Client headers ────────────────────────────────────────────────────────────
44
+
45
+ def test_client_headers():
46
+ from cli_anything.medcare.core.client import MedcareClient
47
+ c = MedcareClient(base_url="http://localhost:8000", token="mytoken")
48
+ assert c._headers() == {"Authorization": "Bearer mytoken"}
49
+
50
+
51
+ # ── REPL skin ─────────────────────────────────────────────────────────────────
52
+
53
+ def test_repl_skin_undo():
54
+ import click
55
+ from cli_anything.medcare.utils.repl_skin import ReplSkin
56
+
57
+ @click.group()
58
+ def dummy_cli():
59
+ pass
60
+
61
+ skin = ReplSkin(dummy_cli)
62
+ skin.push_history("patient list")
63
+ skin.push_history("lab list abc")
64
+ assert skin.undo() == "lab list abc"
65
+ assert skin.undo() == "patient list"
66
+ assert skin.undo() is None
67
+
68
+
69
+ def test_repl_skin_dispatch_calls_cli():
70
+ import click
71
+ from cli_anything.medcare.utils.repl_skin import ReplSkin
72
+
73
+ @click.group()
74
+ def dummy_cli():
75
+ pass
76
+
77
+ called_with = []
78
+
79
+ def fake_main(args, standalone_mode):
80
+ called_with.extend(args)
81
+
82
+ dummy_cli.main = fake_main
83
+ skin = ReplSkin(dummy_cli)
84
+ skin._dispatch("patient list --page 2")
85
+ assert "patient" in called_with
86
+ assert "list" in called_with
87
+
88
+
89
+ # ── Patient functions ─────────────────────────────────────────────────────────
90
+
91
+ def test_patient_search_url():
92
+ from cli_anything.medcare.core import patients
93
+ mock_client = MagicMock()
94
+ mock_client.get.return_value = []
95
+ patients.search_patients(mock_client, "john")
96
+ mock_client.get.assert_called_once_with("/patient/v2/search/john")
97
+
98
+
99
+ def test_patient_list_url():
100
+ from cli_anything.medcare.core import patients
101
+ mock_client = MagicMock()
102
+ mock_client.get.return_value = []
103
+ patients.list_patients(mock_client, page=3)
104
+ mock_client.get.assert_called_once_with("/patient/v2/all/3")
105
+
106
+
107
+ # ── Lab report functions ──────────────────────────────────────────────────────
108
+
109
+ def test_lab_upload_missing_file():
110
+ from cli_anything.medcare.core import lab_reports
111
+ mock_client = MagicMock()
112
+ with pytest.raises(FileNotFoundError):
113
+ lab_reports.upload_lab_report(mock_client, "patient-uuid", "/nonexistent/file.pdf")
114
+
115
+
116
+ def test_lab_get_url():
117
+ from cli_anything.medcare.core import lab_reports
118
+ mock_client = MagicMock()
119
+ mock_client.get.return_value = {}
120
+ lab_reports.get_lab_report(mock_client, "report-uuid-123")
121
+ mock_client.get.assert_called_once_with("/health_info/lab_report/get/report-uuid-123")
122
+
123
+
124
+ # ── Case note functions ───────────────────────────────────────────────────────
125
+
126
+ def test_case_note_create_payload():
127
+ from cli_anything.medcare.core import case_notes
128
+ mock_client = MagicMock()
129
+ mock_client.post.return_value = {"id": "note-uuid"}
130
+ case_notes.create_case_note(
131
+ mock_client,
132
+ patient_uuid="p-uuid",
133
+ note_date="2025-01-15",
134
+ note_time="10.30 A.M.",
135
+ title="Follow-up",
136
+ details="Patient stable.",
137
+ )
138
+ call_kwargs = mock_client.post.call_args
139
+ payload = call_kwargs[1]["data"] if call_kwargs[1] else call_kwargs[0][1]
140
+ assert payload["patient_uuid"] == "p-uuid"
141
+ assert payload["note_date"] == "2025-01-15"
142
+ assert payload["title"] == "Follow-up"
143
+
144
+
145
+ # ── Medication functions ──────────────────────────────────────────────────────
146
+
147
+ def test_medication_paths():
148
+ from cli_anything.medcare.core import medication
149
+ mock_client = MagicMock()
150
+ mock_client.get.return_value = {}
151
+
152
+ medication.get_medications(mock_client, "pat-1")
153
+ mock_client.get.assert_called_with("/patient/v3/medication/pat-1")
154
+
155
+ medication.get_daily_medication(mock_client, "pat-1")
156
+ mock_client.get.assert_called_with("/daily-medication/pat-1")
157
+
158
+ medication.get_health_summary(mock_client, "pat-1")
159
+ mock_client.get.assert_called_with("/patient/v5/health-summary/pat-1")
@@ -0,0 +1,182 @@
1
+ """
2
+ End-to-end tests against a live Medcare backend.
3
+
4
+ Required env vars:
5
+ MEDCARE_TEST_URL — e.g. http://localhost:8000
6
+ MEDCARE_TEST_TOKEN — valid Firebase ID token
7
+ MEDCARE_TEST_PATIENT_UUID — UUID of an existing patient
8
+
9
+ Run with:
10
+ MEDCARE_TEST_URL=... MEDCARE_TEST_TOKEN=... MEDCARE_TEST_PATIENT_UUID=... pytest test_full_e2e.py -v
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+
19
+ import pytest
20
+
21
+ URL = os.environ.get("MEDCARE_TEST_URL", "")
22
+ TOKEN = os.environ.get("MEDCARE_TEST_TOKEN", "")
23
+ PATIENT_UUID = os.environ.get("MEDCARE_TEST_PATIENT_UUID", "")
24
+
25
+ skip_live = pytest.mark.skipif(
26
+ not (URL and TOKEN),
27
+ reason="MEDCARE_TEST_URL and MEDCARE_TEST_TOKEN not set",
28
+ )
29
+ skip_patient = pytest.mark.skipif(
30
+ not PATIENT_UUID,
31
+ reason="MEDCARE_TEST_PATIENT_UUID not set",
32
+ )
33
+
34
+
35
+ def _resolve_cli() -> str:
36
+ """Return the installed CLI path or fall back to python -m."""
37
+ force_installed = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED")
38
+ installed = shutil.which("medcare-agent")
39
+ if installed:
40
+ return installed
41
+ if force_installed:
42
+ pytest.fail("CLI_ANYTHING_FORCE_INSTALLED=1 but medcare-agent not found in PATH")
43
+ return None
44
+
45
+
46
+ def _run(*args: str) -> subprocess.CompletedProcess:
47
+ cli_path = _resolve_cli()
48
+ if cli_path:
49
+ cmd = [cli_path] + list(args)
50
+ else:
51
+ cmd = [sys.executable, "-m", "cli_anything.medcare"] + list(args)
52
+ return subprocess.run(cmd, capture_output=True, text=True)
53
+
54
+
55
+ def _run_json(*args: str) -> dict | list:
56
+ result = _run(*args, "--json")
57
+ assert result.returncode == 0, result.stderr
58
+ return json.loads(result.stdout)
59
+
60
+
61
+ # ── Auth ──────────────────────────────────────────────────────────────────────
62
+
63
+ @skip_live
64
+ def test_login_logout():
65
+ result = _run("login", "--url", URL, "--token", TOKEN)
66
+ assert result.returncode == 0
67
+ assert "Session saved" in result.stdout
68
+
69
+ result = _run("whoami")
70
+ assert result.returncode == 0
71
+ assert URL in result.stdout
72
+
73
+ result = _run("logout")
74
+ assert result.returncode == 0
75
+
76
+ # Re-login for subsequent tests
77
+ _run("login", "--url", URL, "--token", TOKEN)
78
+
79
+
80
+ # ── Patients ──────────────────────────────────────────────────────────────────
81
+
82
+ @skip_live
83
+ def test_patient_list():
84
+ _run("login", "--url", URL, "--token", TOKEN)
85
+ data = _run_json("patient", "list")
86
+ assert isinstance(data, (list, dict))
87
+
88
+
89
+ @skip_live
90
+ def test_patient_search():
91
+ _run("login", "--url", URL, "--token", TOKEN)
92
+ result = _run("--json", "patient", "search", "a")
93
+ assert result.returncode == 0
94
+ data = json.loads(result.stdout)
95
+ assert isinstance(data, (list, dict))
96
+
97
+
98
+ @skip_live
99
+ @skip_patient
100
+ def test_patient_get():
101
+ _run("login", "--url", URL, "--token", TOKEN)
102
+ result = _run("--json", "patient", "get", PATIENT_UUID)
103
+ assert result.returncode == 0
104
+ data = json.loads(result.stdout)
105
+ assert "personal_info" in data or isinstance(data, dict)
106
+
107
+
108
+ # ── Lab Reports ───────────────────────────────────────────────────────────────
109
+
110
+ @skip_live
111
+ @skip_patient
112
+ def test_lab_list():
113
+ _run("login", "--url", URL, "--token", TOKEN)
114
+ result = _run("--json", "lab", "list", PATIENT_UUID)
115
+ assert result.returncode == 0
116
+ data = json.loads(result.stdout)
117
+ assert isinstance(data, (list, dict))
118
+
119
+
120
+ @skip_live
121
+ @skip_patient
122
+ def test_lab_trends():
123
+ _run("login", "--url", URL, "--token", TOKEN)
124
+ result = _run("--json", "lab", "trends", PATIENT_UUID)
125
+ assert result.returncode == 0
126
+ data = json.loads(result.stdout)
127
+ assert isinstance(data, (list, dict))
128
+
129
+
130
+ # ── Case Notes ────────────────────────────────────────────────────────────────
131
+
132
+ @skip_live
133
+ @skip_patient
134
+ def test_note_list():
135
+ _run("login", "--url", URL, "--token", TOKEN)
136
+ result = _run("--json", "note", "list", PATIENT_UUID)
137
+ assert result.returncode == 0
138
+ data = json.loads(result.stdout)
139
+ assert isinstance(data, (list, dict))
140
+
141
+
142
+ @skip_live
143
+ def test_note_categories():
144
+ _run("login", "--url", URL, "--token", TOKEN)
145
+ result = _run("--json", "note", "categories")
146
+ assert result.returncode == 0
147
+ data = json.loads(result.stdout)
148
+ assert isinstance(data, (list, dict))
149
+
150
+
151
+ # ── Staff ─────────────────────────────────────────────────────────────────────
152
+
153
+ @skip_live
154
+ def test_staff_list():
155
+ _run("login", "--url", URL, "--token", TOKEN)
156
+ result = _run("--json", "staff", "list")
157
+ assert result.returncode == 0
158
+ data = json.loads(result.stdout)
159
+ assert isinstance(data, (list, dict))
160
+
161
+
162
+ # ── Preview ───────────────────────────────────────────────────────────────────
163
+
164
+ @skip_live
165
+ @skip_patient
166
+ def test_preview():
167
+ _run("login", "--url", URL, "--token", TOKEN)
168
+ result = _run("preview", PATIENT_UUID)
169
+ assert result.returncode == 0
170
+ assert "Patient" in result.stdout
171
+
172
+
173
+ # ── Installed command ─────────────────────────────────────────────────────────
174
+
175
+ def test_installed_command_help():
176
+ """Verify the installed CLI entry point runs."""
177
+ cli_path = _resolve_cli()
178
+ if not cli_path:
179
+ pytest.skip("medcare-agent not installed; install with: pip install -e .")
180
+ result = subprocess.run([cli_path, "--help"], capture_output=True, text=True)
181
+ assert result.returncode == 0
182
+ assert "Medcare" in result.stdout
@@ -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": "medcare-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() # LoginResponse shape
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() # RefreshTokenResponse shape
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 # best-effort; local session is cleared regardless
@@ -0,0 +1,76 @@
1
+ """Unified REPL skin for the Medcare CLI harness."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import Callable, Optional
6
+ import click
7
+
8
+
9
+ BANNER = """\
10
+ ╔══════════════════════════════════════════╗
11
+ ║ Medcare CLI • type 'help' ║
12
+ ╚══════════════════════════════════════════╝
13
+ """
14
+
15
+ PROMPT = "medcare> "
16
+
17
+
18
+ class ReplSkin:
19
+ """Interactive REPL that delegates lines to a Click group."""
20
+
21
+ def __init__(self, cli_group: click.Group, json_mode: bool = False):
22
+ self.cli = cli_group
23
+ self.json_mode = json_mode
24
+ self._history: list[str] = []
25
+
26
+ # ── History ──────────────────────────────────────────────────────────────
27
+
28
+ def push_history(self, line: str) -> None:
29
+ self._history.append(line)
30
+
31
+ def undo(self) -> Optional[str]:
32
+ return self._history.pop() if self._history else None
33
+
34
+ # ── Dispatch ─────────────────────────────────────────────────────────────
35
+
36
+ def _dispatch(self, line: str) -> None:
37
+ args = line.strip().split()
38
+ if not args:
39
+ return
40
+ if self.json_mode and "--json" not in args:
41
+ args.append("--json")
42
+ try:
43
+ self.cli.main(args=args, standalone_mode=False)
44
+ except click.UsageError as e:
45
+ click.echo(f"Error: {e}", err=True)
46
+ except SystemExit:
47
+ pass
48
+ except Exception as e:
49
+ click.echo(f"Error: {e}", err=True)
50
+
51
+ # ── Run ──────────────────────────────────────────────────────────────────
52
+
53
+ def run(self) -> None:
54
+ click.echo(BANNER)
55
+ while True:
56
+ try:
57
+ line = input(PROMPT).strip()
58
+ except (EOFError, KeyboardInterrupt):
59
+ click.echo("\nBye.")
60
+ break
61
+
62
+ if not line:
63
+ continue
64
+ if line in ("exit", "quit", "q"):
65
+ click.echo("Bye.")
66
+ break
67
+ if line == "undo":
68
+ last = self.undo()
69
+ click.echo(f"Removed from history: {last}" if last else "Nothing to undo.")
70
+ continue
71
+ if line in ("help", "?"):
72
+ self._dispatch("--help")
73
+ continue
74
+
75
+ self.push_history(line)
76
+ self._dispatch(line)
@@ -0,0 +1,106 @@
1
+ """
2
+ Session management: persist credentials to ~/.medcare_session.json.
3
+ Locked writes prevent concurrent JSON corruption.
4
+ Auto-refreshes the Firebase ID 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 time
11
+ from pathlib import Path
12
+
13
+ SESSION_FILE = Path.home() / ".medcare_session.json"
14
+
15
+ _TOKEN_TTL = 55 * 60 # refresh 5 min before Firebase's 1-hour expiry
16
+
17
+
18
+ def _load_raw() -> dict:
19
+ if not SESSION_FILE.exists():
20
+ return {}
21
+ try:
22
+ with open(SESSION_FILE, "r") as f:
23
+ return json.load(f)
24
+ except (json.JSONDecodeError, OSError):
25
+ return {}
26
+
27
+
28
+ def _save_raw(data: dict) -> None:
29
+ tmp = SESSION_FILE.with_suffix(".tmp")
30
+ with open(tmp, "w") as f:
31
+ fcntl.flock(f, fcntl.LOCK_EX)
32
+ json.dump(data, f, indent=2)
33
+ fcntl.flock(f, fcntl.LOCK_UN)
34
+ tmp.replace(SESSION_FILE)
35
+
36
+
37
+ def get_session() -> dict:
38
+ return _load_raw()
39
+
40
+
41
+ def save_session(
42
+ base_url: str,
43
+ token: str,
44
+ refresh_token: str,
45
+ local_id: str,
46
+ email: str,
47
+ ) -> None:
48
+ data = _load_raw()
49
+ data.update(
50
+ base_url=base_url.rstrip("/"),
51
+ token=token,
52
+ refresh_token=refresh_token,
53
+ local_id=local_id,
54
+ email=email,
55
+ issued_at=time.time(),
56
+ )
57
+ _save_raw(data)
58
+
59
+
60
+ def save_token(base_url: str, token: str) -> None:
61
+ """Agent / CI mode: store only base_url + token, no auto-refresh."""
62
+ data = _load_raw()
63
+ data["base_url"] = base_url.rstrip("/")
64
+ data["token"] = token
65
+ data.pop("issued_at", None) # absent = skip auto-refresh
66
+ _save_raw(data)
67
+
68
+
69
+ def clear_session() -> None:
70
+ if SESSION_FILE.exists():
71
+ SESSION_FILE.unlink()
72
+
73
+
74
+ def require_token() -> tuple[str, str]:
75
+ """Return (base_url, token). Auto-refreshes via backend if token is near expiry."""
76
+ data = _load_raw()
77
+ base_url = data.get("base_url")
78
+ token = data.get("token")
79
+
80
+ if not base_url or not token:
81
+ raise RuntimeError("Not logged in. Run: medcare-agent login")
82
+
83
+ issued_at = data.get("issued_at")
84
+ refresh_token = data.get("refresh_token")
85
+
86
+ if issued_at and refresh_token and (time.time() - issued_at) > _TOKEN_TTL:
87
+ try:
88
+ from cli_anything.medcare.utils.auth import refresh as backend_refresh
89
+ new = backend_refresh(base_url, refresh_token)
90
+ token = new["firebase_token"]
91
+ data["token"] = token
92
+ data["refresh_token"] = new["refresh_token"]
93
+ data["issued_at"] = time.time()
94
+ _save_raw(data)
95
+ except Exception:
96
+ raise RuntimeError(
97
+ "Session expired and token refresh failed. Run: medcare-agent login"
98
+ )
99
+
100
+ return base_url, token
101
+
102
+
103
+ def update_session_key(key: str, value) -> None:
104
+ data = _load_raw()
105
+ data[key] = value
106
+ _save_raw(data)
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-anything-medcare
3
+ Version: 0.1.0
4
+ Summary: CLI-Anything harness for the Medcare 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,23 @@
1
+ pyproject.toml
2
+ setup.py
3
+ cli_anything/medcare/__init__.py
4
+ cli_anything/medcare/__main__.py
5
+ cli_anything/medcare/medcare_cli.py
6
+ cli_anything/medcare/core/__init__.py
7
+ cli_anything/medcare/core/case_notes.py
8
+ cli_anything/medcare/core/client.py
9
+ cli_anything/medcare/core/lab_reports.py
10
+ cli_anything/medcare/core/medication.py
11
+ cli_anything/medcare/core/patients.py
12
+ cli_anything/medcare/core/staff.py
13
+ cli_anything/medcare/tests/test_core.py
14
+ cli_anything/medcare/tests/test_full_e2e.py
15
+ cli_anything/medcare/utils/auth.py
16
+ cli_anything/medcare/utils/repl_skin.py
17
+ cli_anything/medcare/utils/session.py
18
+ cli_anything_medcare.egg-info/PKG-INFO
19
+ cli_anything_medcare.egg-info/SOURCES.txt
20
+ cli_anything_medcare.egg-info/dependency_links.txt
21
+ cli_anything_medcare.egg-info/entry_points.txt
22
+ cli_anything_medcare.egg-info/requires.txt
23
+ cli_anything_medcare.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ medcare = cli_anything.medcare.medcare_cli:cli
3
+ medcare-agent = cli_anything.medcare.medcare_cli:cli
@@ -0,0 +1,2 @@
1
+ click>=8.0
2
+ requests>=2.28
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -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-medcare",
5
+ version="0.1.0",
6
+ description="CLI-Anything harness for the Medcare 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
+ "medcare-agent=cli_anything.medcare.medcare_cli:cli",
15
+ "medcare=cli_anything.medcare.medcare_cli:cli",
16
+ ]
17
+ },
18
+ python_requires=">=3.10",
19
+ )