senselab-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +4 -0
- cli/__main__.py +6 -0
- cli/api.py +205 -0
- cli/apply.py +226 -0
- cli/config.py +65 -0
- cli/main.py +1183 -0
- senselab_cli-0.1.0.dist-info/METADATA +186 -0
- senselab_cli-0.1.0.dist-info/RECORD +11 -0
- senselab_cli-0.1.0.dist-info/WHEEL +5 -0
- senselab_cli-0.1.0.dist-info/entry_points.txt +2 -0
- senselab_cli-0.1.0.dist-info/top_level.txt +1 -0
cli/__init__.py
ADDED
cli/__main__.py
ADDED
cli/api.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class APIError(Exception):
|
|
9
|
+
message: str
|
|
10
|
+
status_code: int | None = None
|
|
11
|
+
suggestion: str | None = None
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
if self.suggestion:
|
|
15
|
+
return f"{self.message} {self.suggestion}"
|
|
16
|
+
return self.message
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APIClient:
|
|
20
|
+
def __init__(self, base_url: str, token: str | None, timeout_seconds: int = 60):
|
|
21
|
+
self.base_url = base_url.rstrip("/")
|
|
22
|
+
self.token = token
|
|
23
|
+
self.timeout_seconds = timeout_seconds
|
|
24
|
+
|
|
25
|
+
def _headers(self) -> dict[str, str]:
|
|
26
|
+
headers = {"Content-Type": "application/json"}
|
|
27
|
+
if self.token:
|
|
28
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
29
|
+
return headers
|
|
30
|
+
|
|
31
|
+
def _request(
|
|
32
|
+
self,
|
|
33
|
+
method: str,
|
|
34
|
+
path: str,
|
|
35
|
+
*,
|
|
36
|
+
json_body: dict[str, Any] | None = None,
|
|
37
|
+
params: dict[str, Any] | None = None,
|
|
38
|
+
timeout_seconds: int | None = None,
|
|
39
|
+
) -> Any:
|
|
40
|
+
try:
|
|
41
|
+
import requests
|
|
42
|
+
except ImportError as e:
|
|
43
|
+
raise APIError(
|
|
44
|
+
"Missing dependency 'requests'. Install project dependencies to use CLI API calls."
|
|
45
|
+
) from e
|
|
46
|
+
|
|
47
|
+
if not self.token:
|
|
48
|
+
raise APIError(
|
|
49
|
+
"Not logged in.",
|
|
50
|
+
suggestion="Run `sense-cli login` or set SENSELAB_TOKEN.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
url = urljoin(f"{self.base_url}/", path.lstrip("/"))
|
|
54
|
+
try:
|
|
55
|
+
response = requests.request(
|
|
56
|
+
method=method,
|
|
57
|
+
url=url,
|
|
58
|
+
headers=self._headers(),
|
|
59
|
+
json=json_body,
|
|
60
|
+
params=params,
|
|
61
|
+
timeout=timeout_seconds or self.timeout_seconds,
|
|
62
|
+
)
|
|
63
|
+
except requests.Timeout as e:
|
|
64
|
+
raise APIError(
|
|
65
|
+
"Request timed out while contacting SenseLab API.",
|
|
66
|
+
suggestion="Try again or check backend availability.",
|
|
67
|
+
) from e
|
|
68
|
+
except requests.RequestException as e:
|
|
69
|
+
raise APIError(
|
|
70
|
+
f"Failed to reach API: {e}",
|
|
71
|
+
suggestion="Check network connectivity and --base-url.",
|
|
72
|
+
) from e
|
|
73
|
+
|
|
74
|
+
if response.status_code == 401:
|
|
75
|
+
raise APIError(
|
|
76
|
+
"Not logged in.",
|
|
77
|
+
status_code=401,
|
|
78
|
+
suggestion="Run `sense-cli login` or set SENSELAB_TOKEN.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if response.status_code >= 400:
|
|
82
|
+
try:
|
|
83
|
+
payload = response.json()
|
|
84
|
+
message = payload.get("message") or json.dumps(payload)
|
|
85
|
+
except Exception:
|
|
86
|
+
message = response.text or "Unexpected API error."
|
|
87
|
+
raise APIError(message, status_code=response.status_code)
|
|
88
|
+
|
|
89
|
+
if not response.text:
|
|
90
|
+
return {}
|
|
91
|
+
try:
|
|
92
|
+
return response.json()
|
|
93
|
+
except Exception:
|
|
94
|
+
return response.text
|
|
95
|
+
|
|
96
|
+
def whoami(self) -> dict[str, Any]:
|
|
97
|
+
result = self._request("GET", "/api/whoami/")
|
|
98
|
+
return result if isinstance(result, dict) else {"raw": result}
|
|
99
|
+
|
|
100
|
+
def get_projects(self) -> list[dict[str, Any]]:
|
|
101
|
+
result = self._request("GET", "/api/projects")
|
|
102
|
+
return result if isinstance(result, list) else []
|
|
103
|
+
|
|
104
|
+
def get_specialists(self, project_guid: str) -> list[dict[str, Any]]:
|
|
105
|
+
result = self._request("GET", f"/api/specialists/{project_guid}")
|
|
106
|
+
return result if isinstance(result, list) else []
|
|
107
|
+
|
|
108
|
+
def get_functions(
|
|
109
|
+
self, project_guid: str, specialist_guid: str
|
|
110
|
+
) -> list[dict[str, Any]]:
|
|
111
|
+
result = self._request(
|
|
112
|
+
"GET", f"/api/specialists/{project_guid}/workflows/{specialist_guid}"
|
|
113
|
+
)
|
|
114
|
+
return result if isinstance(result, list) else []
|
|
115
|
+
|
|
116
|
+
def run_orchestrator_sync(
|
|
117
|
+
self, project_guid: str, query: str, conversation_guid: str | None = None
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
payload: dict[str, Any] = {"query": query}
|
|
120
|
+
if conversation_guid:
|
|
121
|
+
payload["conversation_guid"] = conversation_guid
|
|
122
|
+
return self._request(
|
|
123
|
+
"POST",
|
|
124
|
+
f"/api/orchestrator/{project_guid}/run-sync",
|
|
125
|
+
json_body=payload,
|
|
126
|
+
timeout_seconds=600,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def run_specialist_sync(
|
|
130
|
+
self,
|
|
131
|
+
project_guid: str,
|
|
132
|
+
specialist_guid: str,
|
|
133
|
+
query: str,
|
|
134
|
+
conversation_guid: str | None = None,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
payload: dict[str, Any] = {"query": query, "specialist_guid": specialist_guid}
|
|
137
|
+
if conversation_guid:
|
|
138
|
+
payload["conversation_guid"] = conversation_guid
|
|
139
|
+
return self._request(
|
|
140
|
+
"POST",
|
|
141
|
+
f"/api/specialist/{project_guid}/run-sync",
|
|
142
|
+
json_body=payload,
|
|
143
|
+
timeout_seconds=600,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def execute_function(
|
|
147
|
+
self, project_guid: str, specialist_guid: str, workflow_id: str
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
return self._request(
|
|
150
|
+
"POST",
|
|
151
|
+
f"/api/specialists/{project_guid}/workflows/{specialist_guid}/{workflow_id}",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def get_execution_status(
|
|
155
|
+
self, project_guid: str, workflow_id: str, execution_guid: str
|
|
156
|
+
) -> dict[str, Any]:
|
|
157
|
+
result = self._request(
|
|
158
|
+
"GET",
|
|
159
|
+
f"/api/specialists/{project_guid}/workflows/{workflow_id}/{execution_guid}",
|
|
160
|
+
)
|
|
161
|
+
return result if isinstance(result, dict) else {"raw": result}
|
|
162
|
+
|
|
163
|
+
def list_connectors(self) -> list[dict[str, Any]]:
|
|
164
|
+
result = self._request("GET", "/api/connectors/list")
|
|
165
|
+
return result if isinstance(result, list) else []
|
|
166
|
+
|
|
167
|
+
def list_connector_instances(self, project_guid: str) -> list[dict[str, Any]]:
|
|
168
|
+
result = self._request("GET", f"/api/connectors/{project_guid}")
|
|
169
|
+
return result if isinstance(result, list) else []
|
|
170
|
+
|
|
171
|
+
def create_specialist(
|
|
172
|
+
self, project_guid: str, payload: dict[str, Any]
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
return self._request("POST", f"/api/specialists/{project_guid}", json_body=payload)
|
|
175
|
+
|
|
176
|
+
def update_specialist(
|
|
177
|
+
self, project_guid: str, specialist_guid: str, payload: dict[str, Any]
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
return self._request(
|
|
180
|
+
"PUT",
|
|
181
|
+
f"/api/specialists/{project_guid}/{specialist_guid}",
|
|
182
|
+
json_body=payload,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def configure_specialist(
|
|
186
|
+
self, project_guid: str, specialist_guid: str, payload: dict[str, Any]
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
return self._request(
|
|
189
|
+
"POST",
|
|
190
|
+
f"/api/specialists/{project_guid}/{specialist_guid}/config",
|
|
191
|
+
json_body=payload,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def create_specialist_workflows(
|
|
195
|
+
self,
|
|
196
|
+
project_guid: str,
|
|
197
|
+
specialist_guid: str,
|
|
198
|
+
workflows: list[dict[str, Any]],
|
|
199
|
+
) -> list[dict[str, Any]] | dict[str, Any]:
|
|
200
|
+
return self._request(
|
|
201
|
+
"POST",
|
|
202
|
+
f"/api/specialists/{project_guid}/workflows/{specialist_guid}",
|
|
203
|
+
json_body={"workflows": workflows},
|
|
204
|
+
)
|
|
205
|
+
|
cli/apply.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from cli.api import APIClient, APIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ApplyResult:
|
|
15
|
+
specialist_guid: str
|
|
16
|
+
connectors_attached: int
|
|
17
|
+
workflows_created: int
|
|
18
|
+
config_applied: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_uuid(value: str) -> bool:
|
|
22
|
+
try:
|
|
23
|
+
UUID(value)
|
|
24
|
+
return True
|
|
25
|
+
except Exception:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _extract_credential_guid(item: dict[str, Any]) -> str | None:
|
|
30
|
+
for key in ("tool_cred_guid", "cred_guid", "db_secret", "guid", "id"):
|
|
31
|
+
value = item.get(key)
|
|
32
|
+
if value:
|
|
33
|
+
return str(value)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_credential_name(item: dict[str, Any]) -> str | None:
|
|
38
|
+
for key in ("tool_cred_name", "cred_name", "connection_name", "name"):
|
|
39
|
+
value = item.get(key)
|
|
40
|
+
if value:
|
|
41
|
+
return str(value)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_connector_reference(
|
|
46
|
+
connector_ref: str, connector_instances: list[dict[str, Any]]
|
|
47
|
+
) -> str:
|
|
48
|
+
if _is_uuid(connector_ref):
|
|
49
|
+
return connector_ref
|
|
50
|
+
|
|
51
|
+
for item in connector_instances:
|
|
52
|
+
if _extract_credential_name(item) == connector_ref:
|
|
53
|
+
cred_guid = _extract_credential_guid(item)
|
|
54
|
+
if cred_guid:
|
|
55
|
+
return cred_guid
|
|
56
|
+
|
|
57
|
+
raise APIError(
|
|
58
|
+
f"Connector reference '{connector_ref}' not found in project connector instances."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_specialist_payload(spec: dict[str, Any]) -> dict[str, Any]:
|
|
63
|
+
required = ("name", "system_prompt", "prompt_template", "role_guid", "llm_guid")
|
|
64
|
+
missing = [field for field in required if not spec.get(field)]
|
|
65
|
+
if missing:
|
|
66
|
+
raise APIError(f"assistant is missing required fields: {', '.join(missing)}")
|
|
67
|
+
|
|
68
|
+
payload: dict[str, Any] = {
|
|
69
|
+
"name": spec["name"],
|
|
70
|
+
"system_prompt": spec["system_prompt"],
|
|
71
|
+
"prompt_template": spec["prompt_template"],
|
|
72
|
+
"role_guid": spec["role_guid"],
|
|
73
|
+
"llm_guid": spec["llm_guid"],
|
|
74
|
+
}
|
|
75
|
+
if spec.get("description"):
|
|
76
|
+
payload["description"] = spec["description"]
|
|
77
|
+
if spec.get("is_default") is not None:
|
|
78
|
+
payload["is_default"] = bool(spec["is_default"])
|
|
79
|
+
return payload
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _build_config_payload(connectors: list[dict[str, Any]]) -> dict[str, Any]:
|
|
83
|
+
config_payload: dict[str, Any] = {}
|
|
84
|
+
generic_items: list[dict[str, Any]] = []
|
|
85
|
+
for item in connectors:
|
|
86
|
+
connector_type = item.get("type")
|
|
87
|
+
cred_guid = item["resolved_cred_guid"]
|
|
88
|
+
config = item.get("config", {})
|
|
89
|
+
|
|
90
|
+
if connector_type == "github_repos":
|
|
91
|
+
config_payload["github_repos"] = {
|
|
92
|
+
"tool_cred_guid": cred_guid,
|
|
93
|
+
"repos": config.get("repos", []),
|
|
94
|
+
}
|
|
95
|
+
elif connector_type == "gitlab_repos":
|
|
96
|
+
config_payload["gitlab_repos"] = {
|
|
97
|
+
"tool_cred_guid": cred_guid,
|
|
98
|
+
"repos": config.get("repos", []),
|
|
99
|
+
}
|
|
100
|
+
elif connector_type == "aws_services":
|
|
101
|
+
config_payload["aws_services"] = {
|
|
102
|
+
"tool_cred_guid": cred_guid,
|
|
103
|
+
"region": config.get("region", ""),
|
|
104
|
+
"services": config.get("services", []),
|
|
105
|
+
"context": config.get("context", ""),
|
|
106
|
+
}
|
|
107
|
+
elif connector_type == "gcp_services":
|
|
108
|
+
config_payload["gcp_services"] = {
|
|
109
|
+
"tool_cred_guid": cred_guid,
|
|
110
|
+
"region": config.get("region", ""),
|
|
111
|
+
"services": config.get("services", []),
|
|
112
|
+
"context": config.get("context", ""),
|
|
113
|
+
}
|
|
114
|
+
elif connector_type == "generic":
|
|
115
|
+
generic_items.append(
|
|
116
|
+
{
|
|
117
|
+
"tool_cred_guid": cred_guid,
|
|
118
|
+
"plugin": config.get("plugin", ""),
|
|
119
|
+
"context": config.get("context", ""),
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
if generic_items:
|
|
123
|
+
config_payload["generic"] = generic_items
|
|
124
|
+
return config_payload
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_workflows_payload(functions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
128
|
+
workflows: list[dict[str, Any]] = []
|
|
129
|
+
for function in functions:
|
|
130
|
+
if not isinstance(function, dict):
|
|
131
|
+
raise APIError("functions entries must be objects.")
|
|
132
|
+
if not function.get("workflow_name") or not function.get("definition"):
|
|
133
|
+
raise APIError(
|
|
134
|
+
"Each function must include workflow_name and definition fields."
|
|
135
|
+
)
|
|
136
|
+
workflows.append(
|
|
137
|
+
{
|
|
138
|
+
"workflow_name": function["workflow_name"],
|
|
139
|
+
"definition": function["definition"],
|
|
140
|
+
"variables": function.get("variables"),
|
|
141
|
+
"schema_version": function.get("schema_version", "1.0.0"),
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
return workflows
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_apply_file(path: str) -> dict[str, Any]:
|
|
148
|
+
apply_path = Path(path).expanduser()
|
|
149
|
+
if not apply_path.exists():
|
|
150
|
+
raise APIError(f"YAML file not found: {apply_path}")
|
|
151
|
+
|
|
152
|
+
with apply_path.open("r", encoding="utf-8") as f:
|
|
153
|
+
parsed = yaml.safe_load(f)
|
|
154
|
+
if not isinstance(parsed, dict):
|
|
155
|
+
raise APIError("YAML root must be an object.")
|
|
156
|
+
return parsed
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def apply_assistant_config(
|
|
160
|
+
api: APIClient,
|
|
161
|
+
project_guid: str,
|
|
162
|
+
payload: dict[str, Any],
|
|
163
|
+
dry_run: bool = False,
|
|
164
|
+
) -> ApplyResult:
|
|
165
|
+
assistant = payload.get("assistant")
|
|
166
|
+
if not isinstance(assistant, dict):
|
|
167
|
+
raise APIError("Missing required 'assistant' section in YAML.")
|
|
168
|
+
|
|
169
|
+
connectors = payload.get("connectors", [])
|
|
170
|
+
if connectors is None:
|
|
171
|
+
connectors = []
|
|
172
|
+
if not isinstance(connectors, list):
|
|
173
|
+
raise APIError("'connectors' must be a list.")
|
|
174
|
+
|
|
175
|
+
functions = payload.get("functions", [])
|
|
176
|
+
if functions is None:
|
|
177
|
+
functions = []
|
|
178
|
+
if not isinstance(functions, list):
|
|
179
|
+
raise APIError("'functions' must be a list.")
|
|
180
|
+
|
|
181
|
+
specialist_payload = _build_specialist_payload(assistant)
|
|
182
|
+
connector_instances = api.list_connector_instances(project_guid)
|
|
183
|
+
|
|
184
|
+
resolved_connectors: list[dict[str, Any]] = []
|
|
185
|
+
for connector in connectors:
|
|
186
|
+
if not isinstance(connector, dict):
|
|
187
|
+
raise APIError("Each connector entry must be an object.")
|
|
188
|
+
connector_ref = connector.get("connector")
|
|
189
|
+
if not connector_ref:
|
|
190
|
+
raise APIError("Each connector entry must include 'connector'.")
|
|
191
|
+
resolved_guid = _resolve_connector_reference(str(connector_ref), connector_instances)
|
|
192
|
+
resolved_connectors.append({**connector, "resolved_cred_guid": resolved_guid})
|
|
193
|
+
|
|
194
|
+
workflows_payload = _build_workflows_payload(functions)
|
|
195
|
+
config_payload = _build_config_payload(resolved_connectors)
|
|
196
|
+
tool_cred_guids = [item["resolved_cred_guid"] for item in resolved_connectors]
|
|
197
|
+
|
|
198
|
+
if dry_run:
|
|
199
|
+
return ApplyResult(
|
|
200
|
+
specialist_guid="dry-run",
|
|
201
|
+
connectors_attached=len(tool_cred_guids),
|
|
202
|
+
workflows_created=len(workflows_payload),
|
|
203
|
+
config_applied=bool(config_payload),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
create_payload = {**specialist_payload}
|
|
207
|
+
if tool_cred_guids:
|
|
208
|
+
create_payload["tool_cred_guids"] = tool_cred_guids
|
|
209
|
+
specialist_result = api.create_specialist(project_guid, create_payload)
|
|
210
|
+
specialist_guid = specialist_result.get("specialist_guid")
|
|
211
|
+
if not specialist_guid:
|
|
212
|
+
raise APIError("Failed to create specialist: missing specialist_guid in response.")
|
|
213
|
+
|
|
214
|
+
if config_payload:
|
|
215
|
+
api.configure_specialist(project_guid, specialist_guid, config_payload)
|
|
216
|
+
|
|
217
|
+
if workflows_payload:
|
|
218
|
+
api.create_specialist_workflows(project_guid, specialist_guid, workflows_payload)
|
|
219
|
+
|
|
220
|
+
return ApplyResult(
|
|
221
|
+
specialist_guid=str(specialist_guid),
|
|
222
|
+
connectors_attached=len(tool_cred_guids),
|
|
223
|
+
workflows_created=len(workflows_payload),
|
|
224
|
+
config_applied=bool(config_payload),
|
|
225
|
+
)
|
|
226
|
+
|
cli/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_API_URL = "https://api.raia.live"
|
|
9
|
+
DEFAULT_CONFIG_PATH = Path.home() / ".sense-cli" / "config.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CLIConfig:
|
|
14
|
+
base_url: str = DEFAULT_API_URL
|
|
15
|
+
token: str | None = None
|
|
16
|
+
default_project_guid: str | None = None
|
|
17
|
+
email: str | None = None
|
|
18
|
+
frontend_url: str | None = None
|
|
19
|
+
telemetry_disabled: bool = False
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, raw: dict[str, Any]) -> "CLIConfig":
|
|
23
|
+
return cls(
|
|
24
|
+
base_url=raw.get("base_url", DEFAULT_API_URL),
|
|
25
|
+
token=raw.get("token"),
|
|
26
|
+
default_project_guid=raw.get("default_project_guid"),
|
|
27
|
+
email=raw.get("email"),
|
|
28
|
+
frontend_url=raw.get("frontend_url"),
|
|
29
|
+
telemetry_disabled=bool(raw.get("telemetry_disabled", False)),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ensure_parent(path: Path) -> None:
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_config_path() -> Path:
|
|
38
|
+
custom = os.getenv("SENSELAB_CONFIG_PATH")
|
|
39
|
+
return Path(custom).expanduser() if custom else DEFAULT_CONFIG_PATH
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_config() -> CLIConfig:
|
|
43
|
+
path = get_config_path()
|
|
44
|
+
if not path.exists():
|
|
45
|
+
return CLIConfig()
|
|
46
|
+
|
|
47
|
+
with path.open("r", encoding="utf-8") as f:
|
|
48
|
+
raw = json.load(f)
|
|
49
|
+
config = CLIConfig.from_dict(raw)
|
|
50
|
+
|
|
51
|
+
env_api = os.getenv("SENSELAB_API_URL")
|
|
52
|
+
env_token = os.getenv("SENSELAB_TOKEN")
|
|
53
|
+
if env_api:
|
|
54
|
+
config.base_url = env_api
|
|
55
|
+
if env_token:
|
|
56
|
+
config.token = env_token
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def save_config(config: CLIConfig) -> None:
|
|
61
|
+
path = get_config_path()
|
|
62
|
+
_ensure_parent(path)
|
|
63
|
+
with path.open("w", encoding="utf-8") as f:
|
|
64
|
+
json.dump(asdict(config), f, indent=2, sort_keys=True)
|
|
65
|
+
|