osbot-utils 2.21.0__py3-none-any.whl → 2.23.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.
@@ -41,6 +41,7 @@ class Flow(Type_Safe):
41
41
  executed_tasks : typing.List
42
42
  resolved_args : tuple
43
43
  resolved_kwargs : dict
44
+ setup_completed : bool
44
45
 
45
46
  def add_flow_artifact(self, description=None, key=None, data=None, artifact_type=None): # todo: figure out how to make this work since at the moment most are showing an unknown type
46
47
  event_data = Flow_Run__Event_Data()
@@ -64,6 +65,10 @@ class Flow(Type_Safe):
64
65
  flow_events.on__new_result(event_data)
65
66
  self.flow_data.add_result(key, description)
66
67
 
68
+ def check_setup(self):
69
+ if self.setup_completed is False:
70
+ raise ValueError("Error starting Flow, setup has not been called, use .setup(target, *args, **kwargs) to configure it")
71
+
67
72
  def config_logger(self):
68
73
  with self.logger as _:
69
74
  _.set_log_level(logging.DEBUG)
@@ -75,6 +80,7 @@ class Flow(Type_Safe):
75
80
  return self.execute_flow()
76
81
 
77
82
  def execute_flow(self, flow_run_params=None): # todo: see if it makes more sense to call this start_flow_run
83
+ self.check_setup()
78
84
  flow_events.on__flow__start(self.flow_event_data())
79
85
  self.log_debug(f"Created flow run '{self.f__flow_id()}' for flow '{self.f__flow_name()}'")
80
86
  self.set_flow_run_params(flow_run_params)
@@ -240,7 +246,13 @@ class Flow(Type_Safe):
240
246
  self.log_info(f"flow_run_params: {flow_run_params}")
241
247
  self.add_flow_artifact(description="Data received via FastAPI's request.json()", key='post-data', data=flow_run_params)
242
248
 
243
- def setup(self, target, *args, **kwargs):
249
+ def main(self, *args, **kwargs): # method to be overwritten by implementing classes
250
+ pass
251
+
252
+ def setup(self, target=None, *args, **kwargs):
253
+ if target is None:
254
+ target = self.main
255
+
244
256
  with self as _:
245
257
  _.cformat.auto_bold = True
246
258
  _.set_flow_target (target, *args, **kwargs)
@@ -250,6 +262,7 @@ class Flow(Type_Safe):
250
262
  _.flow_data.set_flow_id (self.flow_id)
251
263
  _.flow_data.set_flow_name(self.flow_name)
252
264
  _.add_event_listener()
265
+ _.setup_completed = True
253
266
  return self
254
267
 
255
268
  def setup_flow_run(self):
@@ -103,7 +103,7 @@ class Task(Type_Safe):
103
103
  if self.task_error:
104
104
  self.log_error(f_red(f"Error executing '{self.task_name}' task: {self.task_error}"))
105
105
  if self.raise_on_error:
106
- raise Exception(f"'{self.task_name}' failed and task raise_on_error was set to True. Stopping flow execution")
106
+ raise Exception(f"'{self.task_name}' failed and task raise_on_error was set to True. Stopping flow execution", self.task_error)
107
107
 
108
108
  self.print_task_finished_message()
109
109
 
@@ -116,7 +116,7 @@ class Task(Type_Safe):
116
116
  for frame_info in stack:
117
117
  frame = frame_info.frame
118
118
  for var_name, var_value in list(frame.f_locals.items()): # Check all local variables in the frame
119
- if type(var_value) is Flow:
119
+ if isinstance(var_value, Flow):
120
120
  return var_value
121
121
  return None
122
122
 
@@ -1,5 +1,5 @@
1
- from functools import wraps
2
1
  import asyncio
2
+ from functools import wraps
3
3
  from osbot_utils.helpers.flows.Task import Task
4
4
 
5
5
 
@@ -0,0 +1,119 @@
1
+ from typing import Dict, Any, Type
2
+ from osbot_utils.type_safe.Type_Safe__Base import Type_Safe__Base
3
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
4
+ from osbot_utils.type_safe.shared.Type_Safe__Cache import type_safe_cache
5
+ from osbot_utils.type_safe.shared.Type_Safe__Json_Compressor__Type_Registry import Type_Safe__Json_Compressor__Type_Registry
6
+ from osbot_utils.utils.Objects import class_full_name
7
+
8
+ class Type_Safe__Json_Compressor(Type_Safe__Base):
9
+
10
+ def __init__(self):
11
+ self.type_registry = Type_Safe__Json_Compressor__Type_Registry()
12
+
13
+ def compress(self, data: dict) -> dict:
14
+ if not data:
15
+ return data
16
+
17
+ self.type_registry.clear()
18
+ compressed = self.compress_object(data)
19
+
20
+ if self.type_registry.registry:
21
+ return { "_type_registry": self.type_registry.reverse,
22
+ **compressed }
23
+ return compressed
24
+
25
+ def compress_object(self, obj: Any) -> Any:
26
+ if isinstance(obj, Type_Safe):
27
+ annotations = type_safe_cache.get_obj_annotations(obj)
28
+ return self.process_type_safe_object(obj, annotations)
29
+ elif isinstance(obj, dict):
30
+ return self.compress_dict(obj)
31
+ elif isinstance(obj, list):
32
+ return [self.compress_object(item) for item in obj]
33
+ elif isinstance(obj, tuple):
34
+ return tuple(self.compress_object(item) for item in obj)
35
+ elif isinstance(obj, set):
36
+ return {self.compress_object(item) for item in obj}
37
+ return obj
38
+
39
+ def process_type_safe_object(self, obj : Type_Safe ,
40
+ annotations: Dict
41
+ ) -> Dict:
42
+ result = {}
43
+ for key, value in obj.__dict__.items():
44
+ if key.startswith('_'): # Skip internal attributes
45
+ continue
46
+ if key in annotations:
47
+ annotation = annotations[key]
48
+ result[key] = self.compress_annotated_value(value, annotation)
49
+ else:
50
+ result[key] = self.compress_object(value)
51
+ return result
52
+
53
+ def compress_annotated_value(self, value : Any ,
54
+ annotation : Any
55
+ ) -> Any:
56
+ origin = type_safe_cache.get_origin(annotation)
57
+ if origin in (type, Type): # Handle Type annotations
58
+ if value:
59
+ return self.type_registry.register_type(class_full_name(value))
60
+ return None
61
+ elif origin is dict: # Handle Dict annotations
62
+ return self.compress_dict(value)
63
+ elif origin in (list, tuple, set): # Handle sequence annotations
64
+ return self.compress_sequence(value)
65
+ elif isinstance(value, Type_Safe): # Handle nested Type_Safe objects
66
+ return self.compress_object(value)
67
+ return value
68
+
69
+ def compress_dict(self, data: Dict) -> Dict:
70
+ if not isinstance(data, dict):
71
+ return data
72
+
73
+ result = {}
74
+ for key, value in data.items():
75
+ compressed_key = self.compress_object(key) if isinstance(key, Type_Safe) else key
76
+ compressed_value = self.compress_object(value)
77
+ result[compressed_key] = compressed_value
78
+ return result
79
+
80
+ def compress_sequence(self, sequence: Any) -> Any:
81
+ if isinstance(sequence, (list, tuple, set)):
82
+ compressed = [self.compress_object(item) for item in sequence]
83
+ if isinstance(sequence, list):
84
+ return compressed
85
+ elif isinstance(sequence, tuple):
86
+ return tuple(compressed)
87
+ else:
88
+ return set(compressed)
89
+ return sequence
90
+
91
+ def decompress(self, data: dict) -> dict:
92
+ if not data or "_type_registry" not in data:
93
+ return data
94
+
95
+ registry = data.pop("_type_registry")
96
+ return self.expand_types(data.copy(), registry)
97
+
98
+ def expand_types(self, obj : Any ,
99
+ type_lookup : Dict[str, str]
100
+ ) -> Any:
101
+ if isinstance(obj, dict):
102
+ return {k: self.expand_value(v, type_lookup) for k, v in obj.items()}
103
+ elif isinstance(obj, list):
104
+ return [self.expand_types(item, type_lookup) for item in obj]
105
+ elif isinstance(obj, tuple):
106
+ return tuple(self.expand_types(item, type_lookup) for item in obj)
107
+ elif isinstance(obj, set):
108
+ return {self.expand_types(item, type_lookup) for item in obj}
109
+ return obj
110
+
111
+ def expand_value(self, value : Any ,
112
+ type_lookup : Dict[str, str]
113
+ ) -> Any:
114
+ if isinstance(value, str) and value.startswith('@'):
115
+ return type_lookup.get(value, value)
116
+ return self.expand_types(value, type_lookup)
117
+
118
+ def register_type(self, type_path: str) -> str:
119
+ return self.type_registry.register_type(type_path)
@@ -0,0 +1,39 @@
1
+ from typing import Dict, Optional
2
+
3
+
4
+ class Type_Safe__Json_Compressor__Type_Registry:
5
+
6
+ registry : Dict[str, str]
7
+ reverse : Dict[str, str]
8
+
9
+ def __init__(self):
10
+ self.registry = {}
11
+ self.reverse = {}
12
+
13
+ def create_reference_name(self, type_path: str) -> str:
14
+ parts = type_path.split('.')
15
+ class_name = parts[-1]
16
+ parts = class_name.split('__')
17
+
18
+ if len(parts) > 1:
19
+ name_parts = []
20
+ for part in parts:
21
+ name_parts.append(part.lower())
22
+ return f"@{'_'.join(name_parts)}"
23
+
24
+ return f"@{class_name.lower()}"
25
+
26
+ def register_type(self, type_path: str)-> str:
27
+ if type_path not in self.registry:
28
+ ref = self.create_reference_name(type_path)
29
+ self.registry[type_path] = ref
30
+ self.reverse[ref] = type_path
31
+ return ref
32
+ return self.registry[type_path]
33
+
34
+ def get_type(self, ref: str) -> Optional[str]:
35
+ return self.reverse.get(ref)
36
+
37
+ def clear(self):
38
+ self.registry.clear()
39
+ self.reverse.clear()
@@ -1,3 +1,5 @@
1
+ import collections
2
+ import inspect
1
3
  import types
2
4
  import typing
3
5
  from enum import EnumMeta
@@ -151,14 +153,15 @@ class Type_Safe__Validation:
151
153
  return all(isinstance(k, key_type) and isinstance(v, value_type)
152
154
  for k, v in value.items()) # if it is not a Union or Annotated types just return None (to give an indication to the caller that the comparison was not made)
153
155
 
154
- def check_if__type_matches__obj_annotation__for_attr(self, target,
155
- attr_name,
156
- value
157
- ) -> Optional[bool]:
156
+ def check_if__type_matches__obj_annotation__for_attr(self, target, attr_name, value) -> Optional[bool]:
158
157
  annotations = type_safe_cache.get_obj_annotations(target)
159
158
  attr_type = annotations.get(attr_name)
160
159
  if attr_type:
161
160
  origin_attr_type = get_origin(attr_type) # to handle when type definition contains a generic
161
+
162
+ if origin_attr_type is collections.abc.Callable: # Handle Callable types
163
+ return self.is_callable_compatible(value, attr_type) # ISSUE: THIS IS NEVER CALLED
164
+
162
165
  if origin_attr_type is set:
163
166
  if type(value) is list:
164
167
  return True # if the attribute is a set and the value is a list, then they are compatible
@@ -190,6 +193,42 @@ class Type_Safe__Validation:
190
193
  return value_type is attr_type
191
194
  return None
192
195
 
196
+ def is_callable_compatible(self, value, expected_type) -> bool:
197
+ if not callable(value):
198
+ return False
199
+
200
+ expected_args = get_args(expected_type)
201
+ if not expected_args: # Callable without type hints
202
+ return True
203
+
204
+ if len(expected_args) != 2: # Should have args and return type
205
+ return False
206
+
207
+ expected_param_types = expected_args[0] # First element is tuple of parameter types
208
+ expected_return_type = expected_args[1] # Second element is return type
209
+
210
+
211
+ try: # Get the signature of the actual value
212
+ sig = inspect.signature(value)
213
+ except ValueError: # Some built-in functions don't support introspection
214
+ return True
215
+
216
+ actual_params = list(sig.parameters.values()) # Get actual parameters
217
+
218
+ if len(actual_params) != len(expected_param_types): # Check number of parameters matches
219
+ return False
220
+
221
+ for actual_param, expected_param_type in zip(actual_params, expected_param_types): # Check each parameter type
222
+ if actual_param.annotation != inspect.Parameter.empty:
223
+ if not self.are_types_compatible_for_assigment(actual_param.annotation, expected_param_type):
224
+ return False # todo: check if we shouldn't raise an exception here, since this is the only place where we actually know the types that don't match in the method signature
225
+
226
+ if sig.return_annotation != inspect.Parameter.empty: # Check return type
227
+ if not self.are_types_compatible_for_assigment(sig.return_annotation, expected_return_type):
228
+ return False # todo: check if we shouldn't raise an exception here, since this is the only place where we actually know the types that don't match in the method return type
229
+
230
+ return True
231
+
193
232
  # todo: add cache support to this method
194
233
  def should_skip_type_check(self, var_type): # Determine if type checking should be skipped
195
234
  origin = type_safe_cache.get_origin(var_type) # Use cached get_origin
@@ -273,7 +273,7 @@ def serialize_to_dict(obj):
273
273
  from enum import Enum
274
274
  from typing import List
275
275
 
276
- if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None:
276
+ if isinstance(obj, (str, int, float, bool, bytes, Decimal)) or obj is None: # todo: add support for objects like datetime
277
277
  return obj
278
278
  elif isinstance(obj, Enum):
279
279
  return obj.name
osbot_utils/version CHANGED
@@ -1 +1 @@
1
- v2.21.0
1
+ v2.23.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: osbot_utils
3
- Version: 2.21.0
3
+ Version: 2.23.0
4
4
  Summary: OWASP Security Bot - Utils
5
5
  License: MIT
6
6
  Author: Dinis Cruz
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  Powerful Python util methods and classes that simplify common apis and tasks.
25
25
 
26
- ![Current Release](https://img.shields.io/badge/release-v2.21.0-blue)
26
+ ![Current Release](https://img.shields.io/badge/release-v2.23.0-blue)
27
27
  [![codecov](https://codecov.io/gh/owasp-sbot/OSBot-Utils/graph/badge.svg?token=GNVW0COX1N)](https://codecov.io/gh/owasp-sbot/OSBot-Utils)
28
28
 
29
29
 
@@ -136,15 +136,15 @@ osbot_utils/helpers/cache_requests/Cache__Requests__Row.py,sha256=Sb7E_zDB-LPlWI
136
136
  osbot_utils/helpers/cache_requests/Cache__Requests__Table.py,sha256=BW7tXM0TFYma3Db4M-58IKpx0vevLuFsH7QQeiggPaI,388
137
137
  osbot_utils/helpers/cache_requests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
138
  osbot_utils/helpers/cache_requests/flows/flow__Cache__Requests.py,sha256=xgx_oExxkcvRwQN1UCobimECIMUKGoIX5oGdCmp8Nyw,243
139
- osbot_utils/helpers/flows/Flow.py,sha256=PgShXbbrcMpPkTCls8LI1msZkD3bVk5nHlt9JrNCZGU,12624
140
- osbot_utils/helpers/flows/Task.py,sha256=dVf7DJw3NGvhJdmCbfWVE_KVMspkilIGuK9MgVpGGcE,6350
139
+ osbot_utils/helpers/flows/Flow.py,sha256=SjVh8eUjtEgzr7lSfxYzyUmgmRXEqeaT4tJjRiXMLps,13094
140
+ osbot_utils/helpers/flows/Task.py,sha256=b9DvSBP2DfDRK7wQ2X0zGBUX6j7XmCMf_IL7_DdJ7GE,6371
141
141
  osbot_utils/helpers/flows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
142
  osbot_utils/helpers/flows/actions/Flow__Data.py,sha256=h9h5yVFIK0ZdXukNFgfRVV1XW0YQartVMcJTakwVqKY,5155
143
143
  osbot_utils/helpers/flows/actions/Flow__Events.py,sha256=g7KBafFeA7tV-v31v_m3MT__cZEX63gh8CehnZwRYU0,2840
144
144
  osbot_utils/helpers/flows/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
145
  osbot_utils/helpers/flows/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
146
  osbot_utils/helpers/flows/decorators/flow.py,sha256=7wj5TtUO_ffbACnagZtZ6LfFgclmbQSfn2lKkMMrnJI,670
147
- osbot_utils/helpers/flows/decorators/task.py,sha256=9bhQBPJU1dO-J4FAsFkmxqQMBNtay4FT_b1BdpHJ9sA,734
147
+ osbot_utils/helpers/flows/decorators/task.py,sha256=Z5GqoUirnm98HAyvRlVcoXLcmZNagXjOXon2kb5OHsY,755
148
148
  osbot_utils/helpers/flows/models/Flow_Run__Config.py,sha256=vwtUSXEpBd7q9b8KXX1-FDWk90bwnNE1xiLnP3evks0,543
149
149
  osbot_utils/helpers/flows/models/Flow_Run__Event.py,sha256=2E-T6XXvsF2RnbIt5qq0rktOVBpembYvQudhoyM_6vc,589
150
150
  osbot_utils/helpers/flows/models/Flow_Run__Event_Data.py,sha256=zIAhfz1oUs0ocxbfCPmgj8tsXt2Ycjo0nFdeetKlnZ8,400
@@ -309,10 +309,12 @@ osbot_utils/type_safe/methods/type_safe_property.py,sha256=DcJkOIs6swJtkglsZVKLy
309
309
  osbot_utils/type_safe/shared/Type_Safe__Annotations.py,sha256=nmVqCbhk4kUYrw_mdYqugxQlv4gM3NUUH89FYTHUg-c,1133
310
310
  osbot_utils/type_safe/shared/Type_Safe__Cache.py,sha256=G03pmpds9sTwU5z5pNLssD_GTvVSIR11nGYbkV5KaiY,7913
311
311
  osbot_utils/type_safe/shared/Type_Safe__Convert.py,sha256=mS92_sKjKM_aNSB3ERMEgv-3DtkLVAS8AZF067G-JWM,2995
312
+ osbot_utils/type_safe/shared/Type_Safe__Json_Compressor.py,sha256=uAtb2-wYUUsx_E67oqVHCKPaRUMZBRQFyd8xjqziOEA,5431
313
+ osbot_utils/type_safe/shared/Type_Safe__Json_Compressor__Type_Registry.py,sha256=wYOCg7F1nTrRn8HlnZvrs_8A8WL4gxRYRLnXZpGIiuk,1119
312
314
  osbot_utils/type_safe/shared/Type_Safe__Not_Cached.py,sha256=25FAl6SOLxdStco_rm9tgOYLfuKyBWheGdl7vVa56UU,800
313
315
  osbot_utils/type_safe/shared/Type_Safe__Raise_Exception.py,sha256=pbru8k8CTQMNUfmFBndiJhg2KkqEYzFvJAPcNZHeHfQ,829
314
316
  osbot_utils/type_safe/shared/Type_Safe__Shared__Variables.py,sha256=SuZGl9LryQX6IpOE0I_lbzClT-h17UNylC__-M8ltTY,129
315
- osbot_utils/type_safe/shared/Type_Safe__Validation.py,sha256=CNGuTlfGCwm35d_MQ1ssiNFr2OxsbgI_MczqH_-EK6g,16315
317
+ osbot_utils/type_safe/shared/Type_Safe__Validation.py,sha256=5dVs2zDU_sDh3e025KjaDfUS7B8XENlQ77-7U34b2KY,19017
316
318
  osbot_utils/type_safe/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
317
319
  osbot_utils/type_safe/steps/Type_Safe__Step__Class_Kwargs.py,sha256=snoyJKvZ1crgF2fp0zexwNPnV_E63RfyRIsMAZdrKNY,6995
318
320
  osbot_utils/type_safe/steps/Type_Safe__Step__Default_Kwargs.py,sha256=tzKXDUc0HVP5QvCWsmcPuuZodNvQZ9FeMDNI2x00Ngw,1943
@@ -341,7 +343,7 @@ osbot_utils/utils/Json.py,sha256=TvfDoXwOkWzWH-9KMnme5C7iFsMZOleAeue92qmkH6g,883
341
343
  osbot_utils/utils/Json_Cache.py,sha256=mLPkkDZN-3ZVJiDvV1KBJXILtKkTZ4OepzOsDoBPhWg,2006
342
344
  osbot_utils/utils/Lists.py,sha256=tPz5x5s3sRO97WZ_nsxREBPC5cwaHrhgaYBhsrffTT8,5599
343
345
  osbot_utils/utils/Misc.py,sha256=H_xexJgiTxB3jDeDiW8efGQbO0Zuy8MM0iQ7qXC92JI,17363
344
- osbot_utils/utils/Objects.py,sha256=Jw_oe906vQXZ9TQfticocW_nHbUgitx7fVNdztfTOhs,12855
346
+ osbot_utils/utils/Objects.py,sha256=pLES6h48qsbrPihF4VRKbwzxD9wwh49A8lpJkL2iMRk,12905
345
347
  osbot_utils/utils/Png.py,sha256=V1juGp6wkpPigMJ8HcxrPDIP4bSwu51oNkLI8YqP76Y,1172
346
348
  osbot_utils/utils/Process.py,sha256=lr3CTiEkN3EiBx3ZmzYmTKlQoPdkgZBRjPulMxG-zdo,2357
347
349
  osbot_utils/utils/Python_Logger.py,sha256=M9Oi62LxfnRSlCN8GhaiwiBINvcSdGy39FCWjyDD-Xg,12792
@@ -353,8 +355,8 @@ osbot_utils/utils/Toml.py,sha256=Rxl8gx7mni5CvBAK-Ai02EKw-GwtJdd3yeHT2kMloik,166
353
355
  osbot_utils/utils/Version.py,sha256=Ww6ChwTxqp1QAcxOnztkTicShlcx6fbNsWX5xausHrg,422
354
356
  osbot_utils/utils/Zip.py,sha256=pR6sKliUY0KZXmqNzKY2frfW-YVQEVbLKiyqQX_lc-8,14052
355
357
  osbot_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
356
- osbot_utils/version,sha256=iRLKVsS2FheWNhb1-kryF4F9MunxpjaAPbrCshBkSY4,8
357
- osbot_utils-2.21.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
358
- osbot_utils-2.21.0.dist-info/METADATA,sha256=w8YOdMhQLXWrJ-hTXsuDjNpRzFDnmijUIsgYaMFqE0o,1329
359
- osbot_utils-2.21.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
360
- osbot_utils-2.21.0.dist-info/RECORD,,
358
+ osbot_utils/version,sha256=lqTNJLfzxdb8my8Vw-SUV5UlhJtWt7omX0sWdeN09a8,8
359
+ osbot_utils-2.23.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
360
+ osbot_utils-2.23.0.dist-info/METADATA,sha256=keIoqWHCXxO1cj-j8WjIsp4ul5LAzTp93WmWIghVpVU,1329
361
+ osbot_utils-2.23.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
362
+ osbot_utils-2.23.0.dist-info/RECORD,,