cli-anything-g32 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.
- cli_anything_g32-0.1.0/PKG-INFO +10 -0
- cli_anything_g32-0.1.0/cli_anything/g32/__main__.py +4 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/case_notes.py +27 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/client.py +45 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/lab_reports.py +16 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/medication.py +20 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/patients.py +20 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/staff.py +12 -0
- cli_anything_g32-0.1.0/cli_anything/g32/core/stats.py +40 -0
- cli_anything_g32-0.1.0/cli_anything/g32/g32_cli.py +447 -0
- cli_anything_g32-0.1.0/cli_anything/g32/g32_feature/kpi.py +27 -0
- cli_anything_g32-0.1.0/cli_anything/g32/g32_feature/leave.py +35 -0
- cli_anything_g32-0.1.0/cli_anything/g32/utils/auth.py +42 -0
- cli_anything_g32-0.1.0/cli_anything/g32/utils/repl_skin.py +68 -0
- cli_anything_g32-0.1.0/cli_anything/g32/utils/session.py +108 -0
- cli_anything_g32-0.1.0/cli_anything_g32.egg-info/PKG-INFO +10 -0
- cli_anything_g32-0.1.0/cli_anything_g32.egg-info/SOURCES.txt +21 -0
- cli_anything_g32-0.1.0/cli_anything_g32.egg-info/dependency_links.txt +1 -0
- cli_anything_g32-0.1.0/cli_anything_g32.egg-info/entry_points.txt +3 -0
- cli_anything_g32-0.1.0/cli_anything_g32.egg-info/requires.txt +2 -0
- cli_anything_g32-0.1.0/cli_anything_g32.egg-info/top_level.txt +1 -0
- cli_anything_g32-0.1.0/setup.cfg +4 -0
- cli_anything_g32-0.1.0/setup.py +19 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cli-anything-g32
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI-Anything harness for the G32 Nexus 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,27 @@
|
|
|
1
|
+
"""Case notes domain operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from cli_anything.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def list_case_notes(client: G32Client, patient_uuid: str) -> Any:
|
|
8
|
+
return client.get(f"/patient/case_notes/v2/{patient_uuid}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_case_note(client: G32Client, 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: G32Client, 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: G32Client) -> Any:
|
|
23
|
+
return client.get("/patient/case_notes/v2/categories")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_case_note_templates(client: G32Client) -> Any:
|
|
27
|
+
return client.get("/patient/case_notes/v2/templates")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""HTTP client wrapper for the G32 Nexus FastAPI backend."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
import requests
|
|
6
|
+
from cli_anything.g32.utils.session import require_token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class G32Client:
|
|
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.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_lab_reports(client: G32Client, patient_uuid: str) -> Any:
|
|
8
|
+
return client.get(f"/health_info/lab_report/patient/{patient_uuid}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_lab_report(client: G32Client, report_uuid: str) -> Any:
|
|
12
|
+
return client.get(f"/health_info/lab_report/get/{report_uuid}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_lab_trends(client: G32Client, 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.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_medications(client: G32Client, patient_id: str) -> Any:
|
|
8
|
+
return client.get(f"/patient/v3/medication/{patient_id}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_daily_medication(client: G32Client, patient_id: str) -> Any:
|
|
12
|
+
return client.get(f"/daily-medication/{patient_id}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_medication_monitoring(client: G32Client, patient_id: str) -> Any:
|
|
16
|
+
return client.get(f"/medication-monitoring/{patient_id}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_health_summary(client: G32Client, 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, Optional
|
|
4
|
+
from cli_anything.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def search_patients(client: G32Client, term: str) -> Any:
|
|
8
|
+
return client.get(f"/patient/v2/search/{term}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_patients(client: G32Client, page: int = 1) -> Any:
|
|
12
|
+
return client.get(f"/patient/v2/all/{page}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_patient(client: G32Client, patient_id: str) -> Any:
|
|
16
|
+
return client.get(f"/patient/v2/get/{patient_id}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def list_patients_by_condition(client: G32Client, 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.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def list_staff(client: G32Client) -> Any:
|
|
8
|
+
return client.get("/admin/staffs")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_staff(client: G32Client, 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.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_action_stats(client: G32Client, 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: G32Client, 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: G32Client,
|
|
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,447 @@
|
|
|
1
|
+
"""G32 Nexus CLI — patient data, KPI dashboard, and leave history."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from cli_anything.g32.core.client import G32Client
|
|
11
|
+
from cli_anything.g32.core import patients, lab_reports, case_notes, medication, staff, stats
|
|
12
|
+
from cli_anything.g32.g32_feature import kpi, leave
|
|
13
|
+
from cli_anything.g32.utils.session import save_token, save_session, clear_session, get_session
|
|
14
|
+
from cli_anything.g32.utils import auth as _auth
|
|
15
|
+
from cli_anything.g32.utils.repl_skin import ReplSkin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
def _client(ctx: click.Context) -> G32Client:
|
|
21
|
+
return G32Client()
|
|
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
|
+
"""G32 Nexus CLI — KPI dashboard, leave history, 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://g32.aipharm.xyz", show_default=True, help="G32 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 G32 Nexus. 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: str, end: str) -> 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: str, end: str) -> 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, help="Start date YYYY-MM-DD (default: Jan 1 of current year).")
|
|
348
|
+
@click.option("--end", default=None, help="End date YYYY-MM-DD (default: today).")
|
|
349
|
+
@click.option("--premise", "premise_ids", multiple=True, help="Filter by premise ID (repeatable).")
|
|
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
|
+
# ── G32 KPI commands ──────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
@cli.group("kpi")
|
|
364
|
+
def kpi_group() -> None:
|
|
365
|
+
"""G32 KPI dashboard — roles, years, details, summary."""
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@kpi_group.command("roles")
|
|
369
|
+
@click.pass_context
|
|
370
|
+
def kpi_roles(ctx) -> None:
|
|
371
|
+
"""List valid job_role values for the authenticated staff member."""
|
|
372
|
+
try:
|
|
373
|
+
_out(ctx, kpi.get_roles(_client(ctx)))
|
|
374
|
+
except Exception as e:
|
|
375
|
+
_handle_error(e)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@kpi_group.command("years")
|
|
379
|
+
@click.pass_context
|
|
380
|
+
def kpi_years(ctx) -> None:
|
|
381
|
+
"""List years with KPI data for the authenticated staff member."""
|
|
382
|
+
try:
|
|
383
|
+
_out(ctx, kpi.get_years(_client(ctx)))
|
|
384
|
+
except Exception as e:
|
|
385
|
+
_handle_error(e)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@kpi_group.command("details")
|
|
389
|
+
@click.option("--year", required=True, type=int, help="KPI year.")
|
|
390
|
+
@click.option("--role", required=True, help="Job role (from 'kpi roles').")
|
|
391
|
+
@click.option("--month", default=None, type=int, help="Filter to a specific month (1-12).")
|
|
392
|
+
@click.pass_context
|
|
393
|
+
def kpi_details(ctx, year: int, role: str, month) -> None:
|
|
394
|
+
"""Show per-KPI detail rows for a given year and role."""
|
|
395
|
+
try:
|
|
396
|
+
_out(ctx, kpi.get_details(_client(ctx), year, role, month))
|
|
397
|
+
except Exception as e:
|
|
398
|
+
_handle_error(e)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@kpi_group.command("summary")
|
|
402
|
+
@click.option("--year", required=True, type=int, help="KPI year.")
|
|
403
|
+
@click.option("--role", required=True, help="Job role (from 'kpi roles').")
|
|
404
|
+
@click.pass_context
|
|
405
|
+
def kpi_summary(ctx, year: int, role: str) -> None:
|
|
406
|
+
"""Show monthly KPI summary (total marks, grade) for a year and role."""
|
|
407
|
+
try:
|
|
408
|
+
_out(ctx, kpi.get_summary(_client(ctx), year, role))
|
|
409
|
+
except Exception as e:
|
|
410
|
+
_handle_error(e)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ── G32 Leave commands ────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
@cli.group("leave")
|
|
416
|
+
def leave_group() -> None:
|
|
417
|
+
"""Worksy leave history."""
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@leave_group.command("list")
|
|
421
|
+
@click.option("--uid", default=None, help="Staff Firebase UID (default: self).")
|
|
422
|
+
@click.option("--premise", default=None, help="Filter by premise UUID.")
|
|
423
|
+
@click.option("--status", default=None, help="Filter by status (e.g. approved, pending).")
|
|
424
|
+
@click.option("--type", "leave_type", default=None, help="Filter by leave type.")
|
|
425
|
+
@click.option("--month", default=None, help="Filter by month YYYY-MM.")
|
|
426
|
+
@click.option("--start", default=None, help="Start date YYYY-MM-DD.")
|
|
427
|
+
@click.option("--end", default=None, help="End date YYYY-MM-DD.")
|
|
428
|
+
@click.option("--page", default=1, show_default=True)
|
|
429
|
+
@click.option("--limit", default=100, show_default=True)
|
|
430
|
+
@click.pass_context
|
|
431
|
+
def leave_list(ctx, uid, premise, status, leave_type, month, start, end, page, limit) -> None:
|
|
432
|
+
"""List Worksy leave records with summary statistics."""
|
|
433
|
+
try:
|
|
434
|
+
_out(ctx, leave.get_leave_history(_client(ctx), uid, premise, status, leave_type, month, start, end, page, limit))
|
|
435
|
+
except Exception as e:
|
|
436
|
+
_handle_error(e)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ── REPL ──────────────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
@cli.command("repl")
|
|
442
|
+
@click.option("--json", "json_mode", is_flag=True, default=False)
|
|
443
|
+
@click.pass_context
|
|
444
|
+
def repl(ctx, json_mode: bool) -> None:
|
|
445
|
+
"""Start interactive REPL session."""
|
|
446
|
+
skin = ReplSkin(cli, json_mode=json_mode or ctx.obj.get("json", False))
|
|
447
|
+
skin.run()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""G32 KPI domain operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from cli_anything.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_roles(client: G32Client) -> Any:
|
|
8
|
+
"""GET /g32/kpi/roles — distinct job_role values for the authenticated staff."""
|
|
9
|
+
return client.get("/g32/kpi/roles")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_years(client: G32Client) -> Any:
|
|
13
|
+
"""GET /g32/kpi/years — distinct years with KPI data for the authenticated staff."""
|
|
14
|
+
return client.get("/g32/kpi/years")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_details(client: G32Client, year: int, role: str, month: Optional[int] = None) -> Any:
|
|
18
|
+
"""GET /g32/kpi/details — per-KPI detail rows for a given year/role/month."""
|
|
19
|
+
params: dict = {"year": year, "role": role}
|
|
20
|
+
if month is not None:
|
|
21
|
+
params["month"] = month
|
|
22
|
+
return client.get("/g32/kpi/details", params=params)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_summary(client: G32Client, year: int, role: str) -> Any:
|
|
26
|
+
"""GET /g32/kpi/summary — monthly summary rows (total_marks, grade) for a year/role."""
|
|
27
|
+
return client.get("/g32/kpi/summary", params={"year": year, "role": role})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""G32 leave history domain operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from cli_anything.g32.core.client import G32Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_leave_history(
|
|
8
|
+
client: G32Client,
|
|
9
|
+
staff_uid: Optional[str] = None,
|
|
10
|
+
premise_uuid: Optional[str] = None,
|
|
11
|
+
status: Optional[str] = None,
|
|
12
|
+
leave_type: Optional[str] = None,
|
|
13
|
+
month: Optional[str] = None,
|
|
14
|
+
start_date: Optional[str] = None,
|
|
15
|
+
end_date: Optional[str] = None,
|
|
16
|
+
page: int = 1,
|
|
17
|
+
limit: int = 100,
|
|
18
|
+
) -> Any:
|
|
19
|
+
"""GET /g32/personal/worksy-leave-history — paginated leave records with summary."""
|
|
20
|
+
params: dict = {"page": page, "limit": limit}
|
|
21
|
+
if staff_uid:
|
|
22
|
+
params["staff_firebase_uid"] = staff_uid
|
|
23
|
+
if premise_uuid:
|
|
24
|
+
params["premise_uuid"] = premise_uuid
|
|
25
|
+
if status:
|
|
26
|
+
params["status"] = status
|
|
27
|
+
if leave_type:
|
|
28
|
+
params["leave_type"] = leave_type
|
|
29
|
+
if month:
|
|
30
|
+
params["month"] = month
|
|
31
|
+
if start_date:
|
|
32
|
+
params["start_date"] = start_date
|
|
33
|
+
if end_date:
|
|
34
|
+
params["end_date"] = end_date
|
|
35
|
+
return client.get("/g32/personal/worksy-leave-history", params=params)
|
|
@@ -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": "g32-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 G32 CLI harness."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
BANNER = """\
|
|
8
|
+
╔══════════════════════════════════════════╗
|
|
9
|
+
║ G32 Nexus CLI • type 'help' ║
|
|
10
|
+
╚══════════════════════════════════════════╝
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
PROMPT = "g32> "
|
|
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 ~/.g32_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() / ".g32_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: g32-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.g32.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: g32-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-g32
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI-Anything harness for the G32 Nexus 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,21 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
cli_anything/g32/__main__.py
|
|
3
|
+
cli_anything/g32/g32_cli.py
|
|
4
|
+
cli_anything/g32/core/case_notes.py
|
|
5
|
+
cli_anything/g32/core/client.py
|
|
6
|
+
cli_anything/g32/core/lab_reports.py
|
|
7
|
+
cli_anything/g32/core/medication.py
|
|
8
|
+
cli_anything/g32/core/patients.py
|
|
9
|
+
cli_anything/g32/core/staff.py
|
|
10
|
+
cli_anything/g32/core/stats.py
|
|
11
|
+
cli_anything/g32/g32_feature/kpi.py
|
|
12
|
+
cli_anything/g32/g32_feature/leave.py
|
|
13
|
+
cli_anything/g32/utils/auth.py
|
|
14
|
+
cli_anything/g32/utils/repl_skin.py
|
|
15
|
+
cli_anything/g32/utils/session.py
|
|
16
|
+
cli_anything_g32.egg-info/PKG-INFO
|
|
17
|
+
cli_anything_g32.egg-info/SOURCES.txt
|
|
18
|
+
cli_anything_g32.egg-info/dependency_links.txt
|
|
19
|
+
cli_anything_g32.egg-info/entry_points.txt
|
|
20
|
+
cli_anything_g32.egg-info/requires.txt
|
|
21
|
+
cli_anything_g32.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cli_anything
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_namespace_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="cli-anything-g32",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="CLI-Anything harness for the G32 Nexus 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
|
+
"g32-agent=cli_anything.g32.g32_cli:cli",
|
|
15
|
+
"g32=cli_anything.g32.g32_cli:cli",
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
python_requires=">=3.10",
|
|
19
|
+
)
|