wsapi-sdk 0.1.1__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.
- wsapi/__init__.py +35 -0
- wsapi/client.py +529 -0
- wsapi/exceptions.py +46 -0
- wsapi_sdk-0.1.1.dist-info/METADATA +10 -0
- wsapi_sdk-0.1.1.dist-info/RECORD +7 -0
- wsapi_sdk-0.1.1.dist-info/WHEEL +5 -0
- wsapi_sdk-0.1.1.dist-info/top_level.txt +1 -0
wsapi/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""WasaiTalent API Python SDK"""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
WasaiTalentClient,
|
|
5
|
+
AuthAPI,
|
|
6
|
+
TalentAPI,
|
|
7
|
+
AdminAPI,
|
|
8
|
+
OpenAPI,
|
|
9
|
+
)
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
WasaiAPIError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ForbiddenError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
ConflictError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
ServerError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"WasaiTalentClient",
|
|
22
|
+
"AuthAPI",
|
|
23
|
+
"TalentAPI",
|
|
24
|
+
"AdminAPI",
|
|
25
|
+
"OpenAPI",
|
|
26
|
+
"WasaiAPIError",
|
|
27
|
+
"AuthenticationError",
|
|
28
|
+
"ForbiddenError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"ConflictError",
|
|
31
|
+
"ValidationError",
|
|
32
|
+
"ServerError",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.1"
|
wsapi/client.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""WasaiTalent API SDK 核心客户端"""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
WasaiAPIError, AuthenticationError, ForbiddenError,
|
|
7
|
+
NotFoundError, ConflictError, ValidationError, ServerError
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# HTTP 状态码 → 异常类映射
|
|
11
|
+
_ERROR_MAP = {
|
|
12
|
+
400: ValidationError,
|
|
13
|
+
401: AuthenticationError,
|
|
14
|
+
403: ForbiddenError,
|
|
15
|
+
404: NotFoundError,
|
|
16
|
+
409: ConflictError,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _BaseClient:
|
|
21
|
+
"""底层 HTTP 请求封装,统一处理请求头、错误映射"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, base_url: str, token: Optional[str] = None, api_key: Optional[str] = None):
|
|
24
|
+
self.base_url = base_url.rstrip("/")
|
|
25
|
+
self.session = requests.Session()
|
|
26
|
+
self._token = token
|
|
27
|
+
self._api_key = api_key
|
|
28
|
+
|
|
29
|
+
# ---------- token / key 管理 ----------
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def token(self) -> Optional[str]:
|
|
33
|
+
return self._token
|
|
34
|
+
|
|
35
|
+
@token.setter
|
|
36
|
+
def token(self, value: str):
|
|
37
|
+
self._token = value
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def api_key(self) -> Optional[str]:
|
|
41
|
+
return self._api_key
|
|
42
|
+
|
|
43
|
+
@api_key.setter
|
|
44
|
+
def api_key(self, value: str):
|
|
45
|
+
self._api_key = value
|
|
46
|
+
|
|
47
|
+
# ---------- 请求头构建 ----------
|
|
48
|
+
|
|
49
|
+
def _build_headers(self, extra: Optional[Dict[str, str]] = None, use_api_key: bool = False) -> Dict[str, str]:
|
|
50
|
+
headers: Dict[str, str] = {}
|
|
51
|
+
if use_api_key and self._api_key:
|
|
52
|
+
headers["X-API-Key"] = self._api_key
|
|
53
|
+
elif self._token:
|
|
54
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
55
|
+
if extra:
|
|
56
|
+
headers.update(extra)
|
|
57
|
+
return headers
|
|
58
|
+
|
|
59
|
+
# ---------- 响应处理 ----------
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _raise_for_status(resp: requests.Response):
|
|
63
|
+
if resp.ok:
|
|
64
|
+
return
|
|
65
|
+
status = resp.status_code
|
|
66
|
+
try:
|
|
67
|
+
body = resp.json()
|
|
68
|
+
message = body.get("error", resp.text)
|
|
69
|
+
except Exception:
|
|
70
|
+
message = resp.text
|
|
71
|
+
body = None
|
|
72
|
+
exc_cls = _ERROR_MAP.get(status, ServerError if status >= 500 else WasaiAPIError)
|
|
73
|
+
raise exc_cls(message=message, status_code=status, response_data=body)
|
|
74
|
+
|
|
75
|
+
# ---------- 公共请求方法 ----------
|
|
76
|
+
|
|
77
|
+
def request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
params: Optional[Dict[str, Any]] = None,
|
|
83
|
+
json: Optional[Any] = None,
|
|
84
|
+
data: Optional[Any] = None,
|
|
85
|
+
files: Optional[Dict] = None,
|
|
86
|
+
headers: Optional[Dict[str, str]] = None,
|
|
87
|
+
use_api_key: bool = False,
|
|
88
|
+
raw: bool = False,
|
|
89
|
+
) -> Any:
|
|
90
|
+
url = f"{self.base_url}{path}"
|
|
91
|
+
req_headers = self._build_headers(headers, use_api_key=use_api_key)
|
|
92
|
+
# 清理 None 值
|
|
93
|
+
if params:
|
|
94
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
95
|
+
resp = self.session.request(
|
|
96
|
+
method, url,
|
|
97
|
+
params=params, json=json, data=data, files=files,
|
|
98
|
+
headers=req_headers,
|
|
99
|
+
)
|
|
100
|
+
self._raise_for_status(resp)
|
|
101
|
+
if raw:
|
|
102
|
+
return resp
|
|
103
|
+
# 部分接口返回纯文本(如 CSV 导出)
|
|
104
|
+
ct = resp.headers.get("Content-Type", "")
|
|
105
|
+
if "application/json" in ct:
|
|
106
|
+
return resp.json()
|
|
107
|
+
return resp.text
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AuthAPI:
|
|
111
|
+
"""认证相关接口(/api/auth)"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, client: _BaseClient):
|
|
114
|
+
self._c = client
|
|
115
|
+
|
|
116
|
+
def register(self, username: str, email: str, password: str) -> Dict[str, Any]:
|
|
117
|
+
"""注册新用户,返回 {user, token}"""
|
|
118
|
+
result = self._c.request("POST", "/api/auth/register", json={
|
|
119
|
+
"username": username, "email": email, "password": password
|
|
120
|
+
})
|
|
121
|
+
# 自动保存 token
|
|
122
|
+
if result and "token" in result:
|
|
123
|
+
self._c.token = result["token"]
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def login(self, username: str, password: str) -> Dict[str, Any]:
|
|
127
|
+
"""登录,返回 {user, token}"""
|
|
128
|
+
result = self._c.request("POST", "/api/auth/login", json={
|
|
129
|
+
"username": username, "password": password
|
|
130
|
+
})
|
|
131
|
+
if result and "token" in result:
|
|
132
|
+
self._c.token = result["token"]
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
def me(self) -> Dict[str, Any]:
|
|
136
|
+
"""获取当前用户信息"""
|
|
137
|
+
return self._c.request("GET", "/api/auth/me")
|
|
138
|
+
|
|
139
|
+
def change_password(self, old_password: str, new_password: str) -> Dict[str, Any]:
|
|
140
|
+
"""修改密码"""
|
|
141
|
+
return self._c.request("PUT", "/api/auth/password", json={
|
|
142
|
+
"oldPassword": old_password, "newPassword": new_password
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TalentAPI:
|
|
147
|
+
"""人才管理接口(/api/talents),需 JWT 认证"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, client: _BaseClient):
|
|
150
|
+
self._c = client
|
|
151
|
+
|
|
152
|
+
# ---------- 主资源 CRUD ----------
|
|
153
|
+
|
|
154
|
+
def list(self, **kwargs) -> Dict[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
获取人才列表(分页)。支持的查询参数:
|
|
157
|
+
search, data_source, import_method, status, open_to_work,
|
|
158
|
+
education, gender, location, skills, email, phone, wechat,
|
|
159
|
+
suitable_roles, job_preference, tags, linkedin_url, github_url,
|
|
160
|
+
maimai_url, homepage, paper_title, patent_title, conference_name,
|
|
161
|
+
experience_years_min, experience_years_max,
|
|
162
|
+
expected_salary_min, expected_salary_max,
|
|
163
|
+
page, limit, sort, order
|
|
164
|
+
"""
|
|
165
|
+
return self._c.request("GET", "/api/talents", params=kwargs)
|
|
166
|
+
|
|
167
|
+
def get(self, talent_id: int) -> Dict[str, Any]:
|
|
168
|
+
"""获取人才详情(含 profiles, experiences, educations, notes 等)"""
|
|
169
|
+
return self._c.request("GET", f"/api/talents/{talent_id}")
|
|
170
|
+
|
|
171
|
+
def create(self, name: str, **kwargs) -> Dict[str, Any]:
|
|
172
|
+
"""创建人才,name 必填"""
|
|
173
|
+
body = {"name": name, **kwargs}
|
|
174
|
+
return self._c.request("POST", "/api/talents", json=body)
|
|
175
|
+
|
|
176
|
+
def update(self, talent_id: int, **kwargs) -> Dict[str, Any]:
|
|
177
|
+
"""更新人才信息"""
|
|
178
|
+
return self._c.request("PUT", f"/api/talents/{talent_id}", json=kwargs)
|
|
179
|
+
|
|
180
|
+
def delete(self, talent_id: int) -> Dict[str, Any]:
|
|
181
|
+
"""删除人才"""
|
|
182
|
+
return self._c.request("DELETE", f"/api/talents/{talent_id}")
|
|
183
|
+
|
|
184
|
+
# ---------- 合并/关联 ----------
|
|
185
|
+
|
|
186
|
+
def merge(self, primary_talent_id: int, merged_talent_id: int,
|
|
187
|
+
match_type: str = "manual", match_confidence: float = 1.0) -> Dict[str, Any]:
|
|
188
|
+
"""关联/合并两个人才"""
|
|
189
|
+
return self._c.request("POST", "/api/talents/merge", json={
|
|
190
|
+
"primary_talent_id": primary_talent_id,
|
|
191
|
+
"merged_talent_id": merged_talent_id,
|
|
192
|
+
"match_type": match_type,
|
|
193
|
+
"match_confidence": match_confidence,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
def unmerge(self, primary_talent_id: int, merged_talent_id: int) -> Dict[str, Any]:
|
|
197
|
+
"""取消关联"""
|
|
198
|
+
return self._c.request("DELETE", "/api/talents/merge", json={
|
|
199
|
+
"primary_talent_id": primary_talent_id,
|
|
200
|
+
"merged_talent_id": merged_talent_id,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
# ---------- 统计 ----------
|
|
204
|
+
|
|
205
|
+
def stats_sources(self) -> Dict[str, Any]:
|
|
206
|
+
"""按数据来源统计"""
|
|
207
|
+
return self._c.request("GET", "/api/talents/stats/sources")
|
|
208
|
+
|
|
209
|
+
def stats_import_methods(self) -> Dict[str, Any]:
|
|
210
|
+
"""按导入方式统计"""
|
|
211
|
+
return self._c.request("GET", "/api/talents/stats/import-methods")
|
|
212
|
+
|
|
213
|
+
def stats_companies(self) -> Dict[str, Any]:
|
|
214
|
+
"""按公司统计(Top 20)"""
|
|
215
|
+
return self._c.request("GET", "/api/talents/stats/companies")
|
|
216
|
+
|
|
217
|
+
def stats_platforms(self) -> Dict[str, Any]:
|
|
218
|
+
"""按平台档案统计"""
|
|
219
|
+
return self._c.request("GET", "/api/talents/stats/platforms")
|
|
220
|
+
|
|
221
|
+
# ---------- 子资源通用方法 ----------
|
|
222
|
+
|
|
223
|
+
def _sub_list(self, talent_id: int, resource: str) -> Dict[str, Any]:
|
|
224
|
+
return self._c.request("GET", f"/api/talents/{talent_id}/{resource}")
|
|
225
|
+
|
|
226
|
+
def _sub_create(self, talent_id: int, resource: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
227
|
+
return self._c.request("POST", f"/api/talents/{talent_id}/{resource}", json=body)
|
|
228
|
+
|
|
229
|
+
def _sub_update(self, talent_id: int, resource: str, rid: int, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
230
|
+
return self._c.request("PUT", f"/api/talents/{talent_id}/{resource}/{rid}", json=body)
|
|
231
|
+
|
|
232
|
+
def _sub_delete(self, talent_id: int, resource: str, rid: int) -> Dict[str, Any]:
|
|
233
|
+
return self._c.request("DELETE", f"/api/talents/{talent_id}/{resource}/{rid}")
|
|
234
|
+
|
|
235
|
+
# ---------- 备注 ----------
|
|
236
|
+
|
|
237
|
+
def list_notes(self, talent_id: int) -> Dict[str, Any]:
|
|
238
|
+
return self._sub_list(talent_id, "notes")
|
|
239
|
+
|
|
240
|
+
def add_note(self, talent_id: int, content: str) -> Dict[str, Any]:
|
|
241
|
+
return self._sub_create(talent_id, "notes", {"content": content})
|
|
242
|
+
|
|
243
|
+
def update_note(self, talent_id: int, note_id: int, content: str) -> Dict[str, Any]:
|
|
244
|
+
return self._sub_update(talent_id, "notes", note_id, {"content": content})
|
|
245
|
+
|
|
246
|
+
def delete_note(self, talent_id: int, note_id: int) -> Dict[str, Any]:
|
|
247
|
+
return self._sub_delete(talent_id, "notes", note_id)
|
|
248
|
+
|
|
249
|
+
# ---------- 平台档案 ----------
|
|
250
|
+
|
|
251
|
+
def create_profile(self, talent_id: int, platform: str, **kwargs) -> Dict[str, Any]:
|
|
252
|
+
return self._sub_create(talent_id, "profiles", {"platform": platform, **kwargs})
|
|
253
|
+
|
|
254
|
+
def update_profile(self, talent_id: int, profile_id: int, **kwargs) -> Dict[str, Any]:
|
|
255
|
+
return self._sub_update(talent_id, "profiles", profile_id, kwargs)
|
|
256
|
+
|
|
257
|
+
def delete_profile(self, talent_id: int, profile_id: int) -> Dict[str, Any]:
|
|
258
|
+
return self._sub_delete(talent_id, "profiles", profile_id)
|
|
259
|
+
|
|
260
|
+
# ---------- 工作经历 ----------
|
|
261
|
+
|
|
262
|
+
def list_experiences(self, talent_id: int) -> Dict[str, Any]:
|
|
263
|
+
return self._sub_list(talent_id, "experiences")
|
|
264
|
+
|
|
265
|
+
def add_experience(self, talent_id: int, **kwargs) -> Dict[str, Any]:
|
|
266
|
+
return self._sub_create(talent_id, "experiences", kwargs)
|
|
267
|
+
|
|
268
|
+
def update_experience(self, talent_id: int, exp_id: int, **kwargs) -> Dict[str, Any]:
|
|
269
|
+
return self._sub_update(talent_id, "experiences", exp_id, kwargs)
|
|
270
|
+
|
|
271
|
+
def delete_experience(self, talent_id: int, exp_id: int) -> Dict[str, Any]:
|
|
272
|
+
return self._sub_delete(talent_id, "experiences", exp_id)
|
|
273
|
+
|
|
274
|
+
# ---------- 教育经历 ----------
|
|
275
|
+
|
|
276
|
+
def list_educations(self, talent_id: int) -> Dict[str, Any]:
|
|
277
|
+
return self._sub_list(talent_id, "educations")
|
|
278
|
+
|
|
279
|
+
def add_education(self, talent_id: int, **kwargs) -> Dict[str, Any]:
|
|
280
|
+
return self._sub_create(talent_id, "educations", kwargs)
|
|
281
|
+
|
|
282
|
+
def update_education(self, talent_id: int, edu_id: int, **kwargs) -> Dict[str, Any]:
|
|
283
|
+
return self._sub_update(talent_id, "educations", edu_id, kwargs)
|
|
284
|
+
|
|
285
|
+
def delete_education(self, talent_id: int, edu_id: int) -> Dict[str, Any]:
|
|
286
|
+
return self._sub_delete(talent_id, "educations", edu_id)
|
|
287
|
+
|
|
288
|
+
# ---------- 跟盯记录 ----------
|
|
289
|
+
|
|
290
|
+
def list_followups(self, talent_id: int) -> Dict[str, Any]:
|
|
291
|
+
return self._sub_list(talent_id, "followups")
|
|
292
|
+
|
|
293
|
+
def add_followup(self, talent_id: int, content: str, **kwargs) -> Dict[str, Any]:
|
|
294
|
+
return self._sub_create(talent_id, "followups", {"content": content, **kwargs})
|
|
295
|
+
|
|
296
|
+
def update_followup(self, talent_id: int, fid: int, **kwargs) -> Dict[str, Any]:
|
|
297
|
+
return self._sub_update(talent_id, "followups", fid, kwargs)
|
|
298
|
+
|
|
299
|
+
def delete_followup(self, talent_id: int, fid: int) -> Dict[str, Any]:
|
|
300
|
+
return self._sub_delete(talent_id, "followups", fid)
|
|
301
|
+
|
|
302
|
+
# ---------- 论文 ----------
|
|
303
|
+
|
|
304
|
+
def list_papers(self, talent_id: int) -> Dict[str, Any]:
|
|
305
|
+
return self._sub_list(talent_id, "papers")
|
|
306
|
+
|
|
307
|
+
def add_paper(self, talent_id: int, title: str, **kwargs) -> Dict[str, Any]:
|
|
308
|
+
return self._sub_create(talent_id, "papers", {"title": title, **kwargs})
|
|
309
|
+
|
|
310
|
+
def update_paper(self, talent_id: int, paper_id: int, **kwargs) -> Dict[str, Any]:
|
|
311
|
+
return self._sub_update(talent_id, "papers", paper_id, kwargs)
|
|
312
|
+
|
|
313
|
+
def delete_paper(self, talent_id: int, paper_id: int) -> Dict[str, Any]:
|
|
314
|
+
return self._sub_delete(talent_id, "papers", paper_id)
|
|
315
|
+
|
|
316
|
+
# ---------- 专利 ----------
|
|
317
|
+
|
|
318
|
+
def list_patents(self, talent_id: int) -> Dict[str, Any]:
|
|
319
|
+
return self._sub_list(talent_id, "patents")
|
|
320
|
+
|
|
321
|
+
def add_patent(self, talent_id: int, title: str, **kwargs) -> Dict[str, Any]:
|
|
322
|
+
return self._sub_create(talent_id, "patents", {"title": title, **kwargs})
|
|
323
|
+
|
|
324
|
+
def update_patent(self, talent_id: int, patent_id: int, **kwargs) -> Dict[str, Any]:
|
|
325
|
+
return self._sub_update(talent_id, "patents", patent_id, kwargs)
|
|
326
|
+
|
|
327
|
+
def delete_patent(self, talent_id: int, patent_id: int) -> Dict[str, Any]:
|
|
328
|
+
return self._sub_delete(talent_id, "patents", patent_id)
|
|
329
|
+
|
|
330
|
+
# ---------- 行业会议 ----------
|
|
331
|
+
|
|
332
|
+
def list_conferences(self, talent_id: int) -> Dict[str, Any]:
|
|
333
|
+
return self._sub_list(talent_id, "conferences")
|
|
334
|
+
|
|
335
|
+
def add_conference(self, talent_id: int, conference_name: str, **kwargs) -> Dict[str, Any]:
|
|
336
|
+
return self._sub_create(talent_id, "conferences", {"conference_name": conference_name, **kwargs})
|
|
337
|
+
|
|
338
|
+
def update_conference(self, talent_id: int, conf_id: int, **kwargs) -> Dict[str, Any]:
|
|
339
|
+
return self._sub_update(talent_id, "conferences", conf_id, kwargs)
|
|
340
|
+
|
|
341
|
+
def delete_conference(self, talent_id: int, conf_id: int) -> Dict[str, Any]:
|
|
342
|
+
return self._sub_delete(talent_id, "conferences", conf_id)
|
|
343
|
+
|
|
344
|
+
# ---------- GitHub 项目 ----------
|
|
345
|
+
|
|
346
|
+
def list_repos(self, talent_id: int) -> Dict[str, Any]:
|
|
347
|
+
return self._sub_list(talent_id, "repos")
|
|
348
|
+
|
|
349
|
+
def add_repo(self, talent_id: int, repo_name: str, **kwargs) -> Dict[str, Any]:
|
|
350
|
+
return self._sub_create(talent_id, "repos", {"repo_name": repo_name, **kwargs})
|
|
351
|
+
|
|
352
|
+
def update_repo(self, talent_id: int, repo_id: int, **kwargs) -> Dict[str, Any]:
|
|
353
|
+
return self._sub_update(talent_id, "repos", repo_id, kwargs)
|
|
354
|
+
|
|
355
|
+
def delete_repo(self, talent_id: int, repo_id: int) -> Dict[str, Any]:
|
|
356
|
+
return self._sub_delete(talent_id, "repos", repo_id)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class AdminAPI:
|
|
360
|
+
"""管理员接口(/api/admin),需 JWT + admin 权限"""
|
|
361
|
+
|
|
362
|
+
def __init__(self, client: _BaseClient):
|
|
363
|
+
self._c = client
|
|
364
|
+
|
|
365
|
+
def dashboard(self) -> Dict[str, Any]:
|
|
366
|
+
"""获取系统概览统计"""
|
|
367
|
+
return self._c.request("GET", "/api/admin/dashboard")
|
|
368
|
+
|
|
369
|
+
def list_users(self) -> Dict[str, Any]:
|
|
370
|
+
"""获取所有用户"""
|
|
371
|
+
return self._c.request("GET", "/api/admin/users")
|
|
372
|
+
|
|
373
|
+
def update_user_role(self, user_id: int, role: str) -> Dict[str, Any]:
|
|
374
|
+
"""修改用户角色 (admin / user / viewer)"""
|
|
375
|
+
return self._c.request("PUT", f"/api/admin/users/{user_id}/role", json={"role": role})
|
|
376
|
+
|
|
377
|
+
def delete_user(self, user_id: int) -> Dict[str, Any]:
|
|
378
|
+
"""删除用户(不能删除自己)"""
|
|
379
|
+
return self._c.request("DELETE", f"/api/admin/users/{user_id}")
|
|
380
|
+
|
|
381
|
+
def create_api_key(self, name: str = "Default Key", permissions: str = "read") -> Dict[str, Any]:
|
|
382
|
+
"""创建 API 密钥,返回 {id, name, key, permissions}"""
|
|
383
|
+
return self._c.request("POST", "/api/admin/api-keys", json={"name": name, "permissions": permissions})
|
|
384
|
+
|
|
385
|
+
def list_api_keys(self) -> Dict[str, Any]:
|
|
386
|
+
"""获取 API 密钥列表"""
|
|
387
|
+
return self._c.request("GET", "/api/admin/api-keys")
|
|
388
|
+
|
|
389
|
+
def delete_api_key(self, key_id: int) -> Dict[str, Any]:
|
|
390
|
+
"""删除 API 密钥"""
|
|
391
|
+
return self._c.request("DELETE", f"/api/admin/api-keys/{key_id}")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class OpenAPI:
|
|
395
|
+
"""开放接口(/api/open),需 API Key"""
|
|
396
|
+
|
|
397
|
+
def __init__(self, client: _BaseClient):
|
|
398
|
+
self._c = client
|
|
399
|
+
|
|
400
|
+
# ---------- 通用人才操作 ----------
|
|
401
|
+
|
|
402
|
+
def list_talents(self, **kwargs) -> Dict[str, Any]:
|
|
403
|
+
"""获取人才列表(search, data_source, import_method, limit, offset)"""
|
|
404
|
+
return self._c.request("GET", "/api/open/talents", params=kwargs, use_api_key=True)
|
|
405
|
+
|
|
406
|
+
def get_talent(self, talent_id: int) -> Dict[str, Any]:
|
|
407
|
+
"""获取人才详情 + profiles"""
|
|
408
|
+
return self._c.request("GET", f"/api/open/talents/{talent_id}", use_api_key=True)
|
|
409
|
+
|
|
410
|
+
def create_talent(self, name: str, **kwargs) -> Dict[str, Any]:
|
|
411
|
+
"""创建人才"""
|
|
412
|
+
return self._c.request("POST", "/api/open/talents", json={"name": name, **kwargs}, use_api_key=True)
|
|
413
|
+
|
|
414
|
+
# ---------- 平台导入 ----------
|
|
415
|
+
|
|
416
|
+
def import_github(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
417
|
+
"""GitHub 单条导入"""
|
|
418
|
+
return self._c.request("POST", "/api/open/import/github", json=data, use_api_key=True)
|
|
419
|
+
|
|
420
|
+
def import_github_batch(self, users: list) -> Dict[str, Any]:
|
|
421
|
+
"""GitHub 批量导入"""
|
|
422
|
+
return self._c.request("POST", "/api/open/import/github/batch", json={"users": users}, use_api_key=True)
|
|
423
|
+
|
|
424
|
+
def import_maimai(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
425
|
+
"""脉脉单条导入(支持详细格式 basic_info 和简略格式)"""
|
|
426
|
+
return self._c.request("POST", "/api/open/import/maimai", json=data, use_api_key=True)
|
|
427
|
+
|
|
428
|
+
def import_maimai_batch(self, users: list) -> Dict[str, Any]:
|
|
429
|
+
"""脉脉批量导入"""
|
|
430
|
+
return self._c.request("POST", "/api/open/import/maimai/batch", json={"users": users}, use_api_key=True)
|
|
431
|
+
|
|
432
|
+
def import_linkedin(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
433
|
+
"""LinkedIn 导入(完整简历 / 简略文本两种格式)"""
|
|
434
|
+
return self._c.request("POST", "/api/open/import/linkedin", json=data, use_api_key=True)
|
|
435
|
+
|
|
436
|
+
def import_wechat(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
437
|
+
"""微信单条导入"""
|
|
438
|
+
return self._c.request("POST", "/api/open/import/wechat", json=data, use_api_key=True)
|
|
439
|
+
|
|
440
|
+
def import_wechat_batch(self, contacts: list) -> Dict[str, Any]:
|
|
441
|
+
"""微信批量导入"""
|
|
442
|
+
return self._c.request("POST", "/api/open/import/wechat/batch", json={"contacts": contacts}, use_api_key=True)
|
|
443
|
+
|
|
444
|
+
def import_arxiv(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
445
|
+
"""arXiv 论文导入"""
|
|
446
|
+
return self._c.request("POST", "/api/open/import/arxiv", json=data, use_api_key=True)
|
|
447
|
+
|
|
448
|
+
def import_patent(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
449
|
+
"""专利导入"""
|
|
450
|
+
return self._c.request("POST", "/api/open/import/patent", json=data, use_api_key=True)
|
|
451
|
+
|
|
452
|
+
def import_conference(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
453
|
+
"""行业会议导入"""
|
|
454
|
+
return self._c.request("POST", "/api/open/import/conference", json=data, use_api_key=True)
|
|
455
|
+
|
|
456
|
+
# ---------- CSV 导入/导出 ----------
|
|
457
|
+
|
|
458
|
+
def import_csv(self, file_path: str) -> Dict[str, Any]:
|
|
459
|
+
"""CSV 文件上传导入(multipart/form-data)"""
|
|
460
|
+
with open(file_path, "rb") as f:
|
|
461
|
+
return self._c.request(
|
|
462
|
+
"POST", "/api/open/talents/import",
|
|
463
|
+
files={"file": ("talents.csv", f, "text/csv")},
|
|
464
|
+
use_api_key=True,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def export_csv(self) -> str:
|
|
468
|
+
"""CSV 导出,返回 CSV 文本"""
|
|
469
|
+
return self._c.request("GET", "/api/open/talents/export", use_api_key=True, raw=False)
|
|
470
|
+
|
|
471
|
+
# ---------- 批量导入 & 关联 ----------
|
|
472
|
+
|
|
473
|
+
def batch_import(self, talents: list) -> Dict[str, Any]:
|
|
474
|
+
"""JSON 批量导入"""
|
|
475
|
+
return self._c.request("POST", "/api/open/talents/batch", json={"talents": talents}, use_api_key=True)
|
|
476
|
+
|
|
477
|
+
def merge(self, primary_talent_id: int, merged_talent_id: int,
|
|
478
|
+
match_type: str = "api", match_confidence: float = 1.0) -> Dict[str, Any]:
|
|
479
|
+
"""人才关联"""
|
|
480
|
+
return self._c.request("POST", "/api/open/talents/merge", json={
|
|
481
|
+
"primary_talent_id": primary_talent_id,
|
|
482
|
+
"merged_talent_id": merged_talent_id,
|
|
483
|
+
"match_type": match_type,
|
|
484
|
+
"match_confidence": match_confidence,
|
|
485
|
+
}, use_api_key=True)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class WasaiTalentClient:
|
|
489
|
+
"""
|
|
490
|
+
WasaiTalent API 统一客户端入口。
|
|
491
|
+
|
|
492
|
+
用法:
|
|
493
|
+
# JWT 认证模式
|
|
494
|
+
client = WasaiTalentClient(base_url="http://localhost:3001")
|
|
495
|
+
client.auth.login("admin", "password123")
|
|
496
|
+
talents = client.talents.list(search="张三")
|
|
497
|
+
|
|
498
|
+
# API Key 模式(Open API)
|
|
499
|
+
client = WasaiTalentClient(base_url="http://localhost:3001", api_key="your-api-key")
|
|
500
|
+
result = client.open.list_talents(search="李四")
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
def __init__(
|
|
504
|
+
self,
|
|
505
|
+
base_url: str = "http://localhost:3001",
|
|
506
|
+
token: Optional[str] = None,
|
|
507
|
+
api_key: Optional[str] = None,
|
|
508
|
+
):
|
|
509
|
+
self._client = _BaseClient(base_url=base_url, token=token, api_key=api_key)
|
|
510
|
+
self.auth = AuthAPI(self._client)
|
|
511
|
+
self.talents = TalentAPI(self._client)
|
|
512
|
+
self.admin = AdminAPI(self._client)
|
|
513
|
+
self.open = OpenAPI(self._client)
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def token(self) -> Optional[str]:
|
|
517
|
+
return self._client.token
|
|
518
|
+
|
|
519
|
+
@token.setter
|
|
520
|
+
def token(self, value: str):
|
|
521
|
+
self._client.token = value
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def api_key(self) -> Optional[str]:
|
|
525
|
+
return self._client.api_key
|
|
526
|
+
|
|
527
|
+
@api_key.setter
|
|
528
|
+
def api_key(self, value: str):
|
|
529
|
+
self._client.api_key = value
|
wsapi/exceptions.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""WasaiTalent API SDK 自定义异常"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WasaiAPIError(Exception):
|
|
5
|
+
"""WasaiTalent API 基础异常"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int = None, response_data: dict = None):
|
|
8
|
+
self.message = message
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.response_data = response_data
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
def __str__(self):
|
|
14
|
+
if self.status_code:
|
|
15
|
+
return f"[{self.status_code}] {self.message}"
|
|
16
|
+
return self.message
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthenticationError(WasaiAPIError):
|
|
20
|
+
"""认证失败 (401)"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ForbiddenError(WasaiAPIError):
|
|
25
|
+
"""权限不足 (403)"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NotFoundError(WasaiAPIError):
|
|
30
|
+
"""资源不存在 (404)"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConflictError(WasaiAPIError):
|
|
35
|
+
"""资源冲突 (409)"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ValidationError(WasaiAPIError):
|
|
40
|
+
"""请求参数错误 (400)"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServerError(WasaiAPIError):
|
|
45
|
+
"""服务器错误 (500)"""
|
|
46
|
+
pass
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wsapi-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: WasaiTalent API Python SDK
|
|
5
|
+
Author-email: Chandler <275737875@qq.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: requests>=2.28.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
10
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
wsapi/__init__.py,sha256=JW1NlKsNBP0zG7AsPplR6M54UFiY65Dz7H4KEqXaV4I,584
|
|
2
|
+
wsapi/client.py,sha256=Ta_BHg1Dhn8Y1vd6Hkbfjztw-VXvY_WCLTxT5WtQ7vk,20932
|
|
3
|
+
wsapi/exceptions.py,sha256=Wsh2BzgjRqLTiHe_3arzQUkqz3GnGf_2dLxd0WH1EEM,973
|
|
4
|
+
wsapi_sdk-0.1.1.dist-info/METADATA,sha256=zXemrrSbXf0c1Qcpu1kBrvZqFmrjjiiFM6aNHR6VCvk,300
|
|
5
|
+
wsapi_sdk-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
wsapi_sdk-0.1.1.dist-info/top_level.txt,sha256=7Wgv4p4ljMCW5ScWG2Xb1DGDBDD__6EwSt8zBvSx9g8,6
|
|
7
|
+
wsapi_sdk-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wsapi
|