kisa-utils 0.41.0__py3-none-any.whl → 0.42.1__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.
kisa_utils/db.py CHANGED
@@ -20,7 +20,8 @@ if sqlite3.sqlite_version_info[1]<38:
20
20
  sys.exit(f'we need sqlite3 v3.38.0+ to run this program. current version::{sqlite3.sqlite_version}')
21
21
 
22
22
  MAX_FETCH_ITEMS = 16*1024
23
- RETURN_KISA_RESPONSES = False # ensure all Handles return KISA-Responses globally
23
+ RETURN_KISA_RESPONSES:bool = False # ensure all Handles return KISA-Responses globally
24
+ PARSE_DICTS_TO_KDICTS:bool = True # ensure all dicts parsed on return data are KDicts
24
25
 
25
26
  __EXT__ = 'sqlite3'
26
27
 
@@ -56,6 +57,7 @@ TRIGGERS:dict = {
56
57
  }
57
58
 
58
59
  JSON_SEPARATOR = '::'
60
+ JSON_ARRAY_APPEND_SYMBOL = '+' # arr[+]=N => arr.append(N)
59
61
 
60
62
  RAM_DB_PATHS:list[str] = [':memory:', ':ram:',':RAM:']
61
63
 
@@ -508,24 +510,116 @@ class Api:
508
510
 
509
511
  return tables
510
512
 
511
- def __formRootPathFields(self, root:dict, keys:list, value:Any):
513
+ def __getJSONListIndices(self, jsonPathSection:str) -> Response:
514
+ '''
515
+ get key and indices of a json path
516
+ Returns:
517
+ Response.data ->
518
+ ```
519
+ {
520
+ 'key': str, # if array is a key in a dict, then this is set, otherwise its an empty string
521
+ 'indices': list[int] # contains all indices
522
+ }
523
+ ```
524
+ '''
525
+ data = KDict({
526
+ 'key': '',
527
+ 'indices': [],
528
+ })
529
+
530
+ if '[' not in jsonPathSection:
531
+ return Ok(data)
532
+
533
+ if jsonPathSection.count('[') != jsonPathSection.count(']') or jsonPathSection.index('[') > jsonPathSection.index(']'):
534
+ return Error(f'malformed array indexing found: `{jsonPathSection}`')
535
+
536
+ targetIndex = jsonPathSection.index('[')
537
+ data.key = jsonPathSection[:targetIndex].strip()
538
+
539
+ section = jsonPathSection[targetIndex:]
540
+ while '[' in section:
541
+ if section.count('[') != section.count(']') or section.index('[') > section.index(']'):
542
+ return Error(f'malformed array indexing found: `{jsonPathSection}`')
543
+
544
+ startIndex, endIndex = section.index('['), section.index(']')
545
+
546
+ try:
547
+ index = section[startIndex+1:endIndex]
548
+ index = int(index)
549
+ except:
550
+ index = index.strip()
551
+ if index != JSON_ARRAY_APPEND_SYMBOL:
552
+ return Error(f'malformed array index found: `{jsonPathSection}`->`{section[startIndex+1:endIndex]}`')
553
+
554
+ if JSON_ARRAY_APPEND_SYMBOL in data.indices:
555
+ return Error(f'index `{JSON_ARRAY_APPEND_SYMBOL}` should be the last index: `{jsonPathSection}`->`{section[startIndex+1:endIndex]}`')
556
+
557
+ data.indices.append(index)
558
+
559
+ section = section[endIndex+1:]
560
+
561
+ return Ok(data)
562
+
563
+ def __formRootPathFields(self, root:dict|list, keys:list, value:Any) -> Response:
512
564
  for key in keys[:-1]:
565
+ if not (resp := self.__getJSONListIndices(key)):
566
+ return resp
513
567
 
514
- if key.endswith("[-1]"):
515
- key = key[:-4]
516
- root = root.setdefault(key, [])
517
- else:
568
+ if not resp.data.indices: # dict object
518
569
  root = root.setdefault(key, {})
570
+ else: # list object
571
+ if resp.data.key and isinstance(root, dict):
572
+ root = root.setdefault(resp.data.key, [])
573
+
574
+ if not isinstance(root, list):
575
+ return Error(f'indexing non-array: `{key}`')
576
+
577
+ while resp.data.indices:
578
+ _index = index = resp.data.indices.pop(0)
579
+ if _index == JSON_ARRAY_APPEND_SYMBOL:
580
+ return Error(f'append index `{_index}` should be at the end: `{key}`, index `{_index}({index})`')
581
+ if index < 0:
582
+ index = len(root) + index
583
+
584
+ if not (0<= index < len(root)):
585
+ return Error(f'array-index out of bounds: `{key}`, index `{_index}({index})`')
519
586
 
520
- if keys[-1].endswith("[-1]"):
587
+ root = root[index]
521
588
 
522
- key = keys[-1][:-4]
523
- if key not in root:
524
- root[key] = []
525
-
526
- root[key].append(value)
527
- else:
528
- root[keys[-1]] = value
589
+ key = keys[-1]
590
+
591
+ if not (resp := self.__getJSONListIndices(key)):
592
+ return resp
593
+
594
+ if not resp.data.indices: # dict object
595
+ root[key] = value
596
+ else: # list object
597
+ if resp.data.key and isinstance(root, dict):
598
+ root = root.setdefault(resp.data.key, [])
599
+
600
+ if not isinstance(root, list):
601
+ return Error(f'indexing non-array: `{key}`')
602
+
603
+ while len(resp.data.indices):
604
+ _index = index = resp.data.indices.pop(0)
605
+ if _index != JSON_ARRAY_APPEND_SYMBOL:
606
+ if index < 0:
607
+ index = len(root) + index
608
+
609
+ if not (0<= index < len(root)):
610
+ return Error(f'array-index out of bounds: `{key}`, index `{_index}({index})`')
611
+ elif resp.data.indices:
612
+ return Error(f'append index `{_index}` should be at the end: `{key}`, index `{_index}({index})`')
613
+
614
+ if resp.data.indices:
615
+ root = root[index]
616
+ else:
617
+ if _index != JSON_ARRAY_APPEND_SYMBOL:
618
+ root[index] = value
619
+ else:
620
+ root.append(value)
621
+
622
+ return Ok()
529
623
 
530
624
  def __getRootAndPath(self, string, separator:str=JSON_SEPARATOR):
531
625
  index = string.index(separator)
@@ -617,7 +711,8 @@ class Api:
617
711
  limit:int=MAX_FETCH_ITEMS, returnDicts:bool=False,
618
712
  returnNamespaces:bool=False, parseJson:bool=False,
619
713
  returnGenerator:bool=False,
620
- useKDicts:bool=True,
714
+ useKDicts:bool|None=None,
715
+ offset:int = 0,
621
716
  ) -> list|Response:
622
717
  '''
623
718
  attempt to fetch from the database
@@ -634,6 +729,7 @@ class Api:
634
729
  parseJson(bool):if `True`, we shall parse json objects to python lists and dictionaries where possible
635
730
  returnGenerator(bool): if True, a generator will be returned instead of the list of tuple|dict|SimpleNamespace. this is especially recommended for large data
636
731
  useKDicts(bool): if True, all dicts returned are `KDicts` ie dicts that support the dot notation, just like JS objects
732
+ offset(int): how many rows to skip. along with `limit`, this allows pagination of results
637
733
  '''
638
734
  if not (limit>0 and limit<=MAX_FETCH_ITEMS):
639
735
  err = f'please set a limit on the returned rows. maximum should be {MAX_FETCH_ITEMS}'
@@ -641,6 +737,14 @@ class Api:
641
737
  raise ValueError(err)
642
738
  return Error(err)
643
739
 
740
+ if not isinstance(offset, int) or offset < 0:
741
+ err = f'invalid offset given. expected int >= 0'
742
+ if not self.__returnKISAResponse:
743
+ raise ValueError(err)
744
+ return Error(err)
745
+
746
+ useKDicts = PARSE_DICTS_TO_KDICTS if None==useKDicts else useKDicts
747
+
644
748
  condition = condition.strip() or '1'
645
749
  if not len(condition.strip()):
646
750
  err = 'no condition provided'
@@ -660,7 +764,7 @@ class Api:
660
764
  # columns = [self.__formatDBJson(_) for _ in columns]
661
765
  columns = [self.__formatJSONCondition(_) for _ in columns]
662
766
 
663
- __SQL_stmt = f"select {','.join(columns)} from {table} where {condition} limit {limit}"
767
+ __SQL_stmt = f"select {','.join(columns)} from {table} where {condition} limit {limit} offset {offset}"
664
768
  # print(f'<{__SQL_stmt}>')
665
769
 
666
770
  if not self.__returnKISAResponse:
@@ -857,6 +961,7 @@ class Api:
857
961
 
858
962
  rootResult = rootResult[0]
859
963
  rootResult = storage.decodeJSON(rootResult)
964
+ # print('>>> ',rootResult,'<<<')
860
965
 
861
966
  if isinstance(rootResult, list):
862
967
  if self.__transactionMode: self.__transactionData['update']['failed'] += 1
@@ -868,7 +973,9 @@ class Api:
868
973
 
869
974
  json_roots[root] = rootResult
870
975
 
871
- self.__formRootPathFields(json_roots[root], path.split(JSON_SEPARATOR), columnData[index])
976
+ if not (resp := self.__formRootPathFields(json_roots[root], path.split(JSON_SEPARATOR), columnData[index])):
977
+ reply['log'] = resp.log
978
+ return reply if not self.__returnKISAResponse else resp
872
979
 
873
980
  _columns.append(f'{root}=?')
874
981
 
@@ -876,6 +983,7 @@ class Api:
876
983
  columns = _columns
877
984
  values += conditionData
878
985
 
986
+
879
987
  if not condition:
880
988
  if self.__transactionMode: self.__transactionData['update']['failed'] += 1
881
989
  reply['log'] = 'please provide an update condition. use `1` if you want all data updated'
@@ -0,0 +1,2 @@
1
+ from . import callables
2
+ from . import persistent
@@ -0,0 +1,206 @@
1
+ from kisa_utils.db import Handle
2
+ from kisa_utils import db
3
+ from kisa_utils.queues.callables import queueCallsInThreads
4
+ from kisa_utils.response import Response, Error, Ok
5
+ from kisa_utils.dataStructures import KDict
6
+ from kisa_utils.storage import Path
7
+ from kisa_utils import dates
8
+ from kisa_utils.functionUtils import enforceRequirements
9
+ from kisa_utils.structures.utils import Value
10
+
11
+ db.RETURN_KISA_RESPONSES = True
12
+
13
+ class __PersistentQueueSingleton(type):
14
+ _instances = {}
15
+
16
+ def __call__(cls, *args, **kwargs):
17
+ if not args:
18
+ raise Exception(f'invalid instantiation of {cls}')
19
+ id = args[0]
20
+ storageLocation = kwargs.get('storageLocation', '')
21
+
22
+ if not id.isalnum():
23
+ raise ValueError('persistent queue ID should be an alphanumeric value ie a-zA-Z0-9 with no special characters or spaces')
24
+
25
+ idKey = f'{storageLocation}:{id}'
26
+
27
+ if idKey not in cls._instances:
28
+ cls._instances[idKey] = super().__call__(*args, **kwargs)
29
+
30
+ return cls._instances[idKey]
31
+
32
+
33
+ class PersistentQueue(metaclass=__PersistentQueueSingleton):
34
+ __openedQueues:dict = {} # name: PersistentQueue
35
+
36
+ __allowedDataTypes = str|int|float|dict|KDict|list|tuple|set
37
+
38
+ __dataTypeCasts = {
39
+ 'str': str,
40
+ 'int': int,
41
+ 'float': float,
42
+ 'dict': dict,
43
+ 'KDict': KDict,
44
+ 'list': list,
45
+ 'tuple': tuple,
46
+ 'set': set
47
+ }
48
+
49
+ __defaultDirectoryName:str = '.kisaPersistentQueues'
50
+ __defaultStorageLocation:str = Path.join(Path.HOME, __defaultDirectoryName)
51
+
52
+ __schema = {
53
+ 'data': '''
54
+ tstamp varchar(30) not null,
55
+ dataType varcahr(30) not null,
56
+ data json not null
57
+ ''',
58
+ }
59
+
60
+
61
+ def __init__(self, id:str, /, *, storageLocation:str=''):
62
+ '''
63
+ create a new thread-safe PersistentQueue
64
+ Args:
65
+ id(str): the Queue ID. queue sharing an ID will all reference the same underwlying queue
66
+ storageLocation(str): if provided, the location to store the queue. if not provided, data is stored in the default location. use `obj.defaultStorageLocation` to get the default storage directory
67
+ '''
68
+
69
+ if storageLocation:
70
+ if not Path.exists(storageLocation):
71
+ raise Exception(f'could not find storage directory: `{storageLocation}`')
72
+ else:
73
+ storageLocation = self.__defaultStorageLocation
74
+ if not Path.exists(storageLocation) and not Path.createDirectory(storageLocation):
75
+ raise Exception(f'failed to create storage directory: `{storageLocation}`')
76
+
77
+ self.__length:int = 0
78
+ self.id = id
79
+
80
+ self.dbPath = Path.join(storageLocation, self.id)
81
+
82
+ # ensure thread safety throughout the running program
83
+ self.append = queueCallsInThreads(self.append, group=self.id)
84
+ self.peek = queueCallsInThreads(self.peek, group=self.id)
85
+ self.pop = queueCallsInThreads(self.pop, group=self.id)
86
+
87
+ self.__load__ = queueCallsInThreads(self.__load__, group = "pqueue__load__")
88
+
89
+ if not (resp := self.__load__()):
90
+ raise Exception(f'P-QUEUE DB LOAD ERROR: {resp.log}')
91
+
92
+ @property
93
+ def defaultStorageLocation(self):
94
+ '''
95
+ get the default storage location
96
+ '''
97
+ return self.__defaultStorageLocation
98
+
99
+ @property
100
+ def allowedDataTypes(self):
101
+ '''
102
+ get data types allowed in the queue
103
+ '''
104
+ return self.__allowedDataTypes
105
+
106
+ @property
107
+ def length(self) -> int:
108
+ '''
109
+ get number of items in the queue
110
+ '''
111
+ return self.__length
112
+
113
+ def __load__(self) -> Response:
114
+ '''
115
+ load underlying queue database
116
+ '''
117
+
118
+ with Handle(self.dbPath, tables=self.__schema) as handle:
119
+ if not (resp := handle.fetch('data', ['count(*)'],'',[])):
120
+ return resp
121
+
122
+ self.__length = resp.data[0][0]
123
+
124
+ return Ok()
125
+
126
+ def __resolveIndex(self, index:int) -> Response:
127
+ '''
128
+ resolve index and determine if its legitimate
129
+ '''
130
+ if index<0:
131
+ index = self.__length + index
132
+
133
+ if not (0 <= index < self.__length):
134
+ return Error(f'invalid index given for persistent queue of length {self.__length}')
135
+
136
+ return Ok(index)
137
+
138
+ @enforceRequirements
139
+ def append(self, data:__allowedDataTypes, /) -> Response:
140
+ '''
141
+ append to the queue
142
+ Args:
143
+ data(__allowedDataTypes): the data to insert, use `obj.allowedDataTypes` to get the list of allowed data types
144
+ '''
145
+
146
+ try:
147
+ dataType = type(data).__name__
148
+ except:
149
+ return Error(f'failed to get data-type of `{data}`')
150
+
151
+
152
+ with Handle(self.dbPath, readonly=False) as handle:
153
+ if not (resp := handle.insert('data', [
154
+ dates.currentTimestamp(),
155
+ dataType,
156
+ data
157
+ ])):
158
+ return resp
159
+
160
+ self.__length += 1
161
+ return Ok(self.__length)
162
+
163
+ def peek(self, index:int, /) -> Response:
164
+ '''
165
+ get data at `index` without popping it from the queue
166
+ '''
167
+ if not (resp := self.__resolveIndex(index)): return resp
168
+ index = resp.data
169
+
170
+ with Handle(self.dbPath) as handle:
171
+ if not (resp := handle.fetch('data', ['dataType', 'data'],'',[],limit=1, offset=index, parseJson=True)):
172
+ return resp
173
+
174
+ dataType, data = resp.data[0]
175
+
176
+ try:
177
+ data = self.__dataTypeCasts[dataType](data)
178
+ except:
179
+ return Error(f'failed to convert value to data-type `{dataType}`. the database was most likely corrupted')
180
+
181
+ return Ok(data)
182
+
183
+ def pop(self, /, *, index:int = 0) -> Response:
184
+ '''
185
+ get data at `index` and remove it from the queue
186
+ '''
187
+ if not (resp := self.__resolveIndex(index)): return resp
188
+ index = resp.data
189
+
190
+ with Handle(self.dbPath, readonly=False) as handle:
191
+ if not (resp := handle.fetch('data', ['rowid','dataType', 'data'],'',[],limit=1, offset=index, parseJson=True)):
192
+ return resp
193
+
194
+ rowId, dataType, data = resp.data[0]
195
+
196
+ try:
197
+ data = self.__dataTypeCasts[dataType](data)
198
+ except:
199
+ return Error(f'failed to convert value to data-type `{dataType}`. the database was most likely corrupted')
200
+
201
+ if not (resp := handle.delete('data','rowid=?',[rowId])):
202
+ return resp
203
+
204
+ self.__length -= 1
205
+
206
+ return Ok(data)
kisa_utils/storage.py CHANGED
@@ -5,7 +5,15 @@ from kisa_utils.response import Response, Error, Ok
5
5
  encodeJSON = json.JSONEncoder().encode
6
6
  decodeJSON = json.JSONDecoder().decode
7
7
 
8
- class Path:
8
+ class _Meta(type):
9
+ @property
10
+ def HOME(cls):
11
+ '''
12
+ get user's home directory
13
+ '''
14
+ return os.path.expanduser('~')
15
+
16
+ class Path(metaclass=_Meta):
9
17
  join = os.path.join
10
18
  directoryName = os.path.dirname
11
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kisa-utils
3
- Version: 0.41.0
3
+ Version: 0.42.1
4
4
  Summary: Utility functions and modules for KISA Developers
5
5
  Author: Tom Bukenya
6
6
  Author-email: glayn2bukman@gmail.com
@@ -4,7 +4,7 @@ kisa_utils/codes.py,sha256=PV_S53Skggf4XetOdYoIKtEmM8cpN5wZwUlxje70WZY,904
4
4
  kisa_utils/config.py,sha256=NfluzGKTh66qfNtC-Ae0zNb1XzMTgU2Me9Vi82R9c1E,2285
5
5
  kisa_utils/dataStructures.py,sha256=ZgLpttJ66jfpU1NWzLDD1Czqxzj6sWereffgTQWhlV8,2679
6
6
  kisa_utils/dates.py,sha256=zxe4n0PdKReZjK5ZkvnCZtJ55lk5oqu9oS8VX_nLozw,13966
7
- kisa_utils/db.py,sha256=oUpqpew3a1a69Ow1xSydfB1S_QhiUrCtba_7dbK0XBs,49715
7
+ kisa_utils/db.py,sha256=hWxkW21lgViOqFijxL4cD-Wpt4koWz6jzcE1v0IiT1c,54341
8
8
  kisa_utils/encryption.py,sha256=nFzNpzWV_D9uSEq4FsgCnlS7FQtqWP9fvM_81rsfcLo,4218
9
9
  kisa_utils/figures.py,sha256=pYIpQzu1OXRSsY1d98GhgPifnIRmgl-r7S32ai-Ms0c,3731
10
10
  kisa_utils/functionUtils.py,sha256=PlXjnmU1uJWNdISlJJ3SCgavTsgNBoebaa9dtWSFhRA,6553
@@ -12,12 +12,13 @@ kisa_utils/log.py,sha256=0TYdxcIBts026RCSuVIQBcZ-CW1ES7n3M1nEIjmeLTM,2295
12
12
  kisa_utils/remote.py,sha256=0RDrfC4RUW4m6JLziC0_EXJYqzWp38Rw8NDroJ0MuqI,2149
13
13
  kisa_utils/response.py,sha256=asETUBkeF5OlSTwa-coa7lZDCKmQlHCmHf6eaZFl8CU,4560
14
14
  kisa_utils/standardize.py,sha256=nt-uzHQFoKxGscD_MpDYXw65Teg3724whAqa6Kh_zhE,2231
15
- kisa_utils/storage.py,sha256=waHLmrf19QxvEQZlWlC2qQvEYFFhDeJHc7MMNbcmj7c,5823
15
+ kisa_utils/storage.py,sha256=6NdEVrHMS7WB_vmCwiGigIinu-EjxalFJhk1kj-_vWs,5990
16
16
  kisa_utils/threads.py,sha256=qQqsf64YHMyLpboq5AEXKxYqf3iXUhxiJe6Ymg-vlxI,12840
17
17
  kisa_utils/token.py,sha256=Y2qglWYWpmHxoXBh-TH0r1as0uPV5LLqMNcunLvM4vM,7850
18
18
  kisa_utils/permissions/__config__.py,sha256=i3ELkOydDnjKx2ozQTxLZdZ8DXSeUncnl2kRxANjFmM,613
19
19
  kisa_utils/permissions/__init__.py,sha256=q7LGl26f-MPXkLS6nxBKDotW3xdB8y7pI5S_Oo5fPOw,47976
20
- kisa_utils/queues/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ kisa_utils/queues/__init__.py,sha256=VvhceyN5qeiMel1JFQwLRuVk48oBXaWvDtriCubDOms,48
21
+ kisa_utils/queues/persistent.py,sha256=XTXMu7Q7DMizHIgB3PKrW9iq0PA0ftQa7080j3tCwN0,6668
21
22
  kisa_utils/queues/callables/__init__.py,sha256=OJL3AQnaAS1Eek4H6WBH3WefA2wf-x03cwFmRSK8hoU,141
22
23
  kisa_utils/queues/callables/enqueueFunctionCalls.py,sha256=VIliaMvw4MUdOqts0dXdZCYNxs-QrOVjIRAR3scGrRM,11786
23
24
  kisa_utils/queues/callables/executorQueues.py,sha256=x6bAqxBSZRZ_kL8CK1lSN6JYAYFLxzM84LC1RmwaOLw,6626
@@ -26,7 +27,7 @@ kisa_utils/servers/flask.py,sha256=XZYY1pWnP1mSvaS5Uv8G3EFJV5BJBQtU2gDbO8suvLc,4
26
27
  kisa_utils/structures/__init__.py,sha256=JBU1j3A42jQ62ALKnsS1Hav9YXcYwjDw1wQJtohXPbU,83
27
28
  kisa_utils/structures/utils.py,sha256=665rXIapGwFqejizeJwy3DryeskCQOdgP25BCdLkGvk,2898
28
29
  kisa_utils/structures/validator.py,sha256=JhD9jcfbjTwBr_7OfuNaJd_cYr7wR2emFhsCEo5MCHQ,4323
29
- kisa_utils-0.41.0.dist-info/METADATA,sha256=rtGcPNxF8RSBW5avDpByGXLIuquskvSJ0n9Ij84-EqM,477
30
- kisa_utils-0.41.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
31
- kisa_utils-0.41.0.dist-info/top_level.txt,sha256=GFOLXZYqpBG9xtscGa2uGJAEiZ5NwsqHBH9NylnB29M,11
32
- kisa_utils-0.41.0.dist-info/RECORD,,
30
+ kisa_utils-0.42.1.dist-info/METADATA,sha256=PmeuHNl4KeA7DIqDWMUe40jTqtxO-yTUxzSDlqxp1pU,477
31
+ kisa_utils-0.42.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
32
+ kisa_utils-0.42.1.dist-info/top_level.txt,sha256=GFOLXZYqpBG9xtscGa2uGJAEiZ5NwsqHBH9NylnB29M,11
33
+ kisa_utils-0.42.1.dist-info/RECORD,,