coffy 0.1.4__py3-none-any.whl → 0.1.6__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.
- coffy/__init__.py +1 -1
- coffy/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/graph/__init__.py +3 -1
- coffy/graph/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/graph/__pycache__/graphdb_nx.cpython-312.pyc +0 -0
- coffy/graph/graphdb_nx.py +335 -51
- coffy/nosql/__init__.py +4 -0
- coffy/nosql/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/nosql/__pycache__/engine.cpython-312.pyc +0 -0
- coffy/nosql/engine.py +396 -33
- coffy/sql/__init__.py +7 -0
- coffy/sql/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/sql/__pycache__/engine.cpython-312.pyc +0 -0
- coffy/sql/__pycache__/sqldict.cpython-312.pyc +0 -0
- coffy/sql/engine.py +30 -3
- coffy/sql/sqldict.py +53 -13
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/METADATA +6 -4
- coffy-0.1.6.dist-info/RECORD +28 -0
- coffy/graph/graph_tests.py +0 -137
- coffy/nosql/nosql_tests.py +0 -104
- coffy-0.1.4.dist-info/RECORD +0 -30
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/WHEEL +0 -0
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/top_level.txt +0 -0
coffy/nosql/engine.py
CHANGED
@@ -1,22 +1,43 @@
|
|
1
1
|
# coffy/nosql/engine.py
|
2
2
|
# author: nsarathy
|
3
3
|
|
4
|
+
"""
|
5
|
+
A simple NoSQL database engine.
|
6
|
+
This engine supports basic CRUD operations, querying with filters, and aggregation functions.
|
7
|
+
"""
|
8
|
+
|
4
9
|
import json
|
5
10
|
import os
|
6
11
|
import re
|
7
12
|
|
13
|
+
|
8
14
|
class QueryBuilder:
|
15
|
+
"""
|
16
|
+
A class to build and execute queries on a collection of documents.
|
17
|
+
Supports filtering, aggregation, and lookups.
|
18
|
+
"""
|
19
|
+
|
9
20
|
def __init__(self, documents, all_collections=None):
|
21
|
+
"""
|
22
|
+
Initialize the QueryBuilder with a collection of documents.
|
23
|
+
documents -- List of documents (dictionaries) to query.
|
24
|
+
all_collections -- Optional dictionary of all collections for lookups.
|
25
|
+
"""
|
10
26
|
self.documents = documents
|
11
27
|
self.filters = []
|
12
28
|
self.current_field = None
|
13
29
|
self.all_collections = all_collections or {}
|
14
30
|
self._lookup_done = False
|
15
31
|
self._lookup_results = None
|
16
|
-
|
32
|
+
|
17
33
|
@staticmethod
|
18
34
|
def _get_nested(doc, dotted_key):
|
19
|
-
|
35
|
+
"""
|
36
|
+
Get a nested value from a document using a dotted key.
|
37
|
+
dotted_key -- A string representing the path to the value, e.g., "a.b.c".
|
38
|
+
Returns the value if found, otherwise None.
|
39
|
+
"""
|
40
|
+
keys = dotted_key.split(".")
|
20
41
|
for k in keys:
|
21
42
|
if not isinstance(doc, dict) or k not in doc:
|
22
43
|
return None
|
@@ -24,49 +45,135 @@ class QueryBuilder:
|
|
24
45
|
return doc
|
25
46
|
|
26
47
|
def where(self, field):
|
48
|
+
"""
|
49
|
+
Set the current field for filtering.
|
50
|
+
field -- The field to filter on, can be a dotted path like "a.b.c".
|
51
|
+
Returns self to allow method chaining.
|
52
|
+
"""
|
27
53
|
self.current_field = field
|
28
54
|
return self
|
29
55
|
|
30
56
|
# Comparison
|
31
|
-
def eq(self, value):
|
32
|
-
|
57
|
+
def eq(self, value):
|
58
|
+
"""
|
59
|
+
Filter documents where the current field equals the given value.
|
60
|
+
value -- The value to compare against.
|
61
|
+
Returns self to allow method chaining.
|
62
|
+
"""
|
63
|
+
return self._add_filter(
|
64
|
+
lambda d: QueryBuilder._get_nested(d, self.current_field) == value
|
65
|
+
)
|
66
|
+
|
67
|
+
def ne(self, value):
|
68
|
+
"""
|
69
|
+
Filter documents where the current field does not equal the given value.
|
70
|
+
value -- The value to compare against.
|
71
|
+
Returns self to allow method chaining.
|
72
|
+
"""
|
73
|
+
return self._add_filter(
|
74
|
+
lambda d: QueryBuilder._get_nested(d, self.current_field) != value
|
75
|
+
)
|
76
|
+
|
33
77
|
def gt(self, value):
|
78
|
+
"""
|
79
|
+
Filter documents where the current field is greater than the given value.
|
80
|
+
value -- The value to compare against.
|
81
|
+
Returns self to allow method chaining.
|
82
|
+
"""
|
34
83
|
return self._add_filter(
|
35
|
-
lambda d: isinstance(
|
84
|
+
lambda d: isinstance(
|
85
|
+
QueryBuilder._get_nested(d, self.current_field), (int, float)
|
86
|
+
)
|
87
|
+
and QueryBuilder._get_nested(d, self.current_field) > value
|
36
88
|
)
|
37
89
|
|
38
90
|
def gte(self, value):
|
91
|
+
"""
|
92
|
+
Filter documents where the current field is greater than or equal to the given value.
|
93
|
+
value -- The value to compare against.
|
94
|
+
Returns self to allow method chaining.
|
95
|
+
"""
|
39
96
|
return self._add_filter(
|
40
|
-
lambda d: isinstance(
|
97
|
+
lambda d: isinstance(
|
98
|
+
QueryBuilder._get_nested(d, self.current_field), (int, float)
|
99
|
+
)
|
100
|
+
and QueryBuilder._get_nested(d, self.current_field) >= value
|
41
101
|
)
|
42
102
|
|
43
103
|
def lt(self, value):
|
104
|
+
"""
|
105
|
+
Filter documents where the current field is less than the given value.
|
106
|
+
value -- The value to compare against.
|
107
|
+
Returns self to allow method chaining.
|
108
|
+
"""
|
44
109
|
return self._add_filter(
|
45
|
-
lambda d: isinstance(
|
110
|
+
lambda d: isinstance(
|
111
|
+
QueryBuilder._get_nested(d, self.current_field), (int, float)
|
112
|
+
)
|
113
|
+
and QueryBuilder._get_nested(d, self.current_field) < value
|
46
114
|
)
|
47
115
|
|
48
116
|
def lte(self, value):
|
117
|
+
"""
|
118
|
+
Filter documents where the current field is less than or equal to the given value.
|
119
|
+
value -- The value to compare against.
|
120
|
+
Returns self to allow method chaining.
|
121
|
+
"""
|
49
122
|
return self._add_filter(
|
50
|
-
lambda d: isinstance(
|
123
|
+
lambda d: isinstance(
|
124
|
+
QueryBuilder._get_nested(d, self.current_field), (int, float)
|
125
|
+
)
|
126
|
+
and QueryBuilder._get_nested(d, self.current_field) <= value
|
51
127
|
)
|
52
128
|
|
53
129
|
def in_(self, values):
|
130
|
+
"""
|
131
|
+
Filter documents where the current field is in the given list of values.
|
132
|
+
values -- The list of values to compare against.
|
133
|
+
Returns self to allow method chaining.
|
134
|
+
"""
|
54
135
|
return self._add_filter(
|
55
136
|
lambda d: QueryBuilder._get_nested(d, self.current_field) in values
|
56
137
|
)
|
57
138
|
|
58
139
|
def nin(self, values):
|
140
|
+
"""
|
141
|
+
Filter documents where the current field is not in the given list of values.
|
142
|
+
values -- The list of values to compare against.
|
143
|
+
Returns self to allow method chaining.
|
144
|
+
"""
|
59
145
|
return self._add_filter(
|
60
146
|
lambda d: QueryBuilder._get_nested(d, self.current_field) not in values
|
61
147
|
)
|
62
148
|
|
63
|
-
def matches(self, regex):
|
64
|
-
|
149
|
+
def matches(self, regex):
|
150
|
+
"""
|
151
|
+
Filter documents where the current field matches the given regular expression.
|
152
|
+
regex -- The regular expression to match against.
|
153
|
+
Returns self to allow method chaining.
|
154
|
+
"""
|
155
|
+
return self._add_filter(
|
156
|
+
lambda d: re.search(
|
157
|
+
regex, str(QueryBuilder._get_nested(d, self.current_field))
|
158
|
+
)
|
159
|
+
)
|
160
|
+
|
65
161
|
def exists(self):
|
66
|
-
|
162
|
+
"""
|
163
|
+
Filter documents where the current field exists.
|
164
|
+
Returns self to allow method chaining.
|
165
|
+
"""
|
166
|
+
return self._add_filter(
|
167
|
+
lambda d: QueryBuilder._get_nested(d, self.current_field) is not None
|
168
|
+
)
|
67
169
|
|
68
170
|
# Logic grouping
|
69
171
|
def _and(self, *fns):
|
172
|
+
"""
|
173
|
+
Combine multiple filter functions with logical AND.
|
174
|
+
fns -- Functions that take a QueryBuilder instance and modify its filters.
|
175
|
+
Returns self to allow method chaining.
|
176
|
+
"""
|
70
177
|
for fn in fns:
|
71
178
|
sub = QueryBuilder(self.documents, self.all_collections)
|
72
179
|
fn(sub)
|
@@ -74,6 +181,11 @@ class QueryBuilder:
|
|
74
181
|
return self
|
75
182
|
|
76
183
|
def _not(self, *fns):
|
184
|
+
"""
|
185
|
+
Combine multiple filter functions with logical NOT.
|
186
|
+
fns -- Functions that take a QueryBuilder instance and modify its filters.
|
187
|
+
Returns self to allow method chaining.
|
188
|
+
"""
|
77
189
|
for fn in fns:
|
78
190
|
sub = QueryBuilder(self.documents, self.all_collections)
|
79
191
|
fn(sub)
|
@@ -81,6 +193,11 @@ class QueryBuilder:
|
|
81
193
|
return self
|
82
194
|
|
83
195
|
def _or(self, *fns):
|
196
|
+
"""
|
197
|
+
Combine multiple filter functions with logical OR.
|
198
|
+
fns -- Functions that take a QueryBuilder instance and modify its filters.
|
199
|
+
Returns self to allow method chaining.
|
200
|
+
"""
|
84
201
|
chains = []
|
85
202
|
for fn in fns:
|
86
203
|
sub = QueryBuilder(self.documents, self.all_collections)
|
@@ -91,6 +208,11 @@ class QueryBuilder:
|
|
91
208
|
|
92
209
|
# Add filter
|
93
210
|
def _add_filter(self, fn):
|
211
|
+
"""
|
212
|
+
Add a filter function to the query.
|
213
|
+
fn -- A function that takes a document and returns True if it matches the filter.
|
214
|
+
Returns self to allow method chaining.
|
215
|
+
"""
|
94
216
|
negate = getattr(self, "_negate", False)
|
95
217
|
self._negate = False
|
96
218
|
self.filters.append(lambda d: not fn(d) if negate else fn(d))
|
@@ -98,6 +220,13 @@ class QueryBuilder:
|
|
98
220
|
|
99
221
|
# Core execution
|
100
222
|
def run(self, fields=None):
|
223
|
+
"""
|
224
|
+
Execute the query and return the results.
|
225
|
+
fields -- Optional list of fields to project in the results.
|
226
|
+
If provided, only these fields will be included in the returned documents.
|
227
|
+
Otherwise, the full documents will be returned.
|
228
|
+
Returns a DocList containing the matching documents.
|
229
|
+
"""
|
101
230
|
results = [doc for doc in self.documents if all(f(doc) for f in self.filters)]
|
102
231
|
if self._lookup_done:
|
103
232
|
results = self._lookup_results
|
@@ -114,8 +243,12 @@ class QueryBuilder:
|
|
114
243
|
|
115
244
|
return DocList(results)
|
116
245
|
|
117
|
-
|
118
246
|
def update(self, changes):
|
247
|
+
"""
|
248
|
+
Update documents that match the current filters with the given changes.
|
249
|
+
changes -- A dictionary of fields to update in the matching documents.
|
250
|
+
Returns a dictionary with the count of updated documents.
|
251
|
+
"""
|
119
252
|
count = 0
|
120
253
|
for doc in self.documents:
|
121
254
|
if all(f(doc) for f in self.filters):
|
@@ -124,11 +257,22 @@ class QueryBuilder:
|
|
124
257
|
return {"updated": count}
|
125
258
|
|
126
259
|
def delete(self):
|
260
|
+
"""
|
261
|
+
Delete documents that match the current filters.
|
262
|
+
Returns a dictionary with the count of deleted documents.
|
263
|
+
"""
|
127
264
|
before = len(self.documents)
|
128
|
-
self.documents[:] = [
|
265
|
+
self.documents[:] = [
|
266
|
+
doc for doc in self.documents if not all(f(doc) for f in self.filters)
|
267
|
+
]
|
129
268
|
return {"deleted": before - len(self.documents)}
|
130
269
|
|
131
270
|
def replace(self, new_doc):
|
271
|
+
"""
|
272
|
+
Replace documents that match the current filters with a new document.
|
273
|
+
new_doc -- The new document to replace matching documents with.
|
274
|
+
Returns a dictionary with the count of replaced documents.
|
275
|
+
"""
|
132
276
|
replaced = 0
|
133
277
|
for i, doc in enumerate(self.documents):
|
134
278
|
if all(f(doc) for f in self.filters):
|
@@ -136,27 +280,86 @@ class QueryBuilder:
|
|
136
280
|
replaced += 1
|
137
281
|
return {"replaced": replaced}
|
138
282
|
|
139
|
-
def count(self):
|
140
|
-
|
283
|
+
def count(self):
|
284
|
+
"""
|
285
|
+
Count the number of documents that match the current filters.
|
286
|
+
Returns the count of matching documents.
|
287
|
+
"""
|
288
|
+
return len(self.run())
|
289
|
+
|
290
|
+
def first(self):
|
291
|
+
"""
|
292
|
+
Get the first document that matches the current filters.
|
293
|
+
Returns the first matching document, or None if no documents match.
|
294
|
+
"""
|
295
|
+
return next(iter(self.run()), None)
|
141
296
|
|
142
297
|
# Aggregates
|
143
298
|
def sum(self, field):
|
144
|
-
|
299
|
+
"""
|
300
|
+
Calculate the sum of a numeric field across all matching documents.
|
301
|
+
field -- The field to sum.
|
302
|
+
Returns the total sum of the field values.
|
303
|
+
If no documents match or the field is not numeric, returns 0.
|
304
|
+
"""
|
305
|
+
return sum(
|
306
|
+
doc.get(field, 0)
|
307
|
+
for doc in self.run()
|
308
|
+
if isinstance(doc.get(field), (int, float))
|
309
|
+
)
|
145
310
|
|
146
311
|
def avg(self, field):
|
147
|
-
|
312
|
+
"""
|
313
|
+
Calculate the average of a numeric field across all matching documents.
|
314
|
+
field -- The field to average.
|
315
|
+
Returns the average of the field values.
|
316
|
+
If no documents match or the field is not numeric, returns 0.
|
317
|
+
"""
|
318
|
+
values = [
|
319
|
+
doc.get(field)
|
320
|
+
for doc in self.run()
|
321
|
+
if isinstance(doc.get(field), (int, float))
|
322
|
+
]
|
148
323
|
return sum(values) / len(values) if values else 0
|
149
324
|
|
150
325
|
def min(self, field):
|
151
|
-
|
326
|
+
"""
|
327
|
+
Find the minimum value of a numeric field across all matching documents.
|
328
|
+
field -- The field to find the minimum of.
|
329
|
+
Returns the minimum value of the field.
|
330
|
+
If no documents match or the field is not numeric, returns None.
|
331
|
+
"""
|
332
|
+
values = [
|
333
|
+
doc.get(field)
|
334
|
+
for doc in self.run()
|
335
|
+
if isinstance(doc.get(field), (int, float))
|
336
|
+
]
|
152
337
|
return min(values) if values else None
|
153
338
|
|
154
339
|
def max(self, field):
|
155
|
-
|
340
|
+
"""
|
341
|
+
Find the maximum value of a numeric field across all matching documents.
|
342
|
+
field -- The field to find the maximum of.
|
343
|
+
Returns the maximum value of the field.
|
344
|
+
If no documents match or the field is not numeric, returns None.
|
345
|
+
"""
|
346
|
+
values = [
|
347
|
+
doc.get(field)
|
348
|
+
for doc in self.run()
|
349
|
+
if isinstance(doc.get(field), (int, float))
|
350
|
+
]
|
156
351
|
return max(values) if values else None
|
157
352
|
|
158
353
|
# Lookup
|
159
354
|
def lookup(self, foreign_collection_name, local_key, foreign_key, as_field):
|
355
|
+
"""
|
356
|
+
Perform a lookup to enrich documents with related data from another collection.
|
357
|
+
foreign_collection_name -- The name of the foreign collection to join with.
|
358
|
+
local_key -- The key in the local documents to match against the foreign collection.
|
359
|
+
foreign_key -- The key in the foreign documents to match against the local collection.
|
360
|
+
as_field -- The name of the field to add to the local documents with the joined data.
|
361
|
+
Returns self to allow method chaining.
|
362
|
+
"""
|
160
363
|
foreign_docs = self.all_collections.get(foreign_collection_name, [])
|
161
364
|
fk_map = {doc[foreign_key]: doc for doc in foreign_docs}
|
162
365
|
enriched = []
|
@@ -172,6 +375,11 @@ class QueryBuilder:
|
|
172
375
|
|
173
376
|
# Merge
|
174
377
|
def merge(self, fn):
|
378
|
+
"""
|
379
|
+
Merge the results of the query with additional data.
|
380
|
+
fn -- A function that takes a document and returns a dictionary of fields to update.
|
381
|
+
Returns self to allow method chaining.
|
382
|
+
"""
|
175
383
|
docs = self._lookup_results if self._lookup_done else self.run()
|
176
384
|
merged = []
|
177
385
|
for doc in docs:
|
@@ -183,12 +391,20 @@ class QueryBuilder:
|
|
183
391
|
return self
|
184
392
|
|
185
393
|
|
186
|
-
|
187
394
|
_collection_registry = {}
|
188
395
|
|
396
|
+
|
189
397
|
class CollectionManager:
|
398
|
+
"""
|
399
|
+
Manage a NoSQL collection, providing methods for querying and manipulating documents.
|
400
|
+
"""
|
190
401
|
|
191
402
|
def __init__(self, name: str, path: str = None):
|
403
|
+
"""
|
404
|
+
Initialize a collection manager for a NoSQL collection.
|
405
|
+
name -- The name of the collection.
|
406
|
+
path -- Optional path to a JSON file where the collection data is stored.
|
407
|
+
"""
|
192
408
|
self.name = name
|
193
409
|
self.in_memory = False
|
194
410
|
|
@@ -204,109 +420,249 @@ class CollectionManager:
|
|
204
420
|
_collection_registry[name] = self.documents
|
205
421
|
|
206
422
|
def _load(self):
|
423
|
+
"""
|
424
|
+
Load the collection data from the JSON file.
|
425
|
+
If the file does not exist, create an empty collection.
|
426
|
+
If in_memory is True, initialize an empty collection.
|
427
|
+
"""
|
207
428
|
if self.in_memory:
|
208
429
|
self.documents = []
|
209
430
|
else:
|
210
431
|
try:
|
211
|
-
with open(self.path,
|
432
|
+
with open(self.path, "r", encoding="utf-8") as f:
|
212
433
|
self.documents = json.load(f)
|
213
434
|
except FileNotFoundError:
|
214
435
|
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
215
436
|
self.documents = []
|
216
437
|
|
217
438
|
def _save(self):
|
439
|
+
"""
|
440
|
+
Save the collection data to the JSON file.
|
441
|
+
If in_memory is True, this method does nothing.
|
442
|
+
"""
|
218
443
|
if not self.in_memory:
|
219
|
-
with open(self.path,
|
444
|
+
with open(self.path, "w", encoding="utf-8") as f:
|
220
445
|
json.dump(self.documents, f, indent=4)
|
221
446
|
|
222
447
|
def add(self, document: dict):
|
448
|
+
"""
|
449
|
+
Add a document to the collection.
|
450
|
+
document -- The document to add, must be a dictionary.
|
451
|
+
Returns a dictionary with the count of inserted documents.
|
452
|
+
"""
|
223
453
|
self.documents.append(document)
|
224
454
|
self._save()
|
225
455
|
return {"inserted": 1}
|
226
456
|
|
227
457
|
def add_many(self, docs: list[dict]):
|
458
|
+
"""
|
459
|
+
Add multiple documents to the collection.
|
460
|
+
docs -- A list of documents to add, each must be a dictionary.
|
461
|
+
Returns a dictionary with the count of inserted documents.
|
462
|
+
"""
|
228
463
|
self.documents.extend(docs)
|
229
464
|
self._save()
|
230
465
|
return {"inserted": len(docs)}
|
231
466
|
|
232
467
|
def where(self, field):
|
233
|
-
|
234
|
-
|
468
|
+
"""
|
469
|
+
Start a query to filter documents based on a field.
|
470
|
+
field -- The field to filter on.
|
471
|
+
Returns a QueryBuilder instance to build the query.
|
472
|
+
"""
|
473
|
+
return QueryBuilder(self.documents, all_collections=_collection_registry).where(
|
474
|
+
field
|
475
|
+
)
|
476
|
+
|
235
477
|
def match_any(self, *conditions):
|
478
|
+
"""
|
479
|
+
Start a query to match any of the specified conditions.
|
480
|
+
conditions -- Functions that take a QueryBuilder instance and modify its filters.
|
481
|
+
Returns a QueryBuilder instance with the combined conditions.
|
482
|
+
"""
|
236
483
|
q = QueryBuilder(self.documents, all_collections=_collection_registry)
|
237
484
|
return q._or(*conditions)
|
238
485
|
|
239
486
|
def match_all(self, *conditions):
|
487
|
+
"""
|
488
|
+
Start a query to match all of the specified conditions.
|
489
|
+
conditions -- Functions that take a QueryBuilder instance and modify its filters.
|
490
|
+
Returns a QueryBuilder instance with the combined conditions.
|
491
|
+
"""
|
240
492
|
q = QueryBuilder(self.documents, all_collections=_collection_registry)
|
241
493
|
return q._and(*conditions)
|
242
494
|
|
243
495
|
def not_any(self, *conditions):
|
496
|
+
"""
|
497
|
+
Start a query to negate any of the specified conditions.
|
498
|
+
conditions -- Functions that take a QueryBuilder instance and modify its filters.
|
499
|
+
Returns a QueryBuilder instance with the negated conditions.
|
500
|
+
"""
|
244
501
|
q = QueryBuilder(self.documents, all_collections=_collection_registry)
|
245
502
|
return q._not(lambda nq: nq._or(*conditions))
|
246
|
-
|
503
|
+
|
247
504
|
def lookup(self, *args, **kwargs):
|
248
|
-
|
505
|
+
"""
|
506
|
+
Perform a lookup to enrich documents with related data from another collection.
|
507
|
+
args -- Positional arguments for the lookup.
|
508
|
+
kwargs -- Keyword arguments for the lookup.
|
509
|
+
Returns a QueryBuilder instance with the lookup applied.
|
510
|
+
"""
|
511
|
+
return QueryBuilder(
|
512
|
+
self.documents, all_collections=_collection_registry
|
513
|
+
).lookup(*args, **kwargs)
|
249
514
|
|
250
515
|
def merge(self, *args, **kwargs):
|
251
|
-
|
516
|
+
"""
|
517
|
+
Merge documents from another collection into this collection.
|
518
|
+
args -- Positional arguments for the merge.
|
519
|
+
kwargs -- Keyword arguments for the merge.
|
520
|
+
Returns a QueryBuilder instance with the merge applied.
|
521
|
+
"""
|
522
|
+
return QueryBuilder(self.documents, all_collections=_collection_registry).merge(
|
523
|
+
*args, **kwargs
|
524
|
+
)
|
252
525
|
|
253
526
|
def sum(self, field):
|
527
|
+
"""
|
528
|
+
Calculate the sum of a numeric field across all documents.
|
529
|
+
field -- The field to sum.
|
530
|
+
Returns the sum of the field values.
|
531
|
+
"""
|
254
532
|
return QueryBuilder(self.documents).sum(field)
|
255
533
|
|
256
534
|
def avg(self, field):
|
535
|
+
"""
|
536
|
+
Calculate the average of a numeric field across all documents.
|
537
|
+
field -- The field to average.
|
538
|
+
Returns the average of the field values.
|
539
|
+
"""
|
257
540
|
return QueryBuilder(self.documents).avg(field)
|
258
541
|
|
259
542
|
def min(self, field):
|
543
|
+
"""
|
544
|
+
Calculate the minimum of a numeric field across all documents.
|
545
|
+
field -- The field to find the minimum.
|
546
|
+
Returns the minimum of the field values.
|
547
|
+
"""
|
260
548
|
return QueryBuilder(self.documents).min(field)
|
261
549
|
|
262
550
|
def max(self, field):
|
551
|
+
"""
|
552
|
+
Calculate the maximum of a numeric field across all documents.
|
553
|
+
field -- The field to find the maximum.
|
554
|
+
Returns the maximum of the field values.
|
555
|
+
"""
|
263
556
|
return QueryBuilder(self.documents).max(field)
|
264
557
|
|
265
558
|
def count(self):
|
559
|
+
"""
|
560
|
+
Count the number of documents in the collection.
|
561
|
+
Returns the count of documents.
|
562
|
+
"""
|
266
563
|
return QueryBuilder(self.documents).count()
|
267
564
|
|
268
565
|
def first(self):
|
566
|
+
"""
|
567
|
+
Get the first document in the collection.
|
568
|
+
Returns the first document or None if the collection is empty.
|
569
|
+
"""
|
269
570
|
return QueryBuilder(self.documents).first()
|
270
571
|
|
271
572
|
def clear(self):
|
573
|
+
"""
|
574
|
+
Clear all documents from the collection.
|
575
|
+
Returns a dictionary with the count of cleared documents.
|
576
|
+
"""
|
272
577
|
count = len(self.documents)
|
273
578
|
self.documents = []
|
274
579
|
self._save()
|
275
580
|
return {"cleared": count}
|
276
581
|
|
277
582
|
def export(self, path):
|
278
|
-
|
583
|
+
"""
|
584
|
+
Export the collection to a JSON file.
|
585
|
+
path -- The file path to export the collection.
|
586
|
+
"""
|
587
|
+
with open(path, "w", encoding="utf-8") as f:
|
279
588
|
json.dump(self.documents, f, indent=4)
|
280
589
|
|
281
590
|
def import_(self, path):
|
282
|
-
|
591
|
+
"""
|
592
|
+
Import documents from a JSON file into the collection.
|
593
|
+
path -- The file path to import the collection from.
|
594
|
+
If the file does not exist, it raises a FileNotFoundError.
|
595
|
+
"""
|
596
|
+
with open(path, "r", encoding="utf-8") as f:
|
283
597
|
self.documents = json.load(f)
|
284
598
|
self._save()
|
285
599
|
|
286
|
-
def all(self):
|
287
|
-
|
600
|
+
def all(self):
|
601
|
+
"""
|
602
|
+
Get all documents in the collection.
|
603
|
+
Returns a list of all documents.
|
604
|
+
"""
|
605
|
+
return self.documents
|
288
606
|
|
289
607
|
def save(self, path: str):
|
290
|
-
|
608
|
+
"""
|
609
|
+
Save the current state of the collection to a JSON file.
|
610
|
+
path -- The file path to save the collection.
|
611
|
+
If the path does not end with .json, it raises a ValueError.
|
612
|
+
"""
|
613
|
+
if not path.endswith(".json"):
|
614
|
+
raise ValueError("Invalid file format. Please use a .json file.")
|
615
|
+
with open(path, "w", encoding="utf-8") as f:
|
291
616
|
json.dump(self.documents, f, indent=4)
|
292
|
-
|
617
|
+
|
293
618
|
def all_docs(self):
|
619
|
+
"""
|
620
|
+
Get all documents in the collection.
|
621
|
+
Returns a list of all documents.
|
622
|
+
"""
|
294
623
|
return self.documents
|
295
624
|
|
625
|
+
|
296
626
|
class DocList:
|
627
|
+
"""
|
628
|
+
A class to represent a list of documents with additional utility methods.
|
629
|
+
Provides methods to iterate, access by index, get length, and convert to JSON.
|
630
|
+
"""
|
631
|
+
|
297
632
|
def __init__(self, docs: list[dict]):
|
633
|
+
"""
|
634
|
+
Initialize the DocList with a list of documents.
|
635
|
+
docs -- A list of documents (dictionaries) to store in the DocList.
|
636
|
+
"""
|
298
637
|
self._docs = docs
|
299
638
|
|
300
639
|
def __iter__(self):
|
640
|
+
"""
|
641
|
+
Return an iterator over the documents in the DocList.
|
642
|
+
"""
|
301
643
|
return iter(self._docs)
|
302
644
|
|
303
645
|
def __getitem__(self, index):
|
646
|
+
"""
|
647
|
+
Get a document by index.
|
648
|
+
index -- The index of the document to retrieve.
|
649
|
+
Returns the document at the specified index.
|
650
|
+
"""
|
304
651
|
return self._docs[index]
|
305
652
|
|
306
653
|
def __len__(self):
|
654
|
+
"""
|
655
|
+
Get the number of documents in the DocList.
|
656
|
+
Returns the count of documents.
|
657
|
+
"""
|
307
658
|
return len(self._docs)
|
308
659
|
|
309
660
|
def __repr__(self):
|
661
|
+
"""
|
662
|
+
Return a string representation of the DocList.
|
663
|
+
If the DocList is empty, it returns "<empty result>".
|
664
|
+
Otherwise, it formats the documents as a table with headers.
|
665
|
+
"""
|
310
666
|
if not self._docs:
|
311
667
|
return "<empty result>"
|
312
668
|
keys = list(self._docs[0].keys())
|
@@ -319,8 +675,15 @@ class DocList:
|
|
319
675
|
return f"{header}\n{line}\n" + "\n".join(rows)
|
320
676
|
|
321
677
|
def to_json(self, path: str):
|
678
|
+
"""
|
679
|
+
Save the documents in the DocList to a JSON file.
|
680
|
+
path -- The file path to save the documents.
|
681
|
+
"""
|
322
682
|
with open(path, "w", encoding="utf-8") as f:
|
323
683
|
json.dump(self._docs, f, indent=4)
|
324
684
|
|
325
685
|
def as_list(self):
|
686
|
+
"""
|
687
|
+
Convert the DocList to a regular list of documents.
|
688
|
+
"""
|
326
689
|
return self._docs
|
coffy/sql/__init__.py
CHANGED
@@ -3,8 +3,15 @@
|
|
3
3
|
|
4
4
|
from .engine import execute_query, initialize
|
5
5
|
|
6
|
+
|
6
7
|
def init(path: str = None):
|
8
|
+
"""Initialize the SQL engine with the given path."""
|
7
9
|
initialize(path)
|
8
10
|
|
11
|
+
|
9
12
|
def query(sql: str):
|
13
|
+
"""Execute a SQL query and return the results."""
|
10
14
|
return execute_query(sql)
|
15
|
+
|
16
|
+
|
17
|
+
__all__ = ["init", "query", "execute_query", "initialize"]
|
Binary file
|