sop4py 2.0.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.
sop/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+
2
+
3
+ __version__="2.0.0"
sop/btree.py ADDED
@@ -0,0 +1,612 @@
1
+ import json
2
+ import uuid
3
+ import logging
4
+ import call_go
5
+ import context
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ from typing import TypeVar, Generic, Type
10
+ from dataclasses import dataclass, asdict
11
+
12
+ from transaction import Transaction, TransactionError
13
+
14
+ # Define TypeVars 'TK' & 'TV' to represent Key & Value generic types.
15
+ TK = TypeVar("TK")
16
+ TV = TypeVar("TV")
17
+
18
+ from datetime import timedelta
19
+
20
+ from enum import Enum, auto
21
+
22
+
23
+ class ValueDataSize(Enum):
24
+ Small = 0
25
+ Medium = 1
26
+ Big = 2
27
+
28
+
29
+ class PagingDirection(Enum):
30
+ Forward = 0
31
+ Backward = 1
32
+
33
+
34
+ @dataclass
35
+ class PagingInfo:
36
+ """
37
+ Paging Info is used to package paging details to SOP.
38
+ """
39
+
40
+ # Offset defaults to 0, meaning, fetch from current cursor position in SOP backend.
41
+ page_offset: int = 0
42
+ # Fetch size of 20 is default.
43
+ page_size: int = 20
44
+ # Count of elements to fetch starting with the page offset. If left 0, 'will fetch PageSize number of elements
45
+ # after traversing the B-tree, bringing the cursor to the requested page offset.
46
+ # Otherwise, will fetch FetchCount number of data elements starting from the page offset.
47
+ fetch_count: int = 0
48
+ # Fetch direction of forward(0) is default.
49
+ direction: int = 0
50
+
51
+
52
+ @dataclass
53
+ class CacheConfig:
54
+ """
55
+ Cache config specify the options available for caching in B-tree.
56
+ """
57
+
58
+ # Registry cache duration in minutes.
59
+ registry_cache_duration: int = 10
60
+ is_registry_cache_ttl: bool = False
61
+ node_cache_duration: int = 5
62
+ is_node_cache_ttl: bool = False
63
+ store_info_cache_duration: int = 5
64
+ is_store_info_cache_ttl: bool = False
65
+ value_data_cache_duration: int = 0
66
+ is_value_data_cache_ttl: bool = False
67
+
68
+
69
+ @dataclass
70
+ class IndexFieldSpecification:
71
+ """
72
+ Make sure to specify correct field name so Key data comparer will be able to make a good comparison
73
+ between two Items when organizing Key/Value item pairs in the b-tree.
74
+
75
+ Otherwise it will treat all of item's to have the same key field value and thus, will affect both
76
+ performance and give incorrect ordering of the items.
77
+ """
78
+
79
+ field_name: str
80
+ ascending_sort_order: bool = True
81
+
82
+
83
+ @dataclass
84
+ class IndexSpecification:
85
+ """
86
+ Index Specification lists the fields comprising the index on the Key class & their sort order.
87
+ """
88
+
89
+ index_fields: IndexFieldSpecification = None
90
+
91
+
92
+ @dataclass
93
+ class BtreeOptions:
94
+ """
95
+ Btree options specify the options available for making a B-tree.
96
+ """
97
+
98
+ name: str
99
+ is_unique: bool = False
100
+ slot_length: int = 500
101
+ description: str = ""
102
+ is_value_data_in_node_segment: bool = True
103
+ is_value_data_actively_persisted: bool = False
104
+ is_value_data_globally_cached: bool = False
105
+ index_specification: str = ""
106
+ transaction_id: str = str(uuid.UUID(int=0))
107
+ cache_config: CacheConfig = None
108
+ is_primitive_key: bool = True
109
+ leaf_load_balancing: bool = False
110
+
111
+ def set_value_data_size(self, s: ValueDataSize):
112
+ if s == ValueDataSize.Medium:
113
+ self.is_value_data_actively_persisted = False
114
+ self.is_value_data_globally_cached = True
115
+ self.is_value_data_in_node_segment = False
116
+ if s == ValueDataSize.Big:
117
+ self.is_value_data_actively_persisted = True
118
+ self.is_value_data_globally_cached = False
119
+ self.is_value_data_in_node_segment = False
120
+
121
+
122
+ @dataclass
123
+ class Item(Generic[TK, TV]):
124
+ key: TK
125
+ value: TV = None
126
+ id: str = str(uuid.UUID(int=0))
127
+
128
+
129
+ class BtreeError(TransactionError):
130
+ """Exception for Btree-related errors, 'is derived from TransactionError."""
131
+
132
+ pass
133
+
134
+
135
+ @dataclass
136
+ class ManageBtreeMetaData(Generic[TK, TV]):
137
+ is_primitive_key: bool
138
+ transaction_id: str
139
+ btree_id: str
140
+
141
+
142
+ @dataclass
143
+ class ManageBtreePayload(Generic[TK, TV]):
144
+ items: Item[TK, TV]
145
+
146
+
147
+ class BtreeAction(Enum):
148
+ NewBtree = auto()
149
+ OpenBtree = auto()
150
+ Add = auto()
151
+ AddIfNotExist = auto()
152
+ Update = auto()
153
+ Upsert = auto()
154
+ Remove = auto()
155
+ Find = auto()
156
+ FindWithID = auto()
157
+ GetItems = auto()
158
+ GetValues = auto()
159
+ GetKeys = auto()
160
+ First = auto()
161
+ Last = auto()
162
+ IsUnique = auto()
163
+ Count = auto()
164
+ GetStoreInfo = auto()
165
+
166
+
167
+ class Btree(Generic[TK, TV]):
168
+ """
169
+ B-tree manager. See "new" & "open" class methods below for details how to use.
170
+ Delegates API calls to the SOP library that does Direct IO to disk drives w/ built-in L1/L2 caching.
171
+
172
+ Args:
173
+ Generic (TK, TV): TK - type of the Key part. TV - type of the Value part.
174
+ """
175
+
176
+ transaction_id: uuid.uuid4
177
+ id: uuid.uuid4
178
+
179
+ def __init__(self, id: uuid.uuid4, is_primitive_key: bool, tid: uuid.uuid4):
180
+ self.id = id
181
+ self.is_primitive_key = is_primitive_key
182
+ self.transaction_id = tid
183
+
184
+ @classmethod
185
+ def new(
186
+ cls: Type["Btree[TK,TV]"],
187
+ ctx: context.Context,
188
+ options: BtreeOptions,
189
+ trans: Transaction,
190
+ index_spec: IndexSpecification = None,
191
+ ) -> "Btree[TK,TV]":
192
+ """Create a new B-tree store in the backend storage with the options specified then returns an instance
193
+ of Python Btree (facade) that can let caller code to manage or search/fetch the items of the store.
194
+
195
+ Args:
196
+ cls (Type["Btree[TK,TV]"]): Supports generics for Key (TK) & Value (TV) pair.
197
+ ctx (context.Context): context.Context object, useful for telling SOP in the backend the ID of the context for use in calls.
198
+ options (BtreeOptions): _description_
199
+ trans (Transaction): instance of a Transaction that the B-tree store to be opened belongs.
200
+ index_spec (IndexSpecification, optional): Defaults to None.
201
+
202
+ Raises:
203
+ BtreeError: error message pertaining to creation of a b-tree store related in the backend.
204
+
205
+ Returns:
206
+ Btree[TK,TV]: B-tree instance
207
+ """
208
+
209
+ options.transaction_id = str(trans.transaction_id)
210
+ if index_spec is not None:
211
+ options.index_specification = json.dumps(asdict(index_spec))
212
+
213
+ res = call_go.manage_btree(
214
+ ctx.id, BtreeAction.NewBtree.value, json.dumps(asdict(options)), None
215
+ )
216
+
217
+ if res == None:
218
+ raise BtreeError("unable to create a Btree in SOP")
219
+ try:
220
+ b3id = uuid.UUID(res)
221
+ except:
222
+ # if res can't be converted to UUID, it is expected to be an error msg from SOP.
223
+ raise BtreeError(res)
224
+
225
+ return cls(b3id, options.is_primitive_key, trans.transaction_id)
226
+
227
+ @classmethod
228
+ def open(
229
+ cls: Type["Btree[TK,TV]"], ctx: context.Context, name: str, trans: Transaction
230
+ ) -> "Btree[TK,TV]":
231
+ """Open an existing B-tree store on the backend and returns a B-tree manager that can allow code to do operations on it.
232
+ Args:
233
+ cls (Type["Btree[TK,TV]"]): Supports generics for Key (TK) & Value (TV) pair.
234
+ ctx (context.Context): context.Context object, useful for telling SOP in the backend the ID of the context for use in calls.
235
+ name (str): Name of the B-tree store to open.
236
+ trans (Transaction): instance of a Transaction that the B-tree store to be opened belongs.
237
+
238
+ Raises:
239
+ BtreeError: error message generated when calling different methods of the B-tree.
240
+
241
+ Returns:
242
+ Btree[TK,TV]: Btree instance
243
+ """
244
+ options: BtreeOptions = BtreeOptions(name=name)
245
+ options.transaction_id = str(trans.transaction_id)
246
+ res = call_go.manage_btree(
247
+ ctx.id, BtreeAction.OpenBtree.value, json.dumps(asdict(options)), ""
248
+ )
249
+
250
+ if res == None:
251
+ raise BtreeError(f"unable to open a Btree:{name} in SOP")
252
+ try:
253
+ b3id = uuid.UUID(res)
254
+ except:
255
+ # if res can't be converted to UUID, it is expected to be an error msg from SOP.
256
+ raise BtreeError(res)
257
+
258
+ return cls(b3id, False, trans.transaction_id)
259
+
260
+ def add(self, ctx: context.Context, items: Item[TK, TV]) -> bool:
261
+ return self._manage(ctx, BtreeAction.Add.value, items)
262
+
263
+ def add_if_not_exists(self, ctx: context.Context, items: Item[TK, TV]) -> bool:
264
+ return self._manage(ctx, BtreeAction.AddIfNotExist.value, items)
265
+
266
+ def update(self, ctx: context.Context, items: Item[TK, TV]) -> bool:
267
+ return self._manage(ctx, BtreeAction.Update.value, items)
268
+
269
+ def upsert(self, ctx: context.Context, items: Item[TK, TV]) -> bool:
270
+ return self._manage(ctx, BtreeAction.Upsert.value, items)
271
+
272
+ def remove(self, ctx: context.Context, keys: TK) -> bool:
273
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
274
+ is_primitive_key=self.is_primitive_key,
275
+ btree_id=str(self.id),
276
+ transaction_id=str(self.transaction_id),
277
+ )
278
+ res = call_go.manage_btree(
279
+ ctx.id,
280
+ BtreeAction.Remove.value,
281
+ json.dumps(asdict(metadata)),
282
+ json.dumps(asdict(keys)),
283
+ )
284
+ if res == None:
285
+ raise BtreeError(
286
+ f"unable to remove item w/ key:{keys} from a Btree:{self.id} in SOP"
287
+ )
288
+ return self._to_bool(res)
289
+
290
+ def get_items(
291
+ self,
292
+ ctx: context.Context,
293
+ pagingInfo: PagingInfo,
294
+ ) -> Item[TK, TV]:
295
+ """Fetch items from the B-tree store.
296
+
297
+ Args:
298
+ ctx (context.Context): context.Context object
299
+ pagingInfo (PagingInfo): Paging details that describe how to navigate(walk the b-tree) & fetch a batch of items.
300
+
301
+ Returns: Item[TK, TV]: the fetched batch of items.
302
+ """
303
+ return self._get(ctx, BtreeAction.GetItems.value, pagingInfo)
304
+
305
+ # Keys array contains the keys & their IDs to fetch value data of. Both input & output
306
+ # are Item type though input only has Key & ID field populated to find the data & output has Value of found data.
307
+ def get_values(self, ctx: context.Context, keys: Item[TK, TV]) -> Item[TK, TV]:
308
+ """Fetch items' Value parts from the B-tree store.
309
+
310
+ Args:
311
+ ctx (context.Context): context.Context object
312
+ keys (Item[TK, TV]): Keys that which, their Value parts will be fetched from the store.
313
+
314
+ Raises:
315
+ BtreeError: error message containing details what failed during (Values) fetch.
316
+
317
+ Returns: Item[TK, TV]: items containing the fetched (Values) data.
318
+ """
319
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
320
+ is_primitive_key=self.is_primitive_key,
321
+ btree_id=str(self.id),
322
+ transaction_id=str(self.transaction_id),
323
+ )
324
+ payload: ManageBtreePayload = ManageBtreePayload(items=keys)
325
+ result, error = call_go.get_from_btree(
326
+ ctx.id,
327
+ BtreeAction.GetValues.value,
328
+ json.dumps(asdict(metadata)),
329
+ json.dumps(asdict(payload)),
330
+ )
331
+ if error is not None:
332
+ if result is None:
333
+ raise BtreeError(error)
334
+ else:
335
+ # just log the error since there are partial results we can return.
336
+ logger.error(error)
337
+
338
+ if result is None:
339
+ return None
340
+
341
+ data_dicts = json.loads(result)
342
+ return [Item[TK, TV](**data_dict) for data_dict in data_dicts]
343
+
344
+ def get_keys(
345
+ self,
346
+ ctx: context.Context,
347
+ pagingInfo: PagingInfo,
348
+ ) -> Item[TK, TV]:
349
+ """Fetch a set of Keys from the store.
350
+
351
+ Args:
352
+ ctx (context.Context): context.Context object used when calling the SOP b-tree methods.
353
+ pagingInfo (PagingInfo): Paging details specifying how to navigate/walk the b-tree and fetch keys.
354
+
355
+ Returns: Item[TK, TV]: items with the Keys populated with the fetched data.
356
+ """
357
+ return self._get(ctx, BtreeAction.GetKeys.value, pagingInfo)
358
+
359
+ def find(self, ctx: context.Context, key: TK) -> bool:
360
+ """Find will navigate the b-tree and stop when the matching item, its Key matches with the key param, is found.
361
+ Or a nearby item if there is no match found.
362
+ This positions the cursor so on succeeding call to one or the fetch methods, get_keys, get_values, get_items, will
363
+ fetch the data (items) starting from this cursor (item) position.
364
+
365
+ Args:
366
+ ctx (context.Context): _description_
367
+ key (TK): key of the item to search for.
368
+
369
+ Raises:
370
+ BtreeError: error message pertaining to the error encountered while searching for the item.
371
+
372
+ Returns: bool: true means the item with such key is found, false otherwise. If true is returned, the cursor is positioned
373
+ to the item with matching key, otherwise to an item with key similar to the requested key.
374
+ """
375
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
376
+ is_primitive_key=self.is_primitive_key,
377
+ btree_id=str(self.id),
378
+ transaction_id=str(self.transaction_id),
379
+ )
380
+ payload: ManageBtreePayload = ManageBtreePayload(items=(Item(key=key),))
381
+ res = call_go.navigate_btree(
382
+ ctx.id,
383
+ BtreeAction.Find.value,
384
+ json.dumps(asdict(metadata)),
385
+ json.dumps(asdict(payload)),
386
+ )
387
+ if res == None:
388
+ raise BtreeError(
389
+ f"unable to Find using key:{key} the item of a Btree:{self.id} in SOP"
390
+ )
391
+
392
+ return self._to_bool(res)
393
+
394
+ def find_with_id(self, ctx: context.Context, key: TK, id: uuid.uuid4) -> bool:
395
+ """Similar to Find but which, includes the ID of the item to search for. This is useful when there are duplicated (on key) items.
396
+ Code can then search for the right one despite having many items of same key, based on the item ID.
397
+
398
+ Args:
399
+ ctx (context.Context): _description_
400
+ key (TK): _description_
401
+ id (uuid.uuid4): _description_
402
+
403
+ Raises:
404
+ BtreeError: _description_
405
+
406
+ Returns:
407
+ bool: _description_
408
+ """
409
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
410
+ is_primitive_key=self.is_primitive_key,
411
+ btree_id=str(self.id),
412
+ transaction_id=str(self.transaction_id),
413
+ )
414
+ payload: ManageBtreePayload = ManageBtreePayload(items=(Item(key=key, id=id),))
415
+ res = call_go.navigate_btree(
416
+ ctx.id,
417
+ BtreeAction.FindWithID.value,
418
+ json.dumps(asdict(metadata)),
419
+ json.dumps(asdict(payload)),
420
+ )
421
+ if res == None:
422
+ raise BtreeError(
423
+ f"unable to Find using key:{key} & ID:{id} the item of a Btree:{self.id} in SOP"
424
+ )
425
+
426
+ return self._to_bool(res)
427
+
428
+ def first(
429
+ self,
430
+ ctx: context.Context,
431
+ ) -> bool:
432
+ """Navigate or positions the cursor to the first or top of the b-tree store.
433
+
434
+ Args:
435
+ ctx (context.Context): _description_
436
+
437
+ Raises:
438
+ BtreeError: _description_
439
+
440
+ Returns:
441
+ bool: _description_
442
+ """
443
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
444
+ is_primitive_key=self.is_primitive_key,
445
+ btree_id=str(self.id),
446
+ transaction_id=str(self.transaction_id),
447
+ )
448
+ res = call_go.navigate_btree(
449
+ ctx.id, BtreeAction.First.value, json.dumps(asdict(metadata)), None
450
+ )
451
+ if res == None:
452
+ raise BtreeError(f"First call failed for Btree:{self.id} in SOP")
453
+
454
+ return self._to_bool(res)
455
+
456
+ def last(
457
+ self,
458
+ ctx: context.Context,
459
+ ) -> bool:
460
+ """Navigate or positions the cursor to the last or bottom of the b-tree store.
461
+
462
+ Args:
463
+ ctx (context.Context): _description_
464
+
465
+ Raises:
466
+ BtreeError: _description_
467
+
468
+ Returns:
469
+ bool: _description_
470
+ """
471
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
472
+ is_primitive_key=self.is_primitive_key,
473
+ btree_id=str(self.id),
474
+ transaction_id=str(self.transaction_id),
475
+ )
476
+ res = call_go.navigate_btree(
477
+ ctx.id, BtreeAction.Last.value, json.dumps(asdict(metadata)), None
478
+ )
479
+ if res == None:
480
+ raise BtreeError(f"Last call failed for Btree:{self.id} in SOP")
481
+
482
+ return self._to_bool(res)
483
+
484
+ def is_unique(self) -> bool:
485
+ """true specifies that the b-tree store has no duplicated keyed items. false otherwise.
486
+
487
+ Raises:
488
+ BtreeError: _description_
489
+
490
+ Returns:
491
+ bool: _description_
492
+ """
493
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
494
+ is_primitive_key=self.is_primitive_key,
495
+ btree_id=str(self.id),
496
+ transaction_id=str(self.transaction_id),
497
+ )
498
+ res = call_go.is_unique_btree(
499
+ json.dumps(asdict(metadata)),
500
+ )
501
+ if res == None:
502
+ raise BtreeError(f"IsUnique call failed for Btree:{self.id} in SOP")
503
+
504
+ return self._to_bool(res)
505
+
506
+ def count(self) -> int:
507
+ """Returns the count of items in the b-tree store.
508
+
509
+ Raises:
510
+ BtreeError: _description_
511
+
512
+ Returns:
513
+ int: _description_
514
+ """
515
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
516
+ is_primitive_key=self.is_primitive_key,
517
+ btree_id=str(self.id),
518
+ transaction_id=str(self.transaction_id),
519
+ )
520
+ count, error = call_go.get_btree_item_count(
521
+ json.dumps(asdict(metadata)),
522
+ )
523
+ if error is not None:
524
+ raise BtreeError(error)
525
+ return count
526
+
527
+ def get_store_info(self) -> BtreeOptions:
528
+ """Returns the b-tree information as specified during creation (Btree.new method call).
529
+
530
+ Raises:
531
+ BtreeError: _description_
532
+
533
+ Returns:
534
+ BtreeOptions: _description_
535
+ """
536
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
537
+ is_primitive_key=self.is_primitive_key,
538
+ btree_id=str(self.id),
539
+ transaction_id=str(self.transaction_id),
540
+ )
541
+ payload, error = call_go.get_from_btree(
542
+ 0, BtreeAction.GetStoreInfo.value, json.dumps(asdict(metadata)), None
543
+ )
544
+ if error is not None:
545
+ raise BtreeError(error)
546
+
547
+ data_dict = json.loads(payload)
548
+ return BtreeOptions(**data_dict)
549
+
550
+ def _manage(self, ctx: context.Context, action: int, items: Item[TK, TV]) -> bool:
551
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
552
+ is_primitive_key=self.is_primitive_key,
553
+ btree_id=str(self.id),
554
+ transaction_id=str(self.transaction_id),
555
+ )
556
+ payload: ManageBtreePayload = ManageBtreePayload(items=items)
557
+ res = call_go.manage_btree(
558
+ ctx.id,
559
+ action,
560
+ json.dumps(asdict(metadata)),
561
+ json.dumps(asdict(payload)),
562
+ )
563
+ if res == None:
564
+ raise BtreeError(f"unable to manage item to a Btree:{self.id} in SOP")
565
+
566
+ return self._to_bool(res)
567
+
568
+ def _get(
569
+ self,
570
+ ctx: context.Context,
571
+ getAction: int,
572
+ pagingInfo: PagingInfo,
573
+ ) -> Item[TK, TV]:
574
+ if pagingInfo.direction > PagingDirection.Backward.value:
575
+ pagingInfo.direction = PagingDirection.Backward.value
576
+ if pagingInfo.direction < PagingDirection.Forward.value:
577
+ pagingInfo.direction = PagingDirection.Forward.value
578
+
579
+ metadata: ManageBtreeMetaData = ManageBtreeMetaData(
580
+ is_primitive_key=self.is_primitive_key,
581
+ btree_id=str(self.id),
582
+ transaction_id=str(self.transaction_id),
583
+ )
584
+ result, error = call_go.get_from_btree(
585
+ ctx.id,
586
+ getAction,
587
+ json.dumps(asdict(metadata)),
588
+ json.dumps(asdict(pagingInfo)),
589
+ )
590
+ if error is not None:
591
+ if result is None:
592
+ raise BtreeError(error)
593
+ else:
594
+ # just log the error since there are partial results we can return.
595
+ logger.error(error)
596
+
597
+ if result is None:
598
+ return None
599
+
600
+ data_dicts = json.loads(result)
601
+ return [Item[TK, TV](**data_dict) for data_dict in data_dicts]
602
+
603
+ def _to_bool(self, res: str) -> bool:
604
+ """
605
+ Converts result string "true" or "false" to bool or an errmsg to raise an error.
606
+ """
607
+ if res.lower() == "true":
608
+ return True
609
+ if res.lower() == "false":
610
+ return False
611
+
612
+ raise BtreeError(res)