tapdata-sdk 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.
- tapdata_sdk/__init__.py +55 -0
- tapdata_sdk/client.py +492 -0
- tapdata_sdk/enums.py +58 -0
- tapdata_sdk/exceptions.py +31 -0
- tapdata_sdk/models.py +168 -0
- tapdata_sdk/utils.py +110 -0
- tapdata_sdk-0.1.0.dist-info/METADATA +344 -0
- tapdata_sdk-0.1.0.dist-info/RECORD +11 -0
- tapdata_sdk-0.1.0.dist-info/WHEEL +5 -0
- tapdata_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- tapdata_sdk-0.1.0.dist-info/top_level.txt +1 -0
tapdata_sdk/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tapdata Python SDK
|
|
3
|
+
A Python client library for interacting with Tapdata API.
|
|
4
|
+
Examples:
|
|
5
|
+
>>> from tapdata_sdk import TapdataClient, ConnectionType, Status
|
|
6
|
+
>>>
|
|
7
|
+
>>> # Initialize client
|
|
8
|
+
>>> client = TapdataClient("http://localhost:3030")
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Login
|
|
11
|
+
>>> client.login("admin@test.com", "password")
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Query connections
|
|
14
|
+
>>> connections = client.connections.list(connection_type=ConnectionType.SOURCE)
|
|
15
|
+
>>>
|
|
16
|
+
>>> # Query tasks
|
|
17
|
+
>>> tasks = client.tasks.list_running()
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Operate tasks
|
|
20
|
+
>>> client.tasks.stop(tasks[0].id)
|
|
21
|
+
"""
|
|
22
|
+
from .client import TapdataClient, ConnectionClient, TaskClient
|
|
23
|
+
from .models import Connection, Task, TaskLog
|
|
24
|
+
from .enums import ConnectionType, DatabaseType, Status, LogLevel
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
TapdataError,
|
|
27
|
+
TapdataAuthError,
|
|
28
|
+
TapdataConnectionError,
|
|
29
|
+
TapdataValidationError,
|
|
30
|
+
TapdataTimeoutError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.2.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Client
|
|
37
|
+
"TapdataClient",
|
|
38
|
+
"ConnectionClient",
|
|
39
|
+
"TaskClient",
|
|
40
|
+
# Models
|
|
41
|
+
"Connection",
|
|
42
|
+
"Task",
|
|
43
|
+
"TaskLog",
|
|
44
|
+
# Enums
|
|
45
|
+
"ConnectionType",
|
|
46
|
+
"DatabaseType",
|
|
47
|
+
"Status",
|
|
48
|
+
"LogLevel",
|
|
49
|
+
# Exceptions
|
|
50
|
+
"TapdataError",
|
|
51
|
+
"TapdataAuthError",
|
|
52
|
+
"TapdataConnectionError",
|
|
53
|
+
"TapdataValidationError",
|
|
54
|
+
"TapdataTimeoutError",
|
|
55
|
+
]
|
tapdata_sdk/client.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Tapdata API Client"""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, List, Optional, Union
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import json as jsonx
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
TapdataAuthError,
|
|
13
|
+
TapdataConnectionError,
|
|
14
|
+
TapdataError,
|
|
15
|
+
TapdataTimeoutError,
|
|
16
|
+
)
|
|
17
|
+
from .models import Connection, Task, TaskDetail, TaskLog
|
|
18
|
+
from .utils import rc4_encrypt, gen_sign, build_filter
|
|
19
|
+
from .enums import ConnectionType, DatabaseType, Status, LogLevel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TapdataClient:
|
|
26
|
+
"""
|
|
27
|
+
Tapdata API Client
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
>>> client = TapdataClient("http://localhost:3030")
|
|
31
|
+
>>> client.login("admin@test.com", "password")
|
|
32
|
+
>>> connections = client.connections.list()
|
|
33
|
+
>>> tasks = client.tasks.list()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
DEFAULT_TIMEOUT = 30
|
|
37
|
+
DEFAULT_SECRET = "Gotapd8"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
base_url: str,
|
|
42
|
+
access_token: Optional[str] = None,
|
|
43
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
44
|
+
verify_ssl: bool = True,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize client
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: API base URL
|
|
51
|
+
access_token: Access token (optional)
|
|
52
|
+
timeout: Request timeout in seconds
|
|
53
|
+
verify_ssl: Whether to verify SSL certificate
|
|
54
|
+
"""
|
|
55
|
+
self.base_url = base_url.rstrip("/")
|
|
56
|
+
self.access_token = access_token
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
self.verify_ssl = verify_ssl
|
|
59
|
+
self.session = requests.Session()
|
|
60
|
+
|
|
61
|
+
# Initialize sub-clients
|
|
62
|
+
self.connections = ConnectionClient(self)
|
|
63
|
+
self.tasks = TaskClient(self)
|
|
64
|
+
|
|
65
|
+
def _build_url(self, path: str) -> str:
|
|
66
|
+
"""Build complete URL"""
|
|
67
|
+
return urljoin(self.base_url, path)
|
|
68
|
+
|
|
69
|
+
def _request(
|
|
70
|
+
self,
|
|
71
|
+
method: str,
|
|
72
|
+
path: str,
|
|
73
|
+
params: Optional[dict] = None,
|
|
74
|
+
json: Optional[dict] = None,
|
|
75
|
+
**kwargs,
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""
|
|
78
|
+
Send HTTP request
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
method: HTTP method
|
|
82
|
+
path: API path
|
|
83
|
+
params: URL parameters
|
|
84
|
+
json: JSON request body
|
|
85
|
+
**kwargs: Other request parameters
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Response data
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
TapdataError: API error
|
|
92
|
+
TapdataTimeoutError: Request timeout
|
|
93
|
+
TapdataConnectionError: Connection error
|
|
94
|
+
"""
|
|
95
|
+
params = params or {}
|
|
96
|
+
|
|
97
|
+
# Add access_token
|
|
98
|
+
url = self._build_url(path)
|
|
99
|
+
if self.access_token:
|
|
100
|
+
params["access_token"] = self.access_token
|
|
101
|
+
|
|
102
|
+
if params.get('filter'):
|
|
103
|
+
filter_str = jsonx.dumps(params.get('filter'), separators=(',', ':'))
|
|
104
|
+
params['filter'] = filter_str
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
logger.debug(f"Request: {method} {url}")
|
|
108
|
+
|
|
109
|
+
resp = self.session.request(
|
|
110
|
+
method=method,
|
|
111
|
+
url=url,
|
|
112
|
+
params=params,
|
|
113
|
+
json=json,
|
|
114
|
+
timeout=kwargs.get("timeout", self.timeout),
|
|
115
|
+
verify=self.verify_ssl,
|
|
116
|
+
**{k: v for k, v in kwargs.items() if k != "timeout"},
|
|
117
|
+
)
|
|
118
|
+
resp.raise_for_status()
|
|
119
|
+
|
|
120
|
+
data = resp.json()
|
|
121
|
+
|
|
122
|
+
# Check business status code
|
|
123
|
+
if data.get("code") != "ok":
|
|
124
|
+
error_code = data.get("code")
|
|
125
|
+
if error_code in ["UNAUTHORIZED", "FORBIDDEN"]:
|
|
126
|
+
raise TapdataAuthError(data)
|
|
127
|
+
raise TapdataError(data)
|
|
128
|
+
|
|
129
|
+
logger.debug(f"Response: {data.get('code')}")
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
except requests.exceptions.Timeout as e:
|
|
133
|
+
raise TapdataTimeoutError({"message": f"Request timeout: {e}"})
|
|
134
|
+
except requests.exceptions.ConnectionError as e:
|
|
135
|
+
raise TapdataConnectionError({"message": f"Connection error: {e}"})
|
|
136
|
+
except requests.exceptions.RequestException as e:
|
|
137
|
+
raise TapdataError({"message": f"Request failed: {e}"})
|
|
138
|
+
|
|
139
|
+
def get_timestamp(self) -> int:
|
|
140
|
+
"""
|
|
141
|
+
Get server timestamp
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Server timestamp
|
|
145
|
+
"""
|
|
146
|
+
resp = self._request("GET", "/api/timeStamp")
|
|
147
|
+
return resp["data"]
|
|
148
|
+
|
|
149
|
+
def login(
|
|
150
|
+
self,
|
|
151
|
+
email: str,
|
|
152
|
+
password: str,
|
|
153
|
+
secret: str = DEFAULT_SECRET,
|
|
154
|
+
) -> str:
|
|
155
|
+
"""
|
|
156
|
+
User login
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
email: Email
|
|
160
|
+
password: Password (plain text)
|
|
161
|
+
secret: Encryption secret key
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Access token
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
>>> client = TapdataClient("http://localhost:3030")
|
|
168
|
+
>>> token = client.login("admin@test.com", "password")
|
|
169
|
+
"""
|
|
170
|
+
stime = self.get_timestamp()
|
|
171
|
+
enc_pwd = rc4_encrypt(password, secret)
|
|
172
|
+
sign = gen_sign(email, enc_pwd, stime, secret)
|
|
173
|
+
|
|
174
|
+
resp = self._request(
|
|
175
|
+
"POST",
|
|
176
|
+
"/api/users/login",
|
|
177
|
+
json={
|
|
178
|
+
"email": email,
|
|
179
|
+
"password": enc_pwd,
|
|
180
|
+
"sign": sign,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self.access_token = resp["data"]["id"]
|
|
185
|
+
logger.info(f"Login successful: {email}")
|
|
186
|
+
|
|
187
|
+
return self.access_token
|
|
188
|
+
|
|
189
|
+
def logout(self) -> None:
|
|
190
|
+
"""Logout"""
|
|
191
|
+
self.access_token = None
|
|
192
|
+
self.session.close()
|
|
193
|
+
self.session = requests.Session()
|
|
194
|
+
logger.info("Logged out")
|
|
195
|
+
|
|
196
|
+
def is_authenticated(self) -> bool:
|
|
197
|
+
"""Check if authenticated"""
|
|
198
|
+
return self.access_token is not None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ConnectionClient:
|
|
202
|
+
"""Connection management client"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, client: TapdataClient):
|
|
205
|
+
self.client = client
|
|
206
|
+
|
|
207
|
+
def list(
|
|
208
|
+
self,
|
|
209
|
+
connection_type: Optional[Union[str, ConnectionType]] = None,
|
|
210
|
+
database_type: Optional[Union[str, DatabaseType]] = None,
|
|
211
|
+
status: Optional[Union[str, Status]] = None,
|
|
212
|
+
name: Optional[str] = None,
|
|
213
|
+
skip: int = 0,
|
|
214
|
+
limit: int = 20,
|
|
215
|
+
) -> List[Connection]:
|
|
216
|
+
"""
|
|
217
|
+
Query connection list
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
connection_type: Connection type
|
|
221
|
+
database_type: Database type
|
|
222
|
+
status: Status
|
|
223
|
+
skip: Number of records to skip
|
|
224
|
+
limit: Limit on number of results
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Connection list
|
|
228
|
+
|
|
229
|
+
Examples:
|
|
230
|
+
>>> connections = client.connections.list(
|
|
231
|
+
... connection_type=ConnectionType.SOURCE,
|
|
232
|
+
... database_type=DatabaseType.MYSQL
|
|
233
|
+
... )
|
|
234
|
+
"""
|
|
235
|
+
where = {"createType": {"$ne": "System"}}
|
|
236
|
+
order = "last_updated DESC"
|
|
237
|
+
|
|
238
|
+
if connection_type:
|
|
239
|
+
where["connection_type"] = str(connection_type)
|
|
240
|
+
if database_type:
|
|
241
|
+
where["database_type"] = str(database_type)
|
|
242
|
+
if status:
|
|
243
|
+
where["status"] = str(status)
|
|
244
|
+
if name:
|
|
245
|
+
where["name"] = {"like":str(name),"options":"i"}
|
|
246
|
+
|
|
247
|
+
resp = self.client._request(
|
|
248
|
+
"GET",
|
|
249
|
+
"/api/Connections",
|
|
250
|
+
params={"filter": build_filter(order=order, skip=skip, limit=limit, where=where)},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return [Connection.from_dict(item) for item in resp["data"]["items"]]
|
|
254
|
+
|
|
255
|
+
def get(self, connection_id: str) -> Connection:
|
|
256
|
+
"""
|
|
257
|
+
Get single connection details
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
connection_id: Connection ID
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Connection object
|
|
264
|
+
"""
|
|
265
|
+
resp = self.client._request("GET", f"/api/Connections/{connection_id}")
|
|
266
|
+
return Connection.from_dict(resp["data"])
|
|
267
|
+
|
|
268
|
+
def list_source(self) -> List[Connection]:
|
|
269
|
+
"""Get all source connections"""
|
|
270
|
+
return self.list(connection_type=ConnectionType.SOURCE)
|
|
271
|
+
|
|
272
|
+
def list_target(self) -> List[Connection]:
|
|
273
|
+
"""Get all target connections"""
|
|
274
|
+
return self.list(connection_type=ConnectionType.TARGET)
|
|
275
|
+
|
|
276
|
+
def list_mysql(self) -> List[Connection]:
|
|
277
|
+
"""Get all MySQL connections"""
|
|
278
|
+
return self.list(database_type=DatabaseType.MYSQL)
|
|
279
|
+
|
|
280
|
+
def list_clickhouse(self) -> List[Connection]:
|
|
281
|
+
"""Get all ClickHouse connections"""
|
|
282
|
+
return self.list(database_type=DatabaseType.CLICKHOUSE)
|
|
283
|
+
|
|
284
|
+
def list_mongodb(self) -> List[Connection]:
|
|
285
|
+
"""Get all MongoDB connections"""
|
|
286
|
+
return self.list(database_type=DatabaseType.MONGODB)
|
|
287
|
+
|
|
288
|
+
def list_valid(self) -> List[Connection]:
|
|
289
|
+
"""Get all valid connections"""
|
|
290
|
+
return self.list(status=Status.VALID)
|
|
291
|
+
|
|
292
|
+
def list_invalid(self) -> List[Connection]:
|
|
293
|
+
"""Get all invalid connections"""
|
|
294
|
+
return self.list(status=Status.INVALID)
|
|
295
|
+
|
|
296
|
+
def list_testing(self) -> List[Connection]:
|
|
297
|
+
"""Get all testing connections"""
|
|
298
|
+
return self.list(status=Status.TESTING)
|
|
299
|
+
|
|
300
|
+
class TaskClient:
|
|
301
|
+
"""Task management client"""
|
|
302
|
+
|
|
303
|
+
def __init__(self, client: TapdataClient):
|
|
304
|
+
self.client = client
|
|
305
|
+
|
|
306
|
+
def list(
|
|
307
|
+
self,
|
|
308
|
+
status: Optional[Union[str, Status]] = None,
|
|
309
|
+
name: Optional[str] = None,
|
|
310
|
+
skip: int = 0,
|
|
311
|
+
limit: int = 20,
|
|
312
|
+
) -> List[Task]:
|
|
313
|
+
"""
|
|
314
|
+
Query task list
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
status: Status filter
|
|
318
|
+
skip: Number of records to skip
|
|
319
|
+
limit: Limit on number of results
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Task list
|
|
323
|
+
"""
|
|
324
|
+
where = {}
|
|
325
|
+
if status:
|
|
326
|
+
where["status"] = str(status)
|
|
327
|
+
|
|
328
|
+
if name:
|
|
329
|
+
where["name"] = {"like": str(name),"options":"i"}
|
|
330
|
+
|
|
331
|
+
fields = {
|
|
332
|
+
"id": True,
|
|
333
|
+
"name": True,
|
|
334
|
+
"type": True,
|
|
335
|
+
"status": True,
|
|
336
|
+
"taskRecordId": True,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
resp = self.client._request(
|
|
340
|
+
"GET",
|
|
341
|
+
"/api/Task",
|
|
342
|
+
params={
|
|
343
|
+
"filter": build_filter(
|
|
344
|
+
skip=skip,
|
|
345
|
+
limit=limit,
|
|
346
|
+
where=where,
|
|
347
|
+
fields=fields,
|
|
348
|
+
)
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return [Task.from_dict(item) for item in resp["data"]["items"]]
|
|
353
|
+
|
|
354
|
+
def get(self, task_id: str) -> TaskDetail:
|
|
355
|
+
"""
|
|
356
|
+
Get single task details
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
task_id: Task ID
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Task object
|
|
363
|
+
"""
|
|
364
|
+
resp = self.client._request("GET", f"/api/Task/{task_id}")
|
|
365
|
+
return TaskDetail.from_dict(resp["data"])
|
|
366
|
+
|
|
367
|
+
def list_running(self) -> List[Task]:
|
|
368
|
+
"""Get all running tasks"""
|
|
369
|
+
return self.list(status=Status.RUNNING)
|
|
370
|
+
|
|
371
|
+
def list_error(self) -> List[Task]:
|
|
372
|
+
"""Get all error tasks"""
|
|
373
|
+
return self.list(status=Status.ERROR)
|
|
374
|
+
|
|
375
|
+
def start(self, task_id: str) -> dict:
|
|
376
|
+
"""
|
|
377
|
+
Start task
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
task_id: Task ID
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Operation result
|
|
384
|
+
"""
|
|
385
|
+
logger.info(f"Starting task: {task_id}")
|
|
386
|
+
return self.client._request(
|
|
387
|
+
"PUT",
|
|
388
|
+
"/api/Task/batchStart",
|
|
389
|
+
params={"taskIds": task_id},
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def stop(self, task_id: str) -> dict:
|
|
393
|
+
"""
|
|
394
|
+
Stop task
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
task_id: Task ID
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Operation result
|
|
401
|
+
"""
|
|
402
|
+
logger.info(f"Stopping task: {task_id}")
|
|
403
|
+
return self.client._request(
|
|
404
|
+
"PUT",
|
|
405
|
+
"/api/Task/batchStop",
|
|
406
|
+
params={"taskIds": task_id},
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def reset(self, task_id: str) -> dict:
|
|
410
|
+
"""
|
|
411
|
+
Reset task
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
task_id: Task ID
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Operation result
|
|
418
|
+
"""
|
|
419
|
+
logger.info(f"Resetting task: {task_id}")
|
|
420
|
+
return self.client._request(
|
|
421
|
+
"PATCH",
|
|
422
|
+
"/api/Task/batchRenew",
|
|
423
|
+
params={"taskIds": task_id},
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def delete(self, task_id: str) -> dict:
|
|
427
|
+
"""
|
|
428
|
+
Delete task
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
task_id: Task ID
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Operation result
|
|
435
|
+
"""
|
|
436
|
+
logger.warning(f"Deleting task: {task_id}")
|
|
437
|
+
return self.client._request(
|
|
438
|
+
"DELETE",
|
|
439
|
+
"/api/Task/batchDelete",
|
|
440
|
+
params={"taskIds": task_id},
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def get_logs(
|
|
444
|
+
self,
|
|
445
|
+
task_id: str,
|
|
446
|
+
task_record_id: str,
|
|
447
|
+
start: Optional[int] = None,
|
|
448
|
+
end: Optional[int] = None,
|
|
449
|
+
page: int = 1,
|
|
450
|
+
page_size: int = 20,
|
|
451
|
+
levels: Optional[List[Union[str, LogLevel]]] = None,
|
|
452
|
+
) -> List[TaskLog]:
|
|
453
|
+
"""
|
|
454
|
+
Get task logs
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
task_id: Task ID
|
|
458
|
+
task_record_id: Task record ID
|
|
459
|
+
start: Start timestamp
|
|
460
|
+
end: End timestamp
|
|
461
|
+
page: Page number
|
|
462
|
+
page_size: Items per page
|
|
463
|
+
levels: Log level filter
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Log data
|
|
467
|
+
"""
|
|
468
|
+
if start is None:
|
|
469
|
+
start = int(time.time()*1000)-1000
|
|
470
|
+
|
|
471
|
+
if end is None:
|
|
472
|
+
end = start + 2000
|
|
473
|
+
|
|
474
|
+
if levels is None:
|
|
475
|
+
levels = [LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]
|
|
476
|
+
|
|
477
|
+
resp = self.client._request(
|
|
478
|
+
"POST",
|
|
479
|
+
"/api/MonitoringLogs/query",
|
|
480
|
+
json={
|
|
481
|
+
"taskId": task_id,
|
|
482
|
+
"taskRecordId": task_record_id,
|
|
483
|
+
"start": start,
|
|
484
|
+
"end": end,
|
|
485
|
+
"page": page,
|
|
486
|
+
"pageSize": page_size,
|
|
487
|
+
"order": "asc",
|
|
488
|
+
"levels": [str(level) for level in levels],
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return [TaskLog.from_dict(item) for item in resp["data"]["items"]]
|
tapdata_sdk/enums.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Enumeration type definitions"""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConnectionType(str, Enum):
|
|
6
|
+
"""Connection type"""
|
|
7
|
+
SOURCE = "source"
|
|
8
|
+
TARGET = "target"
|
|
9
|
+
|
|
10
|
+
def __str__(self):
|
|
11
|
+
return self.value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseType(str, Enum):
|
|
15
|
+
"""Database type"""
|
|
16
|
+
MYSQL = "Mysql"
|
|
17
|
+
CLICKHOUSE = "Clickhouse"
|
|
18
|
+
MONGODB = "MongoDB"
|
|
19
|
+
POSTGRESQL = "PostgreSQL"
|
|
20
|
+
ORACLE = "Oracle"
|
|
21
|
+
SQLSERVER = "SQLServer"
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return self.value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Status(str, Enum):
|
|
28
|
+
"""Status"""
|
|
29
|
+
EDIT = "edit"
|
|
30
|
+
WAIT_START = "wait_start"
|
|
31
|
+
WAIT_RUN = "wait_run"
|
|
32
|
+
RUNNING = "running"
|
|
33
|
+
COMPLETE = "complete"
|
|
34
|
+
STOPPING = "stopping"
|
|
35
|
+
STOP = "stop"
|
|
36
|
+
ERROR = "error"
|
|
37
|
+
RENEWING = "renewing"
|
|
38
|
+
RENEW_FAILED = "renew_failed"
|
|
39
|
+
DELETING = "deleting"
|
|
40
|
+
DELETE_FAILED = "delete_failed"
|
|
41
|
+
DELETED = "deleted"
|
|
42
|
+
VALID = "ready"
|
|
43
|
+
INVALID = "invalid"
|
|
44
|
+
TESTING = "testing"
|
|
45
|
+
|
|
46
|
+
def __str__(self):
|
|
47
|
+
return self.value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LogLevel(str, Enum):
|
|
51
|
+
"""Log level"""
|
|
52
|
+
INFO = "INFO"
|
|
53
|
+
WARN = "WARN"
|
|
54
|
+
ERROR = "ERROR"
|
|
55
|
+
DEBUG = "DEBUG"
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return self.value
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Exception definitions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TapdataError(Exception):
|
|
5
|
+
"""Tapdata API error base class"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, resp: dict):
|
|
8
|
+
self.resp = resp
|
|
9
|
+
self.code = resp.get("code", "unknown")
|
|
10
|
+
self.message = resp.get("message", str(resp))
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TapdataAuthError(TapdataError):
|
|
15
|
+
"""Authentication error"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TapdataConnectionError(TapdataError):
|
|
20
|
+
"""Connection error"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TapdataValidationError(TapdataError):
|
|
25
|
+
"""Validation error"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TapdataTimeoutError(TapdataError):
|
|
30
|
+
"""Timeout error"""
|
|
31
|
+
pass
|
tapdata_sdk/models.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Data model definitions"""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Connection:
|
|
8
|
+
"""Connection model"""
|
|
9
|
+
id: str
|
|
10
|
+
name: str
|
|
11
|
+
connection_type: str
|
|
12
|
+
database_type: Optional[str]
|
|
13
|
+
status: str
|
|
14
|
+
endpoint: Optional[str]
|
|
15
|
+
port: Optional[str]
|
|
16
|
+
database: Optional[str]
|
|
17
|
+
user: Optional[str]
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, data: dict) -> "Connection":
|
|
21
|
+
"""Create connection object from API response"""
|
|
22
|
+
config = data.get("config", {})
|
|
23
|
+
uri = config.get("uri")
|
|
24
|
+
host = config.get("host")
|
|
25
|
+
|
|
26
|
+
return cls(
|
|
27
|
+
id=data["id"],
|
|
28
|
+
name=data["name"],
|
|
29
|
+
connection_type=data["connection_type"],
|
|
30
|
+
database_type=data.get("database_type"),
|
|
31
|
+
status=data["status"],
|
|
32
|
+
endpoint=uri or host,
|
|
33
|
+
user=config.get("user",""),
|
|
34
|
+
database=config.get("database",""),
|
|
35
|
+
port=config.get("port","")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
"""Convert to dictionary"""
|
|
40
|
+
return {
|
|
41
|
+
"id": self.id,
|
|
42
|
+
"name": self.name,
|
|
43
|
+
"connection_type": self.connection_type,
|
|
44
|
+
"database_type": self.database_type,
|
|
45
|
+
"status": self.status,
|
|
46
|
+
"endpoint": self.endpoint,
|
|
47
|
+
"port": self.port,
|
|
48
|
+
"database": self.database,
|
|
49
|
+
"user": self.user
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Task:
|
|
55
|
+
"""Task model"""
|
|
56
|
+
id: str
|
|
57
|
+
name: str
|
|
58
|
+
type: str
|
|
59
|
+
status: str
|
|
60
|
+
task_record_id: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, data: dict) -> "Task":
|
|
64
|
+
"""Create task object from API response"""
|
|
65
|
+
return cls(
|
|
66
|
+
id=data["id"],
|
|
67
|
+
name=data["name"],
|
|
68
|
+
type=data["type"],
|
|
69
|
+
status=data["status"],
|
|
70
|
+
task_record_id=data.get("taskRecordId"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict:
|
|
74
|
+
"""Convert to dictionary"""
|
|
75
|
+
return {
|
|
76
|
+
"id": self.id,
|
|
77
|
+
"name": self.name,
|
|
78
|
+
"type": self.type,
|
|
79
|
+
"status": self.status,
|
|
80
|
+
"taskRecordId": self.task_record_id,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class TaskDetail:
|
|
85
|
+
"""Task detail model"""
|
|
86
|
+
id: str
|
|
87
|
+
name: str
|
|
88
|
+
type: str
|
|
89
|
+
status: str
|
|
90
|
+
task_record_id: str
|
|
91
|
+
nodes: List[dict]
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: dict) -> "TaskDetail":
|
|
95
|
+
"""Create task object from API response"""
|
|
96
|
+
nodes = []
|
|
97
|
+
for node in data.get("dag",{}).get("nodes",[]):
|
|
98
|
+
attrs = node.get("attrs",{})
|
|
99
|
+
nodes.append({
|
|
100
|
+
"id": node.get("id"),
|
|
101
|
+
"name": node.get("name"),
|
|
102
|
+
"connectionId": node.get("connectionId"),
|
|
103
|
+
"connectionName": attrs.get("connectionName"),
|
|
104
|
+
"connectionType": attrs.get("__connectionType"),
|
|
105
|
+
"syncObjects": node.get("syncObjects",[])
|
|
106
|
+
})
|
|
107
|
+
return cls(
|
|
108
|
+
id=data["id"],
|
|
109
|
+
name=data["name"],
|
|
110
|
+
type=data["type"],
|
|
111
|
+
status=data["status"],
|
|
112
|
+
task_record_id=data.get("taskRecordId"),
|
|
113
|
+
nodes=nodes
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict:
|
|
117
|
+
"""Convert to dictionary"""
|
|
118
|
+
return {
|
|
119
|
+
"id": self.id,
|
|
120
|
+
"name": self.name,
|
|
121
|
+
"type": self.type,
|
|
122
|
+
"status": self.status,
|
|
123
|
+
"taskRecordId": self.task_record_id,
|
|
124
|
+
"nodes": self.nodes
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class TaskLog:
|
|
130
|
+
"""Task log"""
|
|
131
|
+
task_id: str
|
|
132
|
+
task_record_id: str
|
|
133
|
+
task_name: str
|
|
134
|
+
node_id: str
|
|
135
|
+
node_name: str
|
|
136
|
+
level: str
|
|
137
|
+
message: str
|
|
138
|
+
timestamp: int
|
|
139
|
+
date: str
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def from_dict(cls, data: dict) -> "TaskLog":
|
|
143
|
+
"""Create log object from API response"""
|
|
144
|
+
return cls(
|
|
145
|
+
task_id=data["taskId"],
|
|
146
|
+
task_record_id=data["taskRecordId"],
|
|
147
|
+
task_name=data["taskName"],
|
|
148
|
+
node_name=data.get("nodeName",""),
|
|
149
|
+
node_id=data.get("nodeId",""),
|
|
150
|
+
level=data["level"],
|
|
151
|
+
message=data["message"],
|
|
152
|
+
timestamp=data["timestamp"],
|
|
153
|
+
date=data["date"],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def to_dict(self) -> dict:
|
|
157
|
+
"""Convert to dictionary"""
|
|
158
|
+
return {
|
|
159
|
+
"task_id": self.task_id,
|
|
160
|
+
"task_record_id": self.task_record_id,
|
|
161
|
+
"task_name": self.task_name,
|
|
162
|
+
"node_id": self.node_id,
|
|
163
|
+
"node_name": self.node_name,
|
|
164
|
+
"level": self.level,
|
|
165
|
+
"message": self.message,
|
|
166
|
+
"timestamp": self.timestap,
|
|
167
|
+
"date": self.date
|
|
168
|
+
}
|
tapdata_sdk/utils.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Utility functions"""
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from Crypto.Cipher import ARC4
|
|
8
|
+
from Crypto.Cipher import ARC4
|
|
9
|
+
from Crypto.Hash import MD5
|
|
10
|
+
from Crypto import Random
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"pycryptodome is required. Install it with: pip install pycryptodome"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def encrypt_rc4_cryptojs(plaintext, password):
|
|
18
|
+
"""
|
|
19
|
+
Simulate CryptoJS.RC4.encrypt behavior
|
|
20
|
+
Supports OpenSSL-compatible Salted__ format
|
|
21
|
+
"""
|
|
22
|
+
# 1. Generate random 8-byte salt
|
|
23
|
+
salt = Random.get_random_bytes(8)
|
|
24
|
+
|
|
25
|
+
# 2. Simulate OpenSSL's key derivation logic (EVP_BytesToKey)
|
|
26
|
+
password_bytes = password.encode('utf-8')
|
|
27
|
+
derived_bytes = b""
|
|
28
|
+
last_hash = b""
|
|
29
|
+
while len(derived_bytes) < 32:
|
|
30
|
+
last_hash = MD5.new(last_hash + password_bytes + salt).digest()
|
|
31
|
+
derived_bytes += last_hash
|
|
32
|
+
key = derived_bytes[:32]
|
|
33
|
+
|
|
34
|
+
# 3. Perform RC4 encryption
|
|
35
|
+
cipher = ARC4.new(key)
|
|
36
|
+
ciphertext = cipher.encrypt(plaintext.encode('utf-8'))
|
|
37
|
+
|
|
38
|
+
# 4. Concatenate format: "Salted__" + salt + ciphertext and convert to Base64
|
|
39
|
+
final_payload = b'Salted__' + salt + ciphertext
|
|
40
|
+
return base64.b64encode(final_payload).decode('utf-8')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def rc4_encrypt(text: str, key: str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Encrypt text using RC4 algorithm
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
text: Text to encrypt
|
|
49
|
+
key: Encryption key
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Base64 encoded encrypted result
|
|
53
|
+
"""
|
|
54
|
+
return encrypt_rc4_cryptojs(text, key)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def gen_sign(email: str, password: str, stime: int, key: str) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Generate signature
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
email: Email
|
|
63
|
+
password: Encrypted password
|
|
64
|
+
stime: Timestamp
|
|
65
|
+
key: Signature key
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
SHA1 signature (uppercase)
|
|
69
|
+
"""
|
|
70
|
+
raw = f"{email}{password}{stime}{key}"
|
|
71
|
+
return hashlib.sha1(raw.encode()).hexdigest().upper()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_filter(
|
|
75
|
+
skip: int = 0,
|
|
76
|
+
limit: int = 20,
|
|
77
|
+
where: Optional[dict] = None,
|
|
78
|
+
fields: Optional[dict] = None,
|
|
79
|
+
order: Optional[str] = None,
|
|
80
|
+
noSchema: Optional[int] = 1,
|
|
81
|
+
) -> dict:
|
|
82
|
+
"""
|
|
83
|
+
Build query filter
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
skip: Number of records to skip
|
|
87
|
+
limit: Limit on number of results
|
|
88
|
+
where: Query conditions
|
|
89
|
+
fields: Field filter
|
|
90
|
+
order: Sort order
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Filter dictionary
|
|
94
|
+
"""
|
|
95
|
+
filter_dict = {
|
|
96
|
+
"skip": skip,
|
|
97
|
+
"limit": limit,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if where:
|
|
101
|
+
filter_dict["where"] = where
|
|
102
|
+
|
|
103
|
+
if fields:
|
|
104
|
+
filter_dict["fields"] = fields
|
|
105
|
+
|
|
106
|
+
if order:
|
|
107
|
+
filter_dict["order"] = order
|
|
108
|
+
filter_dict["noSchema"] = 1
|
|
109
|
+
|
|
110
|
+
return filter_dict
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tapdata_sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python client library for interacting with Tapdata API
|
|
5
|
+
Author-email: doubled <309294891@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lddlww/tapdata_sdk
|
|
8
|
+
Project-URL: Documentation, https://github.com/lddlww/tapdata_sdk#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/lddlww/tapdata_sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/lddlww/tapdata_sdk/issues
|
|
11
|
+
Keywords: tapdata,api,client,sdk,data-integration
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: requests>=2.25.0
|
|
27
|
+
Requires-Dist: pycryptodome>=3.10.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
33
|
+
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=0.950; extra == "dev"
|
|
35
|
+
Requires-Dist: types-requests>=2.25.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Tapdata Python SDK
|
|
39
|
+
|
|
40
|
+
A Python client library for interacting with Tapdata API.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- 🔐 Complete authentication support
|
|
45
|
+
- 📦 Type-safe data models
|
|
46
|
+
- 🎯 Clean API interface
|
|
47
|
+
- 🔄 Connection and task management
|
|
48
|
+
- 📊 Task log queries
|
|
49
|
+
- ⚠️ Detailed error handling
|
|
50
|
+
- 📝 Comprehensive documentation and type hints
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install tapdata_sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or install from source:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/lddlww/tapdata_sdk.git
|
|
62
|
+
cd tapdata-sdk
|
|
63
|
+
pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
### Basic Usage
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from tapdata_sdk import TapdataClient
|
|
72
|
+
|
|
73
|
+
# Initialize client
|
|
74
|
+
client = TapdataClient("http://localhost:3030")
|
|
75
|
+
|
|
76
|
+
# Login
|
|
77
|
+
client.login("admin@test.com", "password")
|
|
78
|
+
|
|
79
|
+
# Query connections
|
|
80
|
+
connections = client.connections.list()
|
|
81
|
+
for conn in connections:
|
|
82
|
+
print(f"{conn.name}: {conn.status}")
|
|
83
|
+
|
|
84
|
+
# Query tasks
|
|
85
|
+
tasks = client.tasks.list()
|
|
86
|
+
for task in tasks:
|
|
87
|
+
print(f"{task.name}: {task.status}")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Connection Management
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from tapdata_sdk import ConnectionType, DatabaseType, Status
|
|
94
|
+
|
|
95
|
+
# Query source connections
|
|
96
|
+
source_connections = client.connections.list_source()
|
|
97
|
+
|
|
98
|
+
# Query MySQL connections
|
|
99
|
+
mysql_connections = client.connections.list_mysql()
|
|
100
|
+
|
|
101
|
+
# Query valid connections
|
|
102
|
+
valid_connections = client.connections.list_valid()
|
|
103
|
+
|
|
104
|
+
# Filter using enum types
|
|
105
|
+
connections = client.connections.list(
|
|
106
|
+
connection_type=ConnectionType.SOURCE,
|
|
107
|
+
database_type=DatabaseType.MYSQL,
|
|
108
|
+
status=Status.COMPLETE
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Get single connection details
|
|
112
|
+
connection = client.connections.get("connection_id")
|
|
113
|
+
print(connection.endpoint)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Task Management
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# Query running tasks
|
|
120
|
+
running_tasks = client.tasks.list_running()
|
|
121
|
+
|
|
122
|
+
# Query tasks with specific status
|
|
123
|
+
tasks = client.tasks.list(status=Status.RUNNING)
|
|
124
|
+
|
|
125
|
+
# Get task details
|
|
126
|
+
task = client.tasks.get("task_id")
|
|
127
|
+
|
|
128
|
+
# Start task
|
|
129
|
+
client.tasks.start("task_id")
|
|
130
|
+
|
|
131
|
+
# Stop task
|
|
132
|
+
client.tasks.stop("task_id")
|
|
133
|
+
|
|
134
|
+
# Reset task
|
|
135
|
+
client.tasks.reset("task_id")
|
|
136
|
+
|
|
137
|
+
# Delete task
|
|
138
|
+
client.tasks.delete("task_id")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Query Task Logs
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import time
|
|
145
|
+
|
|
146
|
+
# Get logs from the last hour
|
|
147
|
+
end_time = int(time.time() * 1000)
|
|
148
|
+
start_time = end_time - 3600000 # One hour ago
|
|
149
|
+
|
|
150
|
+
logs = client.tasks.get_logs(
|
|
151
|
+
task_id="task_id",
|
|
152
|
+
task_record_id="record_id",
|
|
153
|
+
start=start_time,
|
|
154
|
+
end=end_time,
|
|
155
|
+
page=1,
|
|
156
|
+
page_size=20
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Error Handling
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from tapdata_sdk import (
|
|
164
|
+
TapdataError,
|
|
165
|
+
TapdataAuthError,
|
|
166
|
+
TapdataTimeoutError,
|
|
167
|
+
TapdataConnectionError
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
client.login("admin@test.com", "wrong_password")
|
|
172
|
+
except TapdataAuthError as e:
|
|
173
|
+
print(f"Authentication failed: {e.message}")
|
|
174
|
+
except TapdataTimeoutError as e:
|
|
175
|
+
print(f"Request timeout: {e.message}")
|
|
176
|
+
except TapdataConnectionError as e:
|
|
177
|
+
print(f"Connection error: {e.message}")
|
|
178
|
+
except TapdataError as e:
|
|
179
|
+
print(f"API error: {e.message}")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Advanced Configuration
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Custom timeout and SSL verification
|
|
186
|
+
client = TapdataClient(
|
|
187
|
+
base_url="https://api.tapdata.io",
|
|
188
|
+
timeout=60, # 60 second timeout
|
|
189
|
+
verify_ssl=False # Disable SSL verification (not recommended in production)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Use existing access_token
|
|
193
|
+
client = TapdataClient(
|
|
194
|
+
base_url="http://localhost:3030",
|
|
195
|
+
access_token="your-existing-token"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Check authentication status
|
|
199
|
+
if client.is_authenticated():
|
|
200
|
+
print("Authenticated")
|
|
201
|
+
|
|
202
|
+
# Logout
|
|
203
|
+
client.logout()
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## API Reference
|
|
207
|
+
|
|
208
|
+
### TapdataClient
|
|
209
|
+
|
|
210
|
+
Main client class providing authentication and sub-client access.
|
|
211
|
+
|
|
212
|
+
**Parameters:**
|
|
213
|
+
- `base_url` (str): API base URL
|
|
214
|
+
- `access_token` (str, optional): Access token
|
|
215
|
+
- `timeout` (int): Request timeout in seconds, default 30
|
|
216
|
+
- `verify_ssl` (bool): Whether to verify SSL certificate, default True
|
|
217
|
+
|
|
218
|
+
**Methods:**
|
|
219
|
+
- `login(email, password, secret)`: User login
|
|
220
|
+
- `logout()`: Logout
|
|
221
|
+
- `is_authenticated()`: Check if authenticated
|
|
222
|
+
- `get_timestamp()`: Get server timestamp
|
|
223
|
+
|
|
224
|
+
**Properties:**
|
|
225
|
+
- `connections`: ConnectionClient instance
|
|
226
|
+
- `tasks`: TaskClient instance
|
|
227
|
+
|
|
228
|
+
### ConnectionClient
|
|
229
|
+
|
|
230
|
+
Connection management client.
|
|
231
|
+
|
|
232
|
+
**Methods:**
|
|
233
|
+
- `list(connection_type, database_type, status, skip, limit)`: Query connection list
|
|
234
|
+
- `get(connection_id)`: Get single connection
|
|
235
|
+
- `list_source()`: Get all source connections
|
|
236
|
+
- `list_target()`: Get all target connections
|
|
237
|
+
- `list_mysql()`: Get all MySQL connections
|
|
238
|
+
- `list_clickhouse()`: Get all ClickHouse connections
|
|
239
|
+
- `list_mongodb()`: Get all MongoDB connections
|
|
240
|
+
- `list_valid()`: Get all valid connections
|
|
241
|
+
- `list_invalid()`: Get all invalid connections
|
|
242
|
+
|
|
243
|
+
### TaskClient
|
|
244
|
+
|
|
245
|
+
Task management client.
|
|
246
|
+
|
|
247
|
+
**Methods:**
|
|
248
|
+
- `list(status, skip, limit)`: Query task list
|
|
249
|
+
- `get(task_id)`: Get single task
|
|
250
|
+
- `list_running()`: Get all running tasks
|
|
251
|
+
- `start(task_id)`: Start task
|
|
252
|
+
- `stop(task_id)`: Stop task
|
|
253
|
+
- `reset(task_id)`: Reset task
|
|
254
|
+
- `delete(task_id)`: Delete task
|
|
255
|
+
- `get_logs(task_id, task_record_id, start, end, page, page_size, levels)`: Get task logs
|
|
256
|
+
|
|
257
|
+
### Enum Types
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from tapdata_sdk import ConnectionType, DatabaseType, Status, LogLevel
|
|
261
|
+
|
|
262
|
+
# Connection types
|
|
263
|
+
ConnectionType.SOURCE
|
|
264
|
+
ConnectionType.TARGET
|
|
265
|
+
|
|
266
|
+
# Database types
|
|
267
|
+
DatabaseType.MYSQL
|
|
268
|
+
DatabaseType.CLICKHOUSE
|
|
269
|
+
DatabaseType.MONGODB
|
|
270
|
+
DatabaseType.POSTGRESQL
|
|
271
|
+
DatabaseType.ORACLE
|
|
272
|
+
DatabaseType.SQLSERVER
|
|
273
|
+
|
|
274
|
+
# Status
|
|
275
|
+
Status.RUNNING
|
|
276
|
+
Status.COMPLETE
|
|
277
|
+
Status.ERROR
|
|
278
|
+
# ... more statuses
|
|
279
|
+
|
|
280
|
+
# Log levels
|
|
281
|
+
LogLevel.INFO
|
|
282
|
+
LogLevel.WARN
|
|
283
|
+
LogLevel.ERROR
|
|
284
|
+
LogLevel.DEBUG
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Development
|
|
288
|
+
|
|
289
|
+
### Setup Development Environment
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Clone repository
|
|
293
|
+
git clone <repository-url>
|
|
294
|
+
cd tapdata-sdk
|
|
295
|
+
|
|
296
|
+
# Create virtual environment
|
|
297
|
+
python -m venv venv
|
|
298
|
+
source venv/bin/activate # Linux/Mac
|
|
299
|
+
# or
|
|
300
|
+
venv\Scripts\activate # Windows
|
|
301
|
+
|
|
302
|
+
# Install dependencies
|
|
303
|
+
pip install -e ".[dev]"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Run Tests
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
pytest tests/
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Code Formatting
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
black tapdata_sdk/
|
|
316
|
+
isort tapdata_sdk/
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Changelog
|
|
320
|
+
|
|
321
|
+
### v0.2.0 (2024-01-29)
|
|
322
|
+
- ✨ Refactored code architecture with modular design
|
|
323
|
+
- 📦 Added data model classes (Connection, Task, TaskLog)
|
|
324
|
+
- 🎯 Improved enum types using Python Enum
|
|
325
|
+
- 🔧 Optimized error handling with multiple exception types
|
|
326
|
+
- 📝 Enhanced documentation and type hints
|
|
327
|
+
- 🏗️ Separated client responsibilities (ConnectionClient, TaskClient)
|
|
328
|
+
- 🔐 Improved authentication flow
|
|
329
|
+
- 📊 Optimized logging
|
|
330
|
+
|
|
331
|
+
### v0.1.0
|
|
332
|
+
- 🎉 Initial release
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
MIT License
|
|
337
|
+
|
|
338
|
+
## Contributing
|
|
339
|
+
|
|
340
|
+
Issues and Pull Requests are welcome!
|
|
341
|
+
|
|
342
|
+
## Support
|
|
343
|
+
|
|
344
|
+
For questions, please submit an Issue or contact the maintainers.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
tapdata_sdk/__init__.py,sha256=DCbgmv3TOkTHR69LFEJ3Y9Oxm9WfUBQUhyqUMyySSVk,1332
|
|
2
|
+
tapdata_sdk/client.py,sha256=yx6W2iVnE_3b8ybguRevr3Oid2dUs73XPLZyu-KVyjw,13888
|
|
3
|
+
tapdata_sdk/enums.py,sha256=ywCPyswMdbISvuII9Lv7QSmwHUbfqsQB3mK_UPV7C9A,1160
|
|
4
|
+
tapdata_sdk/exceptions.py,sha256=Edm1P6Aghb7wJhUOubYU9JuxcQrfI_WGn3wnX4ukH7Y,627
|
|
5
|
+
tapdata_sdk/models.py,sha256=elJxe-6IzSCOpSLCQroUGec4dBgjyf2PU7LHo9O_yQ8,4601
|
|
6
|
+
tapdata_sdk/utils.py,sha256=ojPuW9YwW7dkb0nLJ46_IgMbOzor7DtppYDBuN4nCiY,2682
|
|
7
|
+
tapdata_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=g3faROod99RM6X9tAOfJ_7LsPxI1sOISYpIy2w9VexQ,1064
|
|
8
|
+
tapdata_sdk-0.1.0.dist-info/METADATA,sha256=d477QW0uSb5KbHfEO99E_02ZaVTPak9M0dp-JDhDR6k,7933
|
|
9
|
+
tapdata_sdk-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
+
tapdata_sdk-0.1.0.dist-info/top_level.txt,sha256=YNIwm3uNhULMB32qsLcj-zAoL5yYMKWJbXnstNsIYNk,12
|
|
11
|
+
tapdata_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 doubled
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tapdata_sdk
|