pacificdb 1.2.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.
- pacificdb/__init__.py +9 -0
- pacificdb/client.py +334 -0
- pacificdb/connection.py +88 -0
- pacificdb/errors.py +33 -0
- pacificdb/files.py +233 -0
- pacificdb/operations.py +484 -0
- pacificdb/transport.py +175 -0
- pacificdb-1.2.0.dist-info/METADATA +139 -0
- pacificdb-1.2.0.dist-info/RECORD +12 -0
- pacificdb-1.2.0.dist-info/WHEEL +5 -0
- pacificdb-1.2.0.dist-info/licenses/LICENSE +202 -0
- pacificdb-1.2.0.dist-info/top_level.txt +1 -0
pacificdb/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .client import PacificDBClient
|
|
2
|
+
from .connection import parse_connection_url
|
|
3
|
+
from .errors import PacificDBError, MediaUploadError
|
|
4
|
+
|
|
5
|
+
PacificDB = PacificDBClient
|
|
6
|
+
__all__ = ["PacificDB", "PacificDBClient", "PacificDBError", "MediaUploadError", "parse_connection_url"]
|
|
7
|
+
|
|
8
|
+
__version__ = '1.2.0'
|
|
9
|
+
release_info = {'version': '1.2.0', 'releaseVersion': '1.2.0', 'name': 'PacificDB 1.2.0', 'channel': 'stable', 'tag': 'v1.2.0'}
|
pacificdb/client.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
from .connection import parse_connection_url
|
|
7
|
+
from .errors import PacificDBError, sanitize
|
|
8
|
+
from .transport import ConnectionPool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PacificDBClient:
|
|
12
|
+
__slots__ = ('_settings', '_database', '_token', '_credentials', '_secrets',
|
|
13
|
+
'_auth_state', '_lock', '_pool', '_closed')
|
|
14
|
+
|
|
15
|
+
def __init__(self, host: str = "127.0.0.1", port: int = 9000,
|
|
16
|
+
user_id: str = "system", database: str = "",
|
|
17
|
+
use_tls: bool = False, ca_file: str | None = None,
|
|
18
|
+
timeout: float = 30.0, *, pool_size: int = 16,
|
|
19
|
+
max_response_bytes: int = 64*1024*1024,
|
|
20
|
+
username: str | None = None, password: str | None = None):
|
|
21
|
+
if not isinstance(host, str) or not host or any(c.isspace() for c in host) or '/' in host or '@' in host:
|
|
22
|
+
raise PacificDBError('invalid_connection_options')
|
|
23
|
+
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
|
|
24
|
+
raise PacificDBError('invalid_connection_options')
|
|
25
|
+
if isinstance(pool_size, bool) or not isinstance(pool_size, int) or not 1 <= pool_size <= 32:
|
|
26
|
+
raise PacificDBError('invalid_connection_options')
|
|
27
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or not 0 < timeout <= 1800:
|
|
28
|
+
raise PacificDBError('invalid_connection_options')
|
|
29
|
+
if isinstance(max_response_bytes, bool) or not isinstance(max_response_bytes, int) or not 1 <= max_response_bytes <= 64*1024*1024:
|
|
30
|
+
raise PacificDBError('invalid_connection_options')
|
|
31
|
+
if not isinstance(use_tls, bool) or ca_file and not use_tls:
|
|
32
|
+
raise PacificDBError('invalid_connection_options')
|
|
33
|
+
if not isinstance(user_id, str) or not user_id or not isinstance(database, str):
|
|
34
|
+
raise PacificDBError('invalid_connection_options')
|
|
35
|
+
if (username is not None or password is not None) and (not isinstance(username, str) or not username or not isinstance(password, str) or not password):
|
|
36
|
+
raise PacificDBError('invalid_connection_options')
|
|
37
|
+
self._settings = (host, port, user_id, use_tls, ca_file, timeout, pool_size)
|
|
38
|
+
self._lock = threading.Condition(threading.RLock())
|
|
39
|
+
self._database, self._token, self._closed = database, '', False
|
|
40
|
+
self._credentials = (username, password) if username is not None else None
|
|
41
|
+
self._secrets = {password} if password else set()
|
|
42
|
+
self._auth_state = 'new'
|
|
43
|
+
self._pool = ConnectionPool(host, port, timeout=timeout, pool_size=pool_size,
|
|
44
|
+
use_tls=use_tls, ca_file=ca_file, max_response_bytes=max_response_bytes)
|
|
45
|
+
|
|
46
|
+
host = property(lambda self: self._settings[0])
|
|
47
|
+
port = property(lambda self: self._settings[1])
|
|
48
|
+
user_id = property(lambda self: self._settings[2])
|
|
49
|
+
use_tls = property(lambda self: self._settings[3])
|
|
50
|
+
ca_file = property(lambda self: self._settings[4])
|
|
51
|
+
timeout = property(lambda self: self._settings[5])
|
|
52
|
+
pool_size = property(lambda self: self._settings[6])
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def database(self):
|
|
56
|
+
with self._lock: return self._database
|
|
57
|
+
|
|
58
|
+
@database.setter
|
|
59
|
+
def database(self, value):
|
|
60
|
+
if not isinstance(value, str): raise PacificDBError('invalid_database')
|
|
61
|
+
with self._lock: self._database = value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def token(self):
|
|
65
|
+
with self._lock: return self._token
|
|
66
|
+
|
|
67
|
+
@token.setter
|
|
68
|
+
def token(self, value):
|
|
69
|
+
if not isinstance(value, str): raise PacificDBError('invalid_token')
|
|
70
|
+
with self._lock:
|
|
71
|
+
self._token = value
|
|
72
|
+
if value: self._secrets.add(value)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_url(cls, url, **options):
|
|
76
|
+
return cls(**parse_connection_url(url, options))
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def connect(cls, url, **options):
|
|
80
|
+
client = cls.from_url(url, **options)
|
|
81
|
+
try:
|
|
82
|
+
client.request({'action': 'ping'})
|
|
83
|
+
return client
|
|
84
|
+
except BaseException:
|
|
85
|
+
client.close()
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
return f'PacificDBClient(host={self.host!r}, port={self.port}, database={self.database!r}, tls={self.use_tls})'
|
|
90
|
+
|
|
91
|
+
def __enter__(self):
|
|
92
|
+
with self._lock:
|
|
93
|
+
if self._closed: raise PacificDBError('client_closed')
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __exit__(self, *_):
|
|
97
|
+
self.close()
|
|
98
|
+
|
|
99
|
+
def close(self):
|
|
100
|
+
with self._lock:
|
|
101
|
+
self._closed = True
|
|
102
|
+
self._lock.notify_all()
|
|
103
|
+
self._pool.close()
|
|
104
|
+
|
|
105
|
+
def _safe_error(self, error):
|
|
106
|
+
with self._lock: secrets = tuple(self._secrets)
|
|
107
|
+
if not isinstance(error, PacificDBError):
|
|
108
|
+
return PacificDBError('invalid_request', 'Invalid PacificDB request')
|
|
109
|
+
return PacificDBError(sanitize(error.code, secrets), sanitize(str(error), secrets), sanitize(error.response, secrets))
|
|
110
|
+
|
|
111
|
+
def _send(self, payload, deadline):
|
|
112
|
+
def learn(value):
|
|
113
|
+
if isinstance(value, dict):
|
|
114
|
+
for field, item in value.items():
|
|
115
|
+
if any(part in str(field).lower() for part in ('password', 'token', 'authorization')) and isinstance(item, str) and item:
|
|
116
|
+
self._secrets.add(item)
|
|
117
|
+
learn(item)
|
|
118
|
+
elif isinstance(value, list):
|
|
119
|
+
for item in value: learn(item)
|
|
120
|
+
with self._lock: learn(payload)
|
|
121
|
+
try: wire = json.dumps(payload, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')+b'\n'
|
|
122
|
+
except (TypeError, ValueError, UnicodeError): raise PacificDBError('invalid_request', 'Command must contain valid JSON values') from None
|
|
123
|
+
value = self._pool.request(wire, deadline=deadline)
|
|
124
|
+
if isinstance(value, dict) and value.get('error'):
|
|
125
|
+
error = value['error']
|
|
126
|
+
code = error.get('code', 'engine_error') if isinstance(error, dict) else str(error)
|
|
127
|
+
message = error.get('message', code) if isinstance(error, dict) else value.get('message', code)
|
|
128
|
+
raise PacificDBError(code, str(message), value)
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
def _ensure_authenticated(self, deadline):
|
|
132
|
+
with self._lock:
|
|
133
|
+
if self._closed: raise PacificDBError('client_closed')
|
|
134
|
+
if not self._credentials: return self._token
|
|
135
|
+
while self._auth_state == 'pending' and not self._closed:
|
|
136
|
+
remaining = deadline-time.monotonic()
|
|
137
|
+
if remaining <= 0: raise PacificDBError('request_timeout')
|
|
138
|
+
self._lock.wait(remaining)
|
|
139
|
+
if self._closed or self._auth_state == 'failed': raise PacificDBError('client_closed')
|
|
140
|
+
if self._auth_state == 'ready': return self._token
|
|
141
|
+
self._auth_state = 'pending'
|
|
142
|
+
username, password = self._credentials
|
|
143
|
+
scope = {'userId': self.user_id, 'dbName': self._database}
|
|
144
|
+
try:
|
|
145
|
+
result = self._send({**scope, 'action': 'security_authenticate', 'username': username, 'password': password}, deadline)
|
|
146
|
+
if not isinstance(result, dict) or not isinstance(result.get('token'), str) or not result['token']:
|
|
147
|
+
raise PacificDBError('invalid_response', 'Invalid authentication response')
|
|
148
|
+
with self._lock:
|
|
149
|
+
if self._closed: raise PacificDBError('client_closed')
|
|
150
|
+
self.token = result['token']; self._auth_state = 'ready'; self._lock.notify_all()
|
|
151
|
+
return self._token
|
|
152
|
+
except BaseException:
|
|
153
|
+
with self._lock: self._auth_state = 'failed'
|
|
154
|
+
self.close()
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
def _scoped_request(self):
|
|
158
|
+
with self._lock:
|
|
159
|
+
scope = {'userId': self.user_id, 'dbName': self._database}
|
|
160
|
+
if self._token: scope['token'] = self._token
|
|
161
|
+
def request(command):
|
|
162
|
+
deadline = time.monotonic()+self.timeout
|
|
163
|
+
try:
|
|
164
|
+
if not isinstance(command, dict): raise PacificDBError('invalid_request')
|
|
165
|
+
payload = json.loads(json.dumps({**scope, **command}, allow_nan=False))
|
|
166
|
+
if command.get('action') != 'security_authenticate':
|
|
167
|
+
token = self._ensure_authenticated(deadline)
|
|
168
|
+
if token:
|
|
169
|
+
scope.setdefault('token', token); payload.setdefault('token', scope['token'])
|
|
170
|
+
return self._send(payload, deadline)
|
|
171
|
+
except (PacificDBError, ValueError, TypeError, UnicodeError) as error:
|
|
172
|
+
failure = self._safe_error(error)
|
|
173
|
+
# Raise outside the handler so __context__ cannot retain a private engine error.
|
|
174
|
+
raise failure from None
|
|
175
|
+
request.wire_bytes = lambda command: len(json.dumps({**scope, **command}, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')) + 1
|
|
176
|
+
return request
|
|
177
|
+
|
|
178
|
+
def request(self, command: dict[str, Any]) -> Any:
|
|
179
|
+
return self._scoped_request()(command)
|
|
180
|
+
|
|
181
|
+
def authenticate(self, username: str, password: str) -> dict[str, Any]:
|
|
182
|
+
if not isinstance(username, str) or not username or not isinstance(password, str) or not password:
|
|
183
|
+
raise PacificDBError('invalid_credentials')
|
|
184
|
+
with self._lock: self._secrets.add(password)
|
|
185
|
+
result = self.request({'action': 'security_authenticate', 'username': username, 'password': password})
|
|
186
|
+
if not isinstance(result, dict) or not isinstance(result.get('token'), str) or not result['token']:
|
|
187
|
+
raise PacificDBError('invalid_response', 'Invalid authentication response')
|
|
188
|
+
self.token = result['token']
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _name(value, kind):
|
|
193
|
+
if not isinstance(value, str) or not value:
|
|
194
|
+
raise PacificDBError(f'invalid_{kind}', f'A non-empty {kind} name is required')
|
|
195
|
+
return value
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _object(value):
|
|
199
|
+
if not isinstance(value, dict): raise PacificDBError('invalid_request', 'Expected a JSON object')
|
|
200
|
+
return value
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _array(value):
|
|
204
|
+
if not isinstance(value, list): raise PacificDBError('invalid_request', 'Expected a JSON array')
|
|
205
|
+
return value
|
|
206
|
+
|
|
207
|
+
def _database_request(self, command):
|
|
208
|
+
with self._lock:
|
|
209
|
+
if not self._database: raise PacificDBError('database_required', 'database_required: select or create a database first')
|
|
210
|
+
request = self._scoped_request()
|
|
211
|
+
return request(command)
|
|
212
|
+
|
|
213
|
+
def _collection_request(self, collection, action, **fields):
|
|
214
|
+
self._name(collection, 'collection')
|
|
215
|
+
return self._database_request({'action': action, 'collection': collection, **fields})
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def indexes(self):
|
|
219
|
+
from .operations import Indexes
|
|
220
|
+
return Indexes(self)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def vectors(self):
|
|
224
|
+
from .operations import Vectors
|
|
225
|
+
return Vectors(self)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def media(self):
|
|
229
|
+
from .operations import Media
|
|
230
|
+
return Media(self)
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def backups(self):
|
|
234
|
+
from .operations import Backups
|
|
235
|
+
return Backups(self)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def security(self):
|
|
239
|
+
from .operations import Security
|
|
240
|
+
return Security(self)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def admin(self):
|
|
244
|
+
from .operations import Admin
|
|
245
|
+
return Admin(self)
|
|
246
|
+
|
|
247
|
+
def capabilities(self):
|
|
248
|
+
return self.request({'action': 'community_capabilities'})
|
|
249
|
+
|
|
250
|
+
def create_database(self, name: str | None = None, db_type: str = "binary") -> Any:
|
|
251
|
+
name = self._name(self.database if name is None else name, 'database')
|
|
252
|
+
self._name(db_type, 'database_type')
|
|
253
|
+
response = self.request({'action': 'createDatabase', 'dbName': name, 'dbType': db_type})
|
|
254
|
+
self.database = name
|
|
255
|
+
return response
|
|
256
|
+
|
|
257
|
+
def list_databases(self) -> list[str]:
|
|
258
|
+
response = self.request({'action': 'listDatabases'})
|
|
259
|
+
if not isinstance(response, list) or not all(isinstance(name, str) for name in response):
|
|
260
|
+
raise PacificDBError('unexpected_response_shape', 'Expected an array of database names')
|
|
261
|
+
return response
|
|
262
|
+
|
|
263
|
+
def use_database(self, name: str):
|
|
264
|
+
self._name(name, 'database')
|
|
265
|
+
names = self.list_databases()
|
|
266
|
+
if name not in names: raise PacificDBError('database_not_found')
|
|
267
|
+
self.database = name
|
|
268
|
+
return names
|
|
269
|
+
|
|
270
|
+
def drop_database(self, name: str | None = None):
|
|
271
|
+
with self._lock:
|
|
272
|
+
name = self._name(self._database if name is None else name, 'database')
|
|
273
|
+
request = self._scoped_request()
|
|
274
|
+
response = request({'action': 'dropDatabase', 'dbName': name})
|
|
275
|
+
with self._lock:
|
|
276
|
+
if self._database == name: self._database = ''
|
|
277
|
+
return response
|
|
278
|
+
|
|
279
|
+
def create_collection(self, name: str, *, vector_schema=None) -> Any:
|
|
280
|
+
return self._collection_request(name, 'createCollection',
|
|
281
|
+
**({'vector_schema': vector_schema} if vector_schema is not None else {}))
|
|
282
|
+
|
|
283
|
+
def list_collections(self):
|
|
284
|
+
return self._database_request({'action': 'listCollections'})
|
|
285
|
+
|
|
286
|
+
def drop_collection(self, name: str):
|
|
287
|
+
return self._collection_request(name, 'dropCollection')
|
|
288
|
+
|
|
289
|
+
def insert(self, collection: str, document: dict[str, Any]) -> Any:
|
|
290
|
+
return self._collection_request(collection, 'insert', data=self._object(document))
|
|
291
|
+
|
|
292
|
+
def insert_many(self, collection: str, documents: list[dict]):
|
|
293
|
+
self._array(documents)
|
|
294
|
+
for document in documents: self._object(document)
|
|
295
|
+
return self._collection_request(collection, 'insertMany', data=documents)
|
|
296
|
+
|
|
297
|
+
def find(self, collection: str, filter: dict[str, Any] | None = None,
|
|
298
|
+
limit: int = -1, offset: int = 0) -> Any:
|
|
299
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or limit < -1 or isinstance(offset, bool) or not isinstance(offset, int) or offset < 0:
|
|
300
|
+
raise PacificDBError('invalid_pagination', 'limit must be -1 or nonnegative; offset must be nonnegative')
|
|
301
|
+
return self._collection_request(collection, 'find', filter=self._object({} if filter is None else filter), limit=limit, offset=offset)
|
|
302
|
+
|
|
303
|
+
def find_one(self, collection: str, filter: dict | None = None):
|
|
304
|
+
response = self.find(collection, filter, limit=1)
|
|
305
|
+
rows = response.get('data') if isinstance(response, dict) else response
|
|
306
|
+
if not isinstance(rows, list) or rows and not isinstance(rows[0], dict):
|
|
307
|
+
raise PacificDBError('unexpected_response_shape', 'Expected an array of documents')
|
|
308
|
+
return rows[0] if rows else None
|
|
309
|
+
|
|
310
|
+
def count(self, collection: str, filter: dict | None = None):
|
|
311
|
+
return self._collection_request(collection, 'count', filter=self._object({} if filter is None else filter))
|
|
312
|
+
|
|
313
|
+
def aggregate(self, collection: str, pipeline: list):
|
|
314
|
+
return self._collection_request(collection, 'aggregate', pipeline=self._array(pipeline))
|
|
315
|
+
|
|
316
|
+
def explain(self, collection: str, filter: dict | None = None):
|
|
317
|
+
return self._collection_request(collection, 'explain', filter=self._object({} if filter is None else filter))
|
|
318
|
+
|
|
319
|
+
def update_one(self, collection: str, filter: dict[str, Any], update: dict[str, Any]) -> Any:
|
|
320
|
+
return self._collection_request(collection, 'updateOne', filter=self._object(filter), update=self._object(update))
|
|
321
|
+
|
|
322
|
+
def update_many(self, collection: str, filter: dict, update: dict):
|
|
323
|
+
return self._collection_request(collection, 'updateMany', filter=self._object(filter), update=self._object(update))
|
|
324
|
+
|
|
325
|
+
def delete_one(self, collection: str, filter: dict[str, Any]) -> Any:
|
|
326
|
+
return self._collection_request(collection, 'deleteOne', filter=self._object(filter))
|
|
327
|
+
|
|
328
|
+
def delete_many(self, collection: str, filter: dict):
|
|
329
|
+
return self._collection_request(collection, 'deleteMany', filter=self._object(filter))
|
|
330
|
+
|
|
331
|
+
def bulk_write(self, collection: str, operations: list):
|
|
332
|
+
self._array(operations)
|
|
333
|
+
for operation in operations: self._object(operation)
|
|
334
|
+
return self._collection_request(collection, 'bulkWrite', ops=operations)
|
pacificdb/connection.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Parse the shared connection URL grammar without performing I/O."""
|
|
2
|
+
import ipaddress
|
|
3
|
+
import re
|
|
4
|
+
from urllib.parse import unquote_to_bytes
|
|
5
|
+
from .errors import PacificDBError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _invalid():
|
|
9
|
+
return PacificDBError('invalid_connection_url', 'Invalid PacificDB connection URL or conflicting options')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _decode(value):
|
|
13
|
+
if re.search(r'%(?![0-9a-fA-F]{2})', value):
|
|
14
|
+
raise _invalid()
|
|
15
|
+
try:
|
|
16
|
+
decoded = unquote_to_bytes(value).decode('utf-8', errors='strict')
|
|
17
|
+
if not decoded or any(ord(char) < 32 or ord(char) == 127 for char in decoded):
|
|
18
|
+
raise _invalid()
|
|
19
|
+
return decoded
|
|
20
|
+
except (UnicodeError, ValueError):
|
|
21
|
+
pass
|
|
22
|
+
raise _invalid() from None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _integer(value, maximum):
|
|
26
|
+
if isinstance(value, bool) or not re.fullmatch('[0-9]+', str(value)):
|
|
27
|
+
raise _invalid()
|
|
28
|
+
try: number = int(value)
|
|
29
|
+
except ValueError: raise _invalid() from None
|
|
30
|
+
if not 1 <= number <= maximum:
|
|
31
|
+
raise _invalid()
|
|
32
|
+
return number
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_connection_url(url: str, options: dict | None = None) -> dict:
|
|
36
|
+
if not isinstance(url, str) or re.search(r'[\s\x00-\x1f\x7f#]', url):
|
|
37
|
+
raise _invalid()
|
|
38
|
+
match = re.fullmatch(r'(pacificdbs?)://([^/?#]+)(/[^?#]*)(?:\?([^#]*))?', url)
|
|
39
|
+
if not match:
|
|
40
|
+
raise _invalid()
|
|
41
|
+
scheme, authority, path, query = match.groups()
|
|
42
|
+
database = _decode(path[1:])
|
|
43
|
+
if '/' in database or '\\' in database or database in ('.', '..') or authority.count('@') > 1:
|
|
44
|
+
raise _invalid()
|
|
45
|
+
credentials = {}
|
|
46
|
+
address = authority
|
|
47
|
+
if '@' in authority:
|
|
48
|
+
userinfo, address = authority.split('@')
|
|
49
|
+
if ':' not in userinfo:
|
|
50
|
+
raise _invalid()
|
|
51
|
+
username, password = userinfo.split(':', 1)
|
|
52
|
+
credentials = {'username': _decode(username), 'password': _decode(password)}
|
|
53
|
+
if address.startswith('['):
|
|
54
|
+
host_match = re.fullmatch(r'\[([^\]]+)\](?::([0-9]+))?', address)
|
|
55
|
+
if not host_match: raise _invalid()
|
|
56
|
+
host, port = host_match.groups()
|
|
57
|
+
try: ipaddress.IPv6Address(host)
|
|
58
|
+
except ValueError: raise _invalid() from None
|
|
59
|
+
if '%' in host: raise _invalid()
|
|
60
|
+
else:
|
|
61
|
+
host_match = re.fullmatch(r'([A-Za-z0-9._-]+)(?::([0-9]+))?', address)
|
|
62
|
+
if not host_match: raise _invalid()
|
|
63
|
+
host, port = host_match.groups()
|
|
64
|
+
result = {'host': host.lower(), 'port': _integer(port, 65535) if port else 9000,
|
|
65
|
+
'database': database, 'use_tls': scheme == 'pacificdbs',
|
|
66
|
+
'user_id': 'system', 'timeout': 30.0, 'pool_size': 16, **credentials}
|
|
67
|
+
fixed = {'host', 'port', 'database', 'use_tls', *credentials}
|
|
68
|
+
names = {'userId': 'user_id', 'timeoutMs': 'timeout', 'poolSize': 'pool_size', 'caFile': 'ca_file'}
|
|
69
|
+
seen = set()
|
|
70
|
+
for pair in query.split('&') if query else []:
|
|
71
|
+
if '=' not in pair: raise _invalid()
|
|
72
|
+
key, value = [_decode(piece.replace('+', '%20')) for piece in pair.split('=', 1)]
|
|
73
|
+
if key not in names or key in seen: raise _invalid()
|
|
74
|
+
seen.add(key); fixed.add(names[key])
|
|
75
|
+
result[names[key]] = (_integer(value, 1800000) / 1000 if key == 'timeoutMs' else
|
|
76
|
+
_integer(value, 32) if key == 'poolSize' else value)
|
|
77
|
+
for key, value in (options or {}).items():
|
|
78
|
+
if key not in {*result, 'ca_file', 'max_response_bytes', 'username', 'password'}:
|
|
79
|
+
raise _invalid()
|
|
80
|
+
if key in fixed and result.get(key) != value:
|
|
81
|
+
raise _invalid()
|
|
82
|
+
result[key] = value
|
|
83
|
+
result['pool_size'] = _integer(result['pool_size'], 32)
|
|
84
|
+
timeout = result['timeout']
|
|
85
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not 0.001 <= timeout <= 1800:
|
|
86
|
+
raise _invalid()
|
|
87
|
+
if result.get('ca_file') and not result['use_tls']: raise _invalid()
|
|
88
|
+
return result
|
pacificdb/errors.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Public engine/transport errors; diagnostic values contain no known secrets."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sanitize(value, secrets=()):
|
|
6
|
+
if isinstance(value, str):
|
|
7
|
+
value = re.sub(r'pacificdbs?://[^\s/]*@', 'pacificdb://[redacted]@', value)
|
|
8
|
+
for secret in secrets:
|
|
9
|
+
if secret:
|
|
10
|
+
value = value.replace(secret, '[redacted]')
|
|
11
|
+
return value
|
|
12
|
+
if isinstance(value, dict):
|
|
13
|
+
return {sanitize(key, secrets): '[redacted]' if re.search('password|token|authorization', str(key), re.I)
|
|
14
|
+
else sanitize(item, secrets) for key, item in value.items()}
|
|
15
|
+
if isinstance(value, list):
|
|
16
|
+
return [sanitize(item, secrets) for item in value]
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PacificDBError(RuntimeError):
|
|
21
|
+
def __init__(self, code, message=None, response=None):
|
|
22
|
+
self.code = str(code)
|
|
23
|
+
self.response = response
|
|
24
|
+
super().__init__(message or self.code)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MediaUploadError(PacificDBError):
|
|
28
|
+
def __init__(self, code, message=None, response=None, *, upload_id=None,
|
|
29
|
+
next_chunk=0, received_chunks=0, received_bytes=0, resumable=False):
|
|
30
|
+
super().__init__(code, message, response)
|
|
31
|
+
self.upload_id, self.next_chunk = upload_id, next_chunk
|
|
32
|
+
self.received_chunks, self.received_bytes = received_chunks, received_bytes
|
|
33
|
+
self.resumable = resumable
|