hap-cli 0.5.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.
- hap_cli/README.md +194 -0
- hap_cli/README_CN.md +601 -0
- hap_cli/__init__.py +3 -0
- hap_cli/commands/__init__.py +1 -0
- hap_cli/commands/ai_cmd.py +224 -0
- hap_cli/commands/app_cmd.py +308 -0
- hap_cli/commands/calendar_cmd.py +138 -0
- hap_cli/commands/chat_cmd.py +101 -0
- hap_cli/commands/config_cmd.py +169 -0
- hap_cli/commands/contact_cmd.py +125 -0
- hap_cli/commands/department_cmd.py +168 -0
- hap_cli/commands/group_cmd.py +128 -0
- hap_cli/commands/instance_cmd.py +310 -0
- hap_cli/commands/node_cmd.py +538 -0
- hap_cli/commands/optionset_cmd.py +99 -0
- hap_cli/commands/page_cmd.py +102 -0
- hap_cli/commands/plugin_cmd.py +133 -0
- hap_cli/commands/post_cmd.py +155 -0
- hap_cli/commands/record_cmd.py +228 -0
- hap_cli/commands/role_cmd.py +221 -0
- hap_cli/commands/workflow_cmd.py +284 -0
- hap_cli/commands/worksheet_cmd.py +342 -0
- hap_cli/context.py +43 -0
- hap_cli/core/__init__.py +1 -0
- hap_cli/core/ai.py +133 -0
- hap_cli/core/app.py +307 -0
- hap_cli/core/auth.py +219 -0
- hap_cli/core/calendar_mod.py +114 -0
- hap_cli/core/chat.py +73 -0
- hap_cli/core/contact.py +85 -0
- hap_cli/core/department.py +131 -0
- hap_cli/core/flow_node.py +1001 -0
- hap_cli/core/group.py +99 -0
- hap_cli/core/instance.py +572 -0
- hap_cli/core/optionset.py +112 -0
- hap_cli/core/page.py +138 -0
- hap_cli/core/plugin.py +87 -0
- hap_cli/core/post.py +118 -0
- hap_cli/core/record.py +268 -0
- hap_cli/core/role.py +227 -0
- hap_cli/core/session.py +348 -0
- hap_cli/core/workflow.py +556 -0
- hap_cli/core/worksheet.py +403 -0
- hap_cli/hap_cli.py +105 -0
- hap_cli/skills/SKILL.md +383 -0
- hap_cli/skills/__init__.py +0 -0
- hap_cli/tests/__init__.py +1 -0
- hap_cli/tests/test_core.py +1824 -0
- hap_cli/tests/test_full_e2e.py +136 -0
- hap_cli/tests/test_integration.py +805 -0
- hap_cli/utils/__init__.py +1 -0
- hap_cli/utils/formatting.py +111 -0
- hap_cli/utils/options.py +10 -0
- hap_cli-0.5.0.dist-info/METADATA +223 -0
- hap_cli-0.5.0.dist-info/RECORD +58 -0
- hap_cli-0.5.0.dist-info/WHEEL +5 -0
- hap_cli-0.5.0.dist-info/entry_points.txt +2 -0
- hap_cli-0.5.0.dist-info/top_level.txt +1 -0
hap_cli/core/session.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Session management and API client for MingDAO HAP.
|
|
2
|
+
|
|
3
|
+
URL routing by host type:
|
|
4
|
+
- mingdao SaaS — three sub-environments sharing the same main API but
|
|
5
|
+
different workflow/report API subdomains:
|
|
6
|
+
www.mingdao.com (production) api → api.mingdao.com
|
|
7
|
+
meihua.mingdao.com (pre-release) api → api2.mingdao.com
|
|
8
|
+
sandbox.mingdao.com (sandbox) api → api3.mingdao.com
|
|
9
|
+
|
|
10
|
+
main API : https://www.mingdao.com/api/{Controller}/{Action}
|
|
11
|
+
workflow : https://api{n}.mingdao.com/workflow/{path}
|
|
12
|
+
report : https://api{n}.mingdao.com/report/{path}
|
|
13
|
+
|
|
14
|
+
- nocoly (www.nocoly.com):
|
|
15
|
+
main API : https://www.nocoly.com/wwwapi/{Controller}/{Action}
|
|
16
|
+
workflow : https://www.nocoly.com/api/workflow/{path}
|
|
17
|
+
report : https://www.nocoly.com/report/{path}
|
|
18
|
+
|
|
19
|
+
- selfhosted (any other URL, e.g. https://p-demo.mingdaoyun.cn):
|
|
20
|
+
main API : {login_url}/wwwapi/{Controller}/{Action}
|
|
21
|
+
workflow : {login_url}/api/workflow/{path}
|
|
22
|
+
report : {login_url}/report/{path}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
from urllib.parse import urlparse
|
|
30
|
+
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
CONFIG_DIR = Path.home() / ".hap-cli"
|
|
34
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
35
|
+
|
|
36
|
+
# MingDAO SaaS: login hostname -> API subdomain prefix for workflow/report
|
|
37
|
+
# production uses "api", pre-release uses "api2", sandbox uses "api3"
|
|
38
|
+
MINGDAO_API_SUBDOMAIN: dict[str, str] = {
|
|
39
|
+
"www.mingdao.com": "api",
|
|
40
|
+
"meihua.mingdao.com": "api2",
|
|
41
|
+
"sandbox.mingdao.com": "api3",
|
|
42
|
+
}
|
|
43
|
+
# All known MingDAO SaaS hostnames
|
|
44
|
+
MINGDAO_HOSTS = set(MINGDAO_API_SUBDOMAIN.keys())
|
|
45
|
+
# Known Nocoly SaaS hostnames
|
|
46
|
+
NOCOLY_HOSTS = {"www.nocoly.com"}
|
|
47
|
+
|
|
48
|
+
DEFAULT_CONFIG = {
|
|
49
|
+
"login_url": "",
|
|
50
|
+
"auth_token": "",
|
|
51
|
+
"default_app_id": "",
|
|
52
|
+
"default_project_id": "",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _detect_host_type(url: str) -> str:
|
|
57
|
+
"""Detect host type from any URL (login URL or API base).
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
'mingdao', 'nocoly', or 'selfhosted'
|
|
61
|
+
"""
|
|
62
|
+
if not url:
|
|
63
|
+
return "selfhosted"
|
|
64
|
+
full_url = url if "://" in url else f"https://{url}"
|
|
65
|
+
hostname = urlparse(full_url).hostname or ""
|
|
66
|
+
if hostname in MINGDAO_HOSTS or hostname.endswith(".mingdao.com"):
|
|
67
|
+
return "mingdao"
|
|
68
|
+
if hostname in NOCOLY_HOSTS or hostname.endswith(".nocoly.com"):
|
|
69
|
+
return "nocoly"
|
|
70
|
+
return "selfhosted"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _main_api_base_for_url(url: str) -> str:
|
|
74
|
+
"""Return the main API base URL for any input URL (login URL or old API base).
|
|
75
|
+
|
|
76
|
+
Handles backward-compat: old configs stored API base like
|
|
77
|
+
'https://www.mingdao.com/api' or 'https://host/wwwapi'.
|
|
78
|
+
"""
|
|
79
|
+
if not url:
|
|
80
|
+
return ""
|
|
81
|
+
host_type = _detect_host_type(url)
|
|
82
|
+
if host_type == "mingdao":
|
|
83
|
+
return "https://www.mingdao.com/api"
|
|
84
|
+
if host_type == "nocoly":
|
|
85
|
+
return "https://www.nocoly.com/wwwapi"
|
|
86
|
+
# Self-hosted: strip any /api or /wwwapi suffix from old configs
|
|
87
|
+
base = url.rstrip("/")
|
|
88
|
+
if base.endswith("/api"):
|
|
89
|
+
base = base[:-4]
|
|
90
|
+
elif base.endswith("/wwwapi"):
|
|
91
|
+
base = base[:-7]
|
|
92
|
+
return f"{base}/wwwapi"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_login_url(url: str) -> str:
|
|
96
|
+
"""Normalize a stored URL to a clean login URL.
|
|
97
|
+
|
|
98
|
+
Old configs may have stored API base URLs like:
|
|
99
|
+
'https://www.mingdao.com/api' or 'https://host/wwwapi'.
|
|
100
|
+
Strips those suffixes to get the clean login URL.
|
|
101
|
+
"""
|
|
102
|
+
if not url:
|
|
103
|
+
return ""
|
|
104
|
+
base = url.rstrip("/")
|
|
105
|
+
if base.endswith("/api"):
|
|
106
|
+
host_type = _detect_host_type(base[:-4])
|
|
107
|
+
if host_type == "mingdao":
|
|
108
|
+
return base[:-4]
|
|
109
|
+
if base.endswith("/wwwapi"):
|
|
110
|
+
return base[:-7]
|
|
111
|
+
return base
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Session:
|
|
115
|
+
"""Manages authentication and API communication with MingDAO server."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, server_url: str = "", auth_token: str = ""):
|
|
118
|
+
# Store as login_url internally; server_url is the backward-compat name
|
|
119
|
+
self._login_url = server_url.rstrip("/")
|
|
120
|
+
self.auth_token = auth_token
|
|
121
|
+
self.default_app_id = ""
|
|
122
|
+
self.default_project_id = ""
|
|
123
|
+
|
|
124
|
+
# ── Properties ───────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def login_url(self) -> str:
|
|
128
|
+
return self._login_url
|
|
129
|
+
|
|
130
|
+
@login_url.setter
|
|
131
|
+
def login_url(self, value: str) -> None:
|
|
132
|
+
self._login_url = value.rstrip("/")
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def server_url(self) -> str:
|
|
136
|
+
"""Backward-compatible alias for login_url."""
|
|
137
|
+
return self._login_url
|
|
138
|
+
|
|
139
|
+
@server_url.setter
|
|
140
|
+
def server_url(self, value: str) -> None:
|
|
141
|
+
self._login_url = value.rstrip("/")
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def host_type(self) -> str:
|
|
145
|
+
"""Detected host type: 'mingdao', 'nocoly', or 'selfhosted'."""
|
|
146
|
+
return _detect_host_type(self._login_url)
|
|
147
|
+
|
|
148
|
+
# ── API Base URLs ─────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
def _main_api_base(self) -> str:
|
|
151
|
+
"""Main API base for app/worksheet/record/role/contact etc."""
|
|
152
|
+
if self.host_type == "mingdao":
|
|
153
|
+
return "https://www.mingdao.com/api"
|
|
154
|
+
if self.host_type == "nocoly":
|
|
155
|
+
return "https://www.nocoly.com/wwwapi"
|
|
156
|
+
return f"{self._login_url}/wwwapi"
|
|
157
|
+
|
|
158
|
+
def _mingdao_api_subdomain(self) -> str:
|
|
159
|
+
"""Return the API subdomain prefix for the current MingDAO environment.
|
|
160
|
+
|
|
161
|
+
www.mingdao.com -> 'api'
|
|
162
|
+
meihua.mingdao.com -> 'api2'
|
|
163
|
+
sandbox.mingdao.com -> 'api3'
|
|
164
|
+
"""
|
|
165
|
+
hostname = urlparse(
|
|
166
|
+
self._login_url if "://" in self._login_url else f"https://{self._login_url}"
|
|
167
|
+
).hostname or ""
|
|
168
|
+
return MINGDAO_API_SUBDOMAIN.get(hostname, "api")
|
|
169
|
+
|
|
170
|
+
def _workflow_api_base(self) -> str:
|
|
171
|
+
"""Workflow API base for process/node/instance operations."""
|
|
172
|
+
if self.host_type == "mingdao":
|
|
173
|
+
sub = self._mingdao_api_subdomain()
|
|
174
|
+
return f"https://{sub}.mingdao.com/workflow"
|
|
175
|
+
if self.host_type == "nocoly":
|
|
176
|
+
return "https://www.nocoly.com/api/workflow"
|
|
177
|
+
return f"{self._login_url}/api/workflow"
|
|
178
|
+
|
|
179
|
+
def _report_api_base(self) -> str:
|
|
180
|
+
"""Report API base for statistics/analytics."""
|
|
181
|
+
if self.host_type == "mingdao":
|
|
182
|
+
sub = self._mingdao_api_subdomain()
|
|
183
|
+
return f"https://{sub}.mingdao.com/report"
|
|
184
|
+
if self.host_type == "nocoly":
|
|
185
|
+
return "https://www.nocoly.com/report"
|
|
186
|
+
return f"{self._login_url}/report"
|
|
187
|
+
|
|
188
|
+
# ── Core HTTP ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def _post(
|
|
191
|
+
self,
|
|
192
|
+
url: str,
|
|
193
|
+
data: Optional[dict],
|
|
194
|
+
timeout: int,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Execute a POST request with standard HAP headers."""
|
|
197
|
+
headers = {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"Authorization": f"md_pss_id {self.auth_token}",
|
|
200
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
201
|
+
}
|
|
202
|
+
response = requests.post(
|
|
203
|
+
url,
|
|
204
|
+
json=data or {},
|
|
205
|
+
headers=headers,
|
|
206
|
+
timeout=timeout,
|
|
207
|
+
)
|
|
208
|
+
response.raise_for_status()
|
|
209
|
+
result = response.json()
|
|
210
|
+
if result.get("state") == 0:
|
|
211
|
+
raise APIError(
|
|
212
|
+
result.get("exception", "Unknown API error"),
|
|
213
|
+
code=result.get("code"),
|
|
214
|
+
)
|
|
215
|
+
return result.get("data", result)
|
|
216
|
+
|
|
217
|
+
# ── API Call Methods ──────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def api_call(
|
|
220
|
+
self,
|
|
221
|
+
controller: str,
|
|
222
|
+
action: str,
|
|
223
|
+
data: Optional[dict] = None,
|
|
224
|
+
timeout: int = 30,
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
"""Make a main API call.
|
|
227
|
+
|
|
228
|
+
URL: {main_api_base}/{controller}/{action}
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
controller: API controller name (e.g., 'Worksheet')
|
|
232
|
+
action: API action name (e.g., 'GetFilterRows')
|
|
233
|
+
data: Request body data
|
|
234
|
+
timeout: Request timeout in seconds
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
API response data
|
|
238
|
+
"""
|
|
239
|
+
if not self.is_configured():
|
|
240
|
+
raise SessionError("Session not configured. Run 'config set' first.")
|
|
241
|
+
url = f"{self._main_api_base()}/{controller}/{action}"
|
|
242
|
+
return self._post(url, data, timeout)
|
|
243
|
+
|
|
244
|
+
def workflow_call(
|
|
245
|
+
self,
|
|
246
|
+
path: str,
|
|
247
|
+
data: Optional[dict] = None,
|
|
248
|
+
timeout: int = 30,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
"""Make a workflow API call.
|
|
251
|
+
|
|
252
|
+
URL: {workflow_api_base}/{path}
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
path: API path (e.g., 'process/add', 'flowNode/saveNode')
|
|
256
|
+
data: Request body data
|
|
257
|
+
timeout: Request timeout in seconds
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
API response data
|
|
261
|
+
"""
|
|
262
|
+
if not self.is_configured():
|
|
263
|
+
raise SessionError("Session not configured. Run 'config set' first.")
|
|
264
|
+
url = f"{self._workflow_api_base()}/{path.lstrip('/')}"
|
|
265
|
+
return self._post(url, data, timeout)
|
|
266
|
+
|
|
267
|
+
def report_call(
|
|
268
|
+
self,
|
|
269
|
+
path: str,
|
|
270
|
+
data: Optional[dict] = None,
|
|
271
|
+
timeout: int = 30,
|
|
272
|
+
) -> dict[str, Any]:
|
|
273
|
+
"""Make a report/analytics API call.
|
|
274
|
+
|
|
275
|
+
URL: {report_api_base}/{path}
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
path: API path
|
|
279
|
+
data: Request body data
|
|
280
|
+
timeout: Request timeout in seconds
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
API response data
|
|
284
|
+
"""
|
|
285
|
+
if not self.is_configured():
|
|
286
|
+
raise SessionError("Session not configured. Run 'config set' first.")
|
|
287
|
+
url = f"{self._report_api_base()}/{path.lstrip('/')}"
|
|
288
|
+
return self._post(url, data, timeout)
|
|
289
|
+
|
|
290
|
+
# ── Config Persistence ────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def load(cls) -> "Session":
|
|
294
|
+
"""Load session from config file."""
|
|
295
|
+
config = load_config()
|
|
296
|
+
# Support both new 'login_url' key and legacy 'server_url' key
|
|
297
|
+
raw_url = config.get("login_url") or config.get("server_url", "")
|
|
298
|
+
# Normalize: old configs may have stored the API base URL
|
|
299
|
+
login_url = _normalize_login_url(raw_url)
|
|
300
|
+
session = cls(
|
|
301
|
+
server_url=login_url,
|
|
302
|
+
auth_token=config.get("auth_token", ""),
|
|
303
|
+
)
|
|
304
|
+
session.default_app_id = config.get("default_app_id", "")
|
|
305
|
+
session.default_project_id = config.get("default_project_id", "")
|
|
306
|
+
return session
|
|
307
|
+
|
|
308
|
+
def save(self) -> None:
|
|
309
|
+
"""Save session to config file."""
|
|
310
|
+
save_config(
|
|
311
|
+
{
|
|
312
|
+
"login_url": self._login_url,
|
|
313
|
+
"auth_token": self.auth_token,
|
|
314
|
+
"default_app_id": self.default_app_id,
|
|
315
|
+
"default_project_id": self.default_project_id,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def is_configured(self) -> bool:
|
|
320
|
+
"""Check if session has login URL and auth token."""
|
|
321
|
+
return bool(self._login_url and self.auth_token)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class SessionError(Exception):
|
|
325
|
+
"""Raised when session is not properly configured."""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class APIError(Exception):
|
|
329
|
+
"""Raised when API returns an error."""
|
|
330
|
+
|
|
331
|
+
def __init__(self, message: str, code: Optional[int] = None):
|
|
332
|
+
super().__init__(message)
|
|
333
|
+
self.code = code
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def load_config() -> dict:
|
|
337
|
+
"""Load config from file."""
|
|
338
|
+
if CONFIG_FILE.exists():
|
|
339
|
+
with open(CONFIG_FILE) as f:
|
|
340
|
+
return json.load(f)
|
|
341
|
+
return dict(DEFAULT_CONFIG)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def save_config(config: dict) -> None:
|
|
345
|
+
"""Save config to file."""
|
|
346
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
with open(CONFIG_FILE, "w") as f:
|
|
348
|
+
json.dump(config, f, indent=2)
|