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.
Files changed (58) hide show
  1. hap_cli/README.md +194 -0
  2. hap_cli/README_CN.md +601 -0
  3. hap_cli/__init__.py +3 -0
  4. hap_cli/commands/__init__.py +1 -0
  5. hap_cli/commands/ai_cmd.py +224 -0
  6. hap_cli/commands/app_cmd.py +308 -0
  7. hap_cli/commands/calendar_cmd.py +138 -0
  8. hap_cli/commands/chat_cmd.py +101 -0
  9. hap_cli/commands/config_cmd.py +169 -0
  10. hap_cli/commands/contact_cmd.py +125 -0
  11. hap_cli/commands/department_cmd.py +168 -0
  12. hap_cli/commands/group_cmd.py +128 -0
  13. hap_cli/commands/instance_cmd.py +310 -0
  14. hap_cli/commands/node_cmd.py +538 -0
  15. hap_cli/commands/optionset_cmd.py +99 -0
  16. hap_cli/commands/page_cmd.py +102 -0
  17. hap_cli/commands/plugin_cmd.py +133 -0
  18. hap_cli/commands/post_cmd.py +155 -0
  19. hap_cli/commands/record_cmd.py +228 -0
  20. hap_cli/commands/role_cmd.py +221 -0
  21. hap_cli/commands/workflow_cmd.py +284 -0
  22. hap_cli/commands/worksheet_cmd.py +342 -0
  23. hap_cli/context.py +43 -0
  24. hap_cli/core/__init__.py +1 -0
  25. hap_cli/core/ai.py +133 -0
  26. hap_cli/core/app.py +307 -0
  27. hap_cli/core/auth.py +219 -0
  28. hap_cli/core/calendar_mod.py +114 -0
  29. hap_cli/core/chat.py +73 -0
  30. hap_cli/core/contact.py +85 -0
  31. hap_cli/core/department.py +131 -0
  32. hap_cli/core/flow_node.py +1001 -0
  33. hap_cli/core/group.py +99 -0
  34. hap_cli/core/instance.py +572 -0
  35. hap_cli/core/optionset.py +112 -0
  36. hap_cli/core/page.py +138 -0
  37. hap_cli/core/plugin.py +87 -0
  38. hap_cli/core/post.py +118 -0
  39. hap_cli/core/record.py +268 -0
  40. hap_cli/core/role.py +227 -0
  41. hap_cli/core/session.py +348 -0
  42. hap_cli/core/workflow.py +556 -0
  43. hap_cli/core/worksheet.py +403 -0
  44. hap_cli/hap_cli.py +105 -0
  45. hap_cli/skills/SKILL.md +383 -0
  46. hap_cli/skills/__init__.py +0 -0
  47. hap_cli/tests/__init__.py +1 -0
  48. hap_cli/tests/test_core.py +1824 -0
  49. hap_cli/tests/test_full_e2e.py +136 -0
  50. hap_cli/tests/test_integration.py +805 -0
  51. hap_cli/utils/__init__.py +1 -0
  52. hap_cli/utils/formatting.py +111 -0
  53. hap_cli/utils/options.py +10 -0
  54. hap_cli-0.5.0.dist-info/METADATA +223 -0
  55. hap_cli-0.5.0.dist-info/RECORD +58 -0
  56. hap_cli-0.5.0.dist-info/WHEEL +5 -0
  57. hap_cli-0.5.0.dist-info/entry_points.txt +2 -0
  58. hap_cli-0.5.0.dist-info/top_level.txt +1 -0
@@ -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)