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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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