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.
- pingdartdb-1.0.0/PKG-INFO +56 -0
- pingdartdb-1.0.0/README.md +31 -0
- pingdartdb-1.0.0/pyproject.toml +3 -0
- pingdartdb-1.0.0/setup.cfg +4 -0
- pingdartdb-1.0.0/setup.py +30 -0
- pingdartdb-1.0.0/src/pingdartdb/__init__.py +4 -0
- pingdartdb-1.0.0/src/pingdartdb/client.py +109 -0
- pingdartdb-1.0.0/src/pingdartdb/query_builder.py +392 -0
- pingdartdb-1.0.0/src/pingdartdb/schema_builder.py +45 -0
- pingdartdb-1.0.0/src/pingdartdb.egg-info/PKG-INFO +56 -0
- pingdartdb-1.0.0/src/pingdartdb.egg-info/SOURCES.txt +12 -0
- pingdartdb-1.0.0/src/pingdartdb.egg-info/dependency_links.txt +1 -0
- pingdartdb-1.0.0/src/pingdartdb.egg-info/requires.txt +3 -0
- pingdartdb-1.0.0/src/pingdartdb.egg-info/top_level.txt +1 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pingdartdb
|