pingdartdb 1.0.0__tar.gz

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,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: pingdartdb
3
+ Version: 1.0.0
4
+ Summary: PingDart Direct Database SDK for Python
5
+ Home-page: https://github.com/pingdart/pingdart
6
+ Author: PingDart
7
+ Author-email: support@pingdart.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: pymysql>=1.0.0
14
+ Requires-Dist: cryptography>=3.4.0
15
+ Requires-Dist: requests>=2.25.0
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # PingDartDB Python SDK
27
+
28
+ The official direct database driver for PingDart in Python.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install pingdartdb
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from pingdartdb import PingDartDB
40
+
41
+ db = PingDartDB("pd_your_license_key_here", {
42
+ "host": "localhost",
43
+ "user": "root",
44
+ "password": "password",
45
+ "database": "pingdart_test",
46
+ "type": "mysql"
47
+ })
48
+
49
+ db.connect()
50
+
51
+ # Read data
52
+ result = db.table('users').read({'conditions': {'status': 'active'}})
53
+ print(result)
54
+
55
+ db.close()
56
+ ```
@@ -0,0 +1,31 @@
1
+ # PingDartDB Python SDK
2
+
3
+ The official direct database driver for PingDart in Python.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pingdartdb
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from pingdartdb import PingDartDB
15
+
16
+ db = PingDartDB("pd_your_license_key_here", {
17
+ "host": "localhost",
18
+ "user": "root",
19
+ "password": "password",
20
+ "database": "pingdart_test",
21
+ "type": "mysql"
22
+ })
23
+
24
+ db.connect()
25
+
26
+ # Read data
27
+ result = db.table('users').read({'conditions': {'status': 'active'}})
28
+ print(result)
29
+
30
+ db.close()
31
+ ```
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ import setuptools
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ required = [
7
+ "pymysql>=1.0.0",
8
+ "cryptography>=3.4.0",
9
+ "requests>=2.25.0"
10
+ ]
11
+
12
+ setuptools.setup(
13
+ name="pingdartdb",
14
+ version="1.0.0",
15
+ author="PingDart",
16
+ author_email="support@pingdart.com",
17
+ description="PingDart Direct Database SDK for Python",
18
+ long_description=long_description,
19
+ long_description_content_type="text/markdown",
20
+ url="https://github.com/pingdart/pingdart",
21
+ package_dir={"": "src"},
22
+ packages=setuptools.find_packages(where="src"),
23
+ classifiers=[
24
+ "Programming Language :: Python :: 3",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ ],
28
+ python_requires=">=3.7",
29
+ install_requires=required,
30
+ )
@@ -0,0 +1,4 @@
1
+ from .client import PingDartDB
2
+ from .query_builder import QueryBuilder
3
+
4
+ __all__ = ['PingDartDB', 'QueryBuilder']
@@ -0,0 +1,109 @@
1
+ import json
2
+ import time
3
+ import hashlib
4
+ import binascii
5
+ import requests
6
+ import pymysql
7
+ import pymysql.cursors
8
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
9
+ from cryptography.hazmat.backends import default_backend
10
+
11
+ from .query_builder import QueryBuilder
12
+
13
+ class PingDartDB:
14
+ def __init__(self, api_key: str, db_config: dict):
15
+ self.api_key = api_key
16
+ self.db_config = db_config
17
+ self.db_type = db_config.get('type', 'mysql')
18
+ self.license = None
19
+ self.connection = None
20
+
21
+ if self.db_type not in ['mysql', 'postgresql']:
22
+ raise Exception(f"Unsupported database type: '{self.db_type}'. Use 'mysql' or 'postgresql'.")
23
+
24
+ self._validate_key(api_key)
25
+
26
+ def _validate_key(self, api_key: str):
27
+ if not api_key or not api_key.startswith('pd_'):
28
+ raise Exception('PingDart Authorization Failed: Invalid PingDart License Key format.')
29
+
30
+ key_body = api_key.replace('pd_', '')
31
+ parts = key_body.split('.')
32
+
33
+ if len(parts) != 2:
34
+ raise Exception('PingDart Authorization Failed: Invalid PingDart License Key format.')
35
+
36
+ try:
37
+ iv = binascii.unhexlify(parts[0])
38
+ encrypted_text = binascii.unhexlify(parts[1])
39
+
40
+ secret_key = 'PingDartSuperSecretKey2026!@#$'.encode('utf-8')
41
+ key = binascii.b2a_base64(hashlib.sha256(secret_key).digest(), newline=False)[:32]
42
+
43
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
44
+ decryptor = cipher.decryptor()
45
+
46
+ decrypted_padded = decryptor.update(encrypted_text) + decryptor.finalize()
47
+
48
+ # Remove PKCS7 padding
49
+ padding_len = decrypted_padded[-1]
50
+ decrypted = decrypted_padded[:-padding_len]
51
+
52
+ payload = json.loads(decrypted.decode('utf-8'))
53
+
54
+ if 'expiresAt' in payload:
55
+ # ISO to unix timestamp logic could go here; simplified for now
56
+ pass
57
+
58
+ self.license = payload
59
+
60
+ except Exception as e:
61
+ if 'PingDart Authorization Failed' in str(e):
62
+ raise e
63
+ raise Exception(f"PingDart Authorization Failed: License key is corrupted or tampered with. {e}")
64
+
65
+ def connect(self):
66
+ try:
67
+ if self.db_type == 'mysql':
68
+ self.connection = pymysql.connect(
69
+ host=self.db_config['host'],
70
+ user=self.db_config['user'],
71
+ password=self.db_config['password'],
72
+ database=self.db_config['database'],
73
+ cursorclass=pymysql.cursors.DictCursor,
74
+ autocommit=True
75
+ )
76
+ else:
77
+ raise Exception("PostgreSQL support requires psycopg2.")
78
+
79
+ except Exception as e:
80
+ raise Exception(f"Database connection failed: {e}")
81
+
82
+ if self.license and self.license.get('tier') != 'free':
83
+ self._validate_live_server()
84
+
85
+ return True
86
+
87
+ def _validate_live_server(self):
88
+ try:
89
+ resp = requests.post(
90
+ 'https://cloudapi.pingdart.com/api/realtime/validate-sdk',
91
+ json={'apiKey': self.api_key},
92
+ timeout=10
93
+ )
94
+ result = resp.json()
95
+ if not result.get('success'):
96
+ raise Exception(f"PingDart Live Authorization Failed: {result.get('message', 'Invalid response')}")
97
+ except Exception as e:
98
+ raise Exception(f"PingDart Live Authorization Failed: {e}")
99
+
100
+ def table(self, table_name: str):
101
+ return QueryBuilder(table_name, self.connection, self.db_type)
102
+
103
+ def schema(self):
104
+ from .schema_builder import SchemaBuilder
105
+ return SchemaBuilder(self.connection, self.db_type)
106
+
107
+ def close(self):
108
+ if self.connection:
109
+ self.connection.close()
@@ -0,0 +1,392 @@
1
+ import math
2
+
3
+ class QueryBuilder:
4
+ def __init__(self, table_name: str, connection, db_type: str):
5
+ self.table_name = table_name
6
+ self.connection = connection
7
+ self.db_type = db_type
8
+ self.q = '"' if db_type in ['postgresql', 'pgsql'] else '`'
9
+
10
+ def _build_where_clause(self, conditions: dict):
11
+ clauses = []
12
+ replacements = []
13
+
14
+ if not conditions or not isinstance(conditions, dict):
15
+ return clauses, replacements
16
+
17
+ for key, value in conditions.items():
18
+ if isinstance(value, dict):
19
+ # BETWEEN operator
20
+ if 'min' in value and 'max' in value:
21
+ clauses.append(f"{self.q}{key}{self.q} BETWEEN %s AND %s")
22
+ replacements.extend([value['min'], value['max']])
23
+ elif 'start' in value and 'end' in value:
24
+ clauses.append(f"{self.q}{key}{self.q} BETWEEN %s AND %s")
25
+ replacements.extend([value['start'], value['end']])
26
+ elif isinstance(value, list):
27
+ # IN clause
28
+ if len(value) > 0:
29
+ placeholders = ', '.join(['%s'] * len(value))
30
+ clauses.append(f"{self.q}{key}{self.q} IN ({placeholders})")
31
+ replacements.extend(value)
32
+ else:
33
+ clauses.append("1=0")
34
+ elif isinstance(value, str):
35
+ if value.startswith('!'):
36
+ clauses.append(f"{self.q}{key}{self.q} != %s")
37
+ replacements.append(value[1:])
38
+ elif value.startswith('>=') or value.startswith('=>'):
39
+ clauses.append(f"{self.q}{key}{self.q} >= %s")
40
+ replacements.append(value[2:])
41
+ elif value.startswith('<=') or value.startswith('=<'):
42
+ clauses.append(f"{self.q}{key}{self.q} <= %s")
43
+ replacements.append(value[2:])
44
+ elif value.startswith('>'):
45
+ clauses.append(f"{self.q}{key}{self.q} > %s")
46
+ replacements.append(value[1:])
47
+ elif value.startswith('<'):
48
+ clauses.append(f"{self.q}{key}{self.q} < %s")
49
+ replacements.append(value[1:])
50
+ else:
51
+ clauses.append(f"{self.q}{key}{self.q} = %s")
52
+ replacements.append(value)
53
+ else:
54
+ clauses.append(f"{self.q}{key}{self.q} = %s")
55
+ replacements.append(value)
56
+
57
+ return clauses, replacements
58
+
59
+ def _process_margedata_batch(self, rows: list, margedata_item: dict, search: dict):
60
+ target_table = margedata_item.get('target_table')
61
+ target_column = margedata_item.get('target_column')
62
+ target_value = margedata_item.get('target_value')
63
+ target_label = margedata_item.get('target_label')
64
+ search_fields = margedata_item.get('search_fields', [])
65
+ nested_margedata = margedata_item.get('margedata', [])
66
+ range_data = margedata_item.get('range')
67
+
68
+ if not all([target_table, target_column, target_value, target_label]):
69
+ return
70
+
71
+ parent_ids = list(set([row[target_value] for row in rows if target_value in row and row[target_value] is not None]))
72
+
73
+ if not parent_ids:
74
+ for row in rows:
75
+ row[target_label] = []
76
+ return
77
+
78
+ conditions = []
79
+ replacements = []
80
+
81
+ placeholders = ', '.join(['%s'] * len(parent_ids))
82
+ conditions.append(f"{self.q}{target_column}{self.q} IN ({placeholders})")
83
+ replacements.extend(parent_ids)
84
+
85
+ for field in search_fields:
86
+ for search_key, full_column in field.items():
87
+ parts = full_column.split('.')
88
+ column = parts[1] if len(parts) > 1 else parts[0]
89
+ if search and search_key in search and search[search_key]:
90
+ like_op = 'ILIKE' if self.db_type == 'postgresql' else 'LIKE'
91
+ conditions.append(f"{self.q}{column}{self.q} {like_op} %s")
92
+ replacements.append(f"%{search[search_key]}%")
93
+
94
+ if isinstance(range_data, dict) and all(k in range_data for k in ['latitude', 'longitude', 'radius', 'target_latitude', 'target_longitude']):
95
+ earth_radius = 6371
96
+ conditions.append(f"""
97
+ ({earth_radius} * ACOS(
98
+ COS(RADIANS(%s)) *
99
+ COS(RADIANS({self.q}{range_data['target_latitude']}{self.q})) *
100
+ COS(RADIANS({self.q}{range_data['target_longitude']}{self.q}) - RADIANS(%s)) +
101
+ SIN(RADIANS(%s)) *
102
+ SIN(RADIANS({self.q}{range_data['target_latitude']}{self.q}))
103
+ )) <= %s
104
+ """)
105
+ replacements.extend([
106
+ range_data['latitude'],
107
+ range_data['longitude'],
108
+ range_data['latitude'],
109
+ range_data['radius']
110
+ ])
111
+
112
+ query = f"SELECT * FROM {self.q}{target_table}{self.q} WHERE {' AND '.join(conditions)}"
113
+
114
+ try:
115
+ with self.connection.cursor() as cursor:
116
+ cursor.execute(query, replacements)
117
+ all_matches = cursor.fetchall()
118
+
119
+ grouped_matches = {}
120
+ for match in all_matches:
121
+ pid = match[target_column]
122
+ if pid not in grouped_matches:
123
+ grouped_matches[pid] = []
124
+ grouped_matches[pid].append(match)
125
+
126
+ for row in rows:
127
+ pid = row[target_value]
128
+ row[target_label] = grouped_matches.get(pid, [])
129
+
130
+ if nested_margedata and all_matches:
131
+ for child_item in nested_margedata:
132
+ self._process_margedata_batch(all_matches, child_item, search)
133
+
134
+ except Exception as e:
135
+ print(f"Error in batch margedata ({target_label}): {e}")
136
+ for row in rows:
137
+ row[target_label] = []
138
+
139
+ def read(self, options: dict = None):
140
+ if options is None:
141
+ options = {}
142
+
143
+ conditions = options.get('conditions', {})
144
+ search = options.get('search', {})
145
+ order_by = options.get('orderBy')
146
+ pagination = options.get('pagination')
147
+ range_data = options.get('range')
148
+ margedata = options.get('margedata', [])
149
+
150
+ query = f"SELECT * FROM {self.q}{self.table_name}{self.q} WHERE 1=1"
151
+ replacements = []
152
+
153
+ cond_clauses, cond_reps = self._build_where_clause(conditions)
154
+ if cond_clauses:
155
+ query += " AND " + " AND ".join(cond_clauses)
156
+ replacements.extend(cond_reps)
157
+
158
+ if isinstance(search, dict) and search:
159
+ search_clauses = []
160
+ for key, value in search.items():
161
+ if value:
162
+ like_op = 'ILIKE' if self.db_type == 'postgresql' else 'LIKE'
163
+ search_clauses.append(f"{self.q}{key}{self.q} {like_op} %s")
164
+ replacements.append(f"%{value}%")
165
+ if search_clauses:
166
+ query += " AND (" + " OR ".join(search_clauses) + ")"
167
+
168
+ if isinstance(range_data, dict) and all(k in range_data for k in ['latitude', 'longitude', 'radius', 'target_latitude', 'target_longitude']):
169
+ earth_radius = 6371
170
+ join_table = range_data.get('table_name')
171
+
172
+ if not join_table or join_table == self.table_name:
173
+ query += f""" AND (
174
+ {earth_radius} * ACOS(
175
+ COS(RADIANS(%s)) *
176
+ COS(RADIANS({self.q}{range_data['target_latitude']}{self.q})) *
177
+ COS(RADIANS({self.q}{range_data['target_longitude']}{self.q}) - RADIANS(%s)) +
178
+ SIN(RADIANS(%s)) *
179
+ SIN(RADIANS({self.q}{range_data['target_latitude']}{self.q}))
180
+ )
181
+ ) <= %s"""
182
+ replacements.extend([range_data['latitude'], range_data['longitude'], range_data['latitude'], range_data['radius']])
183
+ else:
184
+ alias = 'rt'
185
+ join_col = range_data.get('join_column', 'user_id')
186
+ main_col = range_data.get('main_join_column', 'id')
187
+ query += f""" AND EXISTS (
188
+ SELECT 1 FROM {self.q}{join_table}{self.q} AS {alias}
189
+ WHERE {alias}.{self.q}{join_col}{self.q} = {self.q}{self.table_name}{self.q}.{self.q}{main_col}{self.q}
190
+ AND (
191
+ {earth_radius} * ACOS(
192
+ COS(RADIANS(%s)) *
193
+ COS(RADIANS({alias}.{self.q}{range_data['target_latitude']}{self.q})) *
194
+ COS(RADIANS({alias}.{self.q}{range_data['target_longitude']}{self.q}) - RADIANS(%s)) +
195
+ SIN(RADIANS(%s)) *
196
+ SIN(RADIANS({alias}.{self.q}{range_data['target_latitude']}{self.q}))
197
+ )
198
+ ) <= %s
199
+ )"""
200
+ replacements.extend([range_data['latitude'], range_data['longitude'], range_data['latitude'], range_data['radius']])
201
+
202
+ if order_by:
203
+ query += f" ORDER BY {order_by}"
204
+
205
+ count_query = query.replace("SELECT *", "SELECT COUNT(*) as total", 1)
206
+
207
+ with self.connection.cursor() as cursor:
208
+ cursor.execute(count_query, replacements)
209
+ total_count = cursor.fetchone()['total']
210
+
211
+ total_pages = 0
212
+ if isinstance(pagination, dict) and 'page' in pagination and 'limit' in pagination:
213
+ page = max(1, int(pagination['page']))
214
+ limit = int(pagination['limit'])
215
+ offset = (page - 1) * limit
216
+ query += f" LIMIT {limit} OFFSET {offset}"
217
+ total_pages = math.ceil(total_count / limit)
218
+
219
+ with self.connection.cursor() as cursor:
220
+ cursor.execute(query, replacements)
221
+ results = cursor.fetchall()
222
+
223
+ if margedata and results:
224
+ for md_item in margedata:
225
+ self._process_margedata_batch(results, md_item, search)
226
+
227
+ return {
228
+ 'success': True,
229
+ 'data': results,
230
+ 'totalCount': total_count,
231
+ 'totalPages': total_pages
232
+ }
233
+
234
+ def insert(self, arg1, arg2=None):
235
+ data = None
236
+ conditions = None
237
+
238
+ if arg2 is not None:
239
+ data = arg2
240
+ if isinstance(arg1, dict) and 'conditions' in arg1:
241
+ conditions = arg1['conditions']
242
+ else:
243
+ data = arg1
244
+
245
+ if not data:
246
+ raise Exception("Data object cannot be empty")
247
+
248
+ is_bulk = isinstance(data, list)
249
+ records = data if is_bulk else [data]
250
+
251
+ if not records:
252
+ raise Exception("Data array cannot be empty")
253
+
254
+ if isinstance(conditions, dict) and conditions:
255
+ if not is_bulk:
256
+ cond_clauses, check_reps = self._build_where_clause(conditions)
257
+ if cond_clauses:
258
+ check_query = f"SELECT COUNT(*) as total FROM {self.q}{self.table_name}{self.q} WHERE " + " AND ".join(cond_clauses)
259
+ with self.connection.cursor() as cursor:
260
+ cursor.execute(check_query, check_reps)
261
+ existing_count = cursor.fetchone()['total']
262
+ if existing_count > 0:
263
+ return {
264
+ 'success': False,
265
+ 'exists': True,
266
+ 'message': 'Record already exists matching the given conditions. Insert skipped.'
267
+ }
268
+ else:
269
+ condition_keys = list(conditions.keys())
270
+ to_insert = []
271
+ skipped = []
272
+
273
+ for record in records:
274
+ per_record_conditions = {}
275
+ for key in condition_keys:
276
+ per_record_conditions[key] = record.get(key, conditions[key])
277
+
278
+ cond_clauses, check_reps = self._build_where_clause(per_record_conditions)
279
+ if cond_clauses:
280
+ check_query = f"SELECT COUNT(*) as total FROM {self.q}{self.table_name}{self.q} WHERE " + " AND ".join(cond_clauses)
281
+ with self.connection.cursor() as cursor:
282
+ cursor.execute(check_query, check_reps)
283
+ if cursor.fetchone()['total'] > 0:
284
+ skipped.append(record)
285
+ else:
286
+ to_insert.append(record)
287
+ else:
288
+ to_insert.append(record)
289
+
290
+ if not to_insert:
291
+ return {
292
+ 'success': False,
293
+ 'exists': True,
294
+ 'message': f"All {len(records)} records already exist. Insert skipped.",
295
+ 'skipped': len(skipped)
296
+ }
297
+ records = to_insert # Swap for actual execution
298
+
299
+ keys = list(records[0].keys())
300
+ columns = ", ".join([f"{self.q}{k}{self.q}" for k in keys])
301
+
302
+ value_sets = []
303
+ replacements = []
304
+
305
+ for record in records:
306
+ placeholders = ", ".join(["%s"] * len(keys))
307
+ value_sets.append(f"({placeholders})")
308
+ replacements.extend([record.get(k) for k in keys])
309
+
310
+ query = f"INSERT INTO {self.q}{self.table_name}{self.q} ({columns}) VALUES " + ", ".join(value_sets)
311
+
312
+ with self.connection.cursor() as cursor:
313
+ cursor.execute(query, replacements)
314
+ inserted_rows = cursor.rowcount
315
+
316
+ msg = f"{len(records)} record(s) inserted. {len(records) - len(records)} duplicate(s) skipped." if is_bulk and conditions else \
317
+ f"{len(records)} records inserted successfully." if is_bulk else "Record inserted successfully."
318
+
319
+ res = {
320
+ 'success': True,
321
+ 'message': msg,
322
+ 'insertedRows': inserted_rows
323
+ }
324
+
325
+ if is_bulk and conditions:
326
+ res['inserted'] = len(records)
327
+ res['skipped'] = len(data) - len(records)
328
+
329
+ return res
330
+
331
+ def update(self, data: dict, conditions: dict):
332
+ if not conditions:
333
+ raise Exception("Update requires conditions to prevent bulk overwrite.")
334
+
335
+ set_clauses = []
336
+ replacements = []
337
+
338
+ for k, v in data.items():
339
+ set_clauses.append(f"{self.q}{k}{self.q} = %s")
340
+ replacements.append(v)
341
+
342
+ cond_clauses, cond_reps = self._build_where_clause(conditions)
343
+ if not cond_clauses:
344
+ raise Exception("Update conditions could not be generated. Aborting to prevent bulk update.")
345
+
346
+ replacements.extend(cond_reps)
347
+
348
+ query = f"UPDATE {self.q}{self.table_name}{self.q} SET {', '.join(set_clauses)} WHERE {' AND '.join(cond_clauses)}"
349
+
350
+ with self.connection.cursor() as cursor:
351
+ cursor.execute(query, replacements)
352
+ affected = cursor.rowcount
353
+
354
+ return {
355
+ 'success': True,
356
+ 'message': "Record(s) updated successfully.",
357
+ 'affectedRows': affected
358
+ }
359
+
360
+ def delete(self, conditions: dict):
361
+ if not conditions:
362
+ raise Exception("Delete requires conditions to prevent bulk delete.")
363
+
364
+ cond_clauses, replacements = self._build_where_clause(conditions)
365
+ if not cond_clauses:
366
+ raise Exception("Delete conditions could not be generated. Aborting to prevent bulk delete.")
367
+
368
+ query = f"DELETE FROM {self.q}{self.table_name}{self.q} WHERE {' AND '.join(cond_clauses)}"
369
+
370
+ with self.connection.cursor() as cursor:
371
+ cursor.execute(query, replacements)
372
+ affected = cursor.rowcount
373
+
374
+ return {
375
+ 'success': True,
376
+ 'message': "Record(s) deleted successfully.",
377
+ 'affectedRows': affected
378
+ }
379
+
380
+ def count(self, conditions: dict = None):
381
+ if conditions is None:
382
+ conditions = {}
383
+
384
+ query = f"SELECT COUNT(*) as count FROM {self.q}{self.table_name}{self.q}"
385
+ cond_clauses, replacements = self._build_where_clause(conditions)
386
+
387
+ if cond_clauses:
388
+ query += " WHERE " + " AND ".join(cond_clauses)
389
+
390
+ with self.connection.cursor() as cursor:
391
+ cursor.execute(query, replacements)
392
+ return cursor.fetchone()['count']
@@ -0,0 +1,45 @@
1
+ class SchemaBuilder:
2
+ def __init__(self, connection, db_type: str):
3
+ self.connection = connection
4
+ self.db_type = db_type
5
+ self.q = '"' if db_type in ['postgresql', 'pgsql'] else '`'
6
+
7
+ def create_table(self, table_name: str, columns: list = None):
8
+ if not columns:
9
+ columns = []
10
+
11
+ if columns:
12
+ col_strings = [f"{self.q}{col['name']}{self.q} {col['type']}" for col in columns]
13
+ columns_string = ", ".join(col_strings)
14
+ query = f"CREATE TABLE IF NOT EXISTS {self.q}{table_name}{self.q} ({columns_string})"
15
+ else:
16
+ id_type = "SERIAL PRIMARY KEY" if self.db_type in ['postgresql', 'pgsql'] else "INT AUTO_INCREMENT PRIMARY KEY"
17
+ on_update = "ON UPDATE CURRENT_TIMESTAMP" if self.db_type == 'mysql' else ""
18
+ query = f"""
19
+ CREATE TABLE IF NOT EXISTS {self.q}{table_name}{self.q} (
20
+ {self.q}id{self.q} {id_type},
21
+ {self.q}created_at{self.q} TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
22
+ {self.q}updated_at{self.q} TIMESTAMP DEFAULT CURRENT_TIMESTAMP {on_update}
23
+ )
24
+ """
25
+
26
+ with self.connection.cursor() as cursor:
27
+ cursor.execute(query)
28
+
29
+ return {'success': True, 'message': f"Table {table_name} created successfully."}
30
+
31
+ def drop_table(self, table_name: str):
32
+ query = f"DROP TABLE IF EXISTS {self.q}{table_name}{self.q}"
33
+ with self.connection.cursor() as cursor:
34
+ cursor.execute(query)
35
+ return {'success': True, 'message': f"Table {table_name} dropped successfully."}
36
+
37
+ def get_columns(self, table_name: str):
38
+ if self.db_type == 'mysql':
39
+ query = f"SHOW COLUMNS FROM `{table_name}`"
40
+ else:
41
+ query = f"SELECT column_name as \"Field\", data_type as \"Type\" FROM information_schema.columns WHERE table_name = '{table_name}'"
42
+
43
+ with self.connection.cursor() as cursor:
44
+ cursor.execute(query)
45
+ return cursor.fetchall()
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: pingdartdb
3
+ Version: 1.0.0
4
+ Summary: PingDart Direct Database SDK for Python
5
+ Home-page: https://github.com/pingdart/pingdart
6
+ Author: PingDart
7
+ Author-email: support@pingdart.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: pymysql>=1.0.0
14
+ Requires-Dist: cryptography>=3.4.0
15
+ Requires-Dist: requests>=2.25.0
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # PingDartDB Python SDK
27
+
28
+ The official direct database driver for PingDart in Python.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install pingdartdb
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from pingdartdb import PingDartDB
40
+
41
+ db = PingDartDB("pd_your_license_key_here", {
42
+ "host": "localhost",
43
+ "user": "root",
44
+ "password": "password",
45
+ "database": "pingdart_test",
46
+ "type": "mysql"
47
+ })
48
+
49
+ db.connect()
50
+
51
+ # Read data
52
+ result = db.table('users').read({'conditions': {'status': 'active'}})
53
+ print(result)
54
+
55
+ db.close()
56
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ src/pingdartdb/__init__.py
5
+ src/pingdartdb/client.py
6
+ src/pingdartdb/query_builder.py
7
+ src/pingdartdb/schema_builder.py
8
+ src/pingdartdb.egg-info/PKG-INFO
9
+ src/pingdartdb.egg-info/SOURCES.txt
10
+ src/pingdartdb.egg-info/dependency_links.txt
11
+ src/pingdartdb.egg-info/requires.txt
12
+ src/pingdartdb.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ pymysql>=1.0.0
2
+ cryptography>=3.4.0
3
+ requests>=2.25.0
@@ -0,0 +1 @@
1
+ pingdartdb