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/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
- keys = dotted_key.split('.')
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): return self._add_filter(lambda d: QueryBuilder._get_nested(d, self.current_field) == value)
32
- def ne(self, value): return self._add_filter(lambda d: QueryBuilder._get_nested(d, self.current_field) != value)
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(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) > value
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(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) >= value
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(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) < value
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(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) <= value
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): return self._add_filter(lambda d: re.search(regex, str(QueryBuilder._get_nested(d, self.current_field))))
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
- return self._add_filter(lambda d: QueryBuilder._get_nested(d, self.current_field) is not None)
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[:] = [doc for doc in self.documents if not all(f(doc) for f in self.filters)]
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): return len(self.run())
140
- def first(self): return next(iter(self.run()), None)
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
- return sum(doc.get(field, 0) for doc in self.run() if isinstance(doc.get(field), (int, float)))
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
- values = [doc.get(field) for doc in self.run() if isinstance(doc.get(field), (int, float))]
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
- values = [doc.get(field) for doc in self.run() if isinstance(doc.get(field), (int, float))]
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
- values = [doc.get(field) for doc in self.run() if isinstance(doc.get(field), (int, float))]
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, 'r', encoding='utf-8') as f:
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, 'w', encoding='utf-8') as f:
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
- return QueryBuilder(self.documents, all_collections=_collection_registry).where(field)
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
- return QueryBuilder(self.documents, all_collections=_collection_registry).lookup(*args, **kwargs)
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
- return QueryBuilder(self.documents, all_collections=_collection_registry).merge(*args, **kwargs)
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
- with open(path, 'w', encoding='utf-8') as f:
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
- with open(path, 'r', encoding='utf-8') as f:
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): return self.documents
287
- def count(self): return len(self.documents)
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
- with open(path, 'w', encoding='utf-8') as f:
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"]