lmnr 0.4.64__py3-none-any.whl → 0.4.66__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.
@@ -7,6 +7,7 @@ class Instruments(Enum):
7
7
  ALEPHALPHA = "alephalpha"
8
8
  ANTHROPIC = "anthropic"
9
9
  BEDROCK = "bedrock"
10
+ BROWSER_USE = "browser_use"
10
11
  CHROMA = "chroma"
11
12
  COHERE = "cohere"
12
13
  GOOGLE_GENERATIVEAI = "google_generativeai"
@@ -6,7 +6,6 @@ import uuid
6
6
  from contextvars import Context
7
7
  from lmnr.sdk.log import VerboseColorfulFormatter
8
8
  from lmnr.openllmetry_sdk.instruments import Instruments
9
- from lmnr.sdk.browser import init_browser_tracing
10
9
  from lmnr.openllmetry_sdk.tracing.attributes import (
11
10
  ASSOCIATION_PROPERTIES,
12
11
  SPAN_IDS_PATH,
@@ -28,7 +27,7 @@ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
28
27
  )
29
28
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import Compression
30
29
  from opentelemetry.instrumentation.threading import ThreadingInstrumentor
31
- from opentelemetry.context import get_value, attach, get_current, set_value, Context
30
+ from opentelemetry.context import get_value, attach, get_current, set_value
32
31
  from opentelemetry.propagate import set_global_textmap
33
32
  from opentelemetry.propagators.textmap import TextMapPropagator
34
33
  from opentelemetry.sdk.resources import Resource
@@ -38,7 +37,7 @@ from opentelemetry.sdk.trace.export import (
38
37
  SimpleSpanProcessor,
39
38
  BatchSpanProcessor,
40
39
  )
41
- from opentelemetry.sdk.trace import SpanLimits
40
+ from opentelemetry.trace import get_tracer_provider, ProxyTracerProvider
42
41
 
43
42
  from typing import Dict, Optional, Set
44
43
 
@@ -238,6 +237,10 @@ def set_association_properties(properties: dict) -> None:
238
237
  _set_association_properties_attributes(span, properties)
239
238
 
240
239
 
240
+ def get_association_properties(context: Optional[Context] = None) -> dict:
241
+ return get_value("association_properties", context) or {}
242
+
243
+
241
244
  def update_association_properties(
242
245
  properties: dict,
243
246
  set_on_current_span: bool = True,
@@ -296,18 +299,21 @@ def init_spans_exporter(api_endpoint: str, headers: Dict[str, str]) -> SpanExpor
296
299
  # TODO: check if it's safer to use the default tracer provider obtained from
297
300
  # get_tracer_provider()
298
301
  def init_tracer_provider(resource: Resource) -> TracerProvider:
299
- tracer_provider = TracerProvider(
300
- resource=resource,
301
- span_limits=SpanLimits(
302
- # this defaults to 128, which causes us to drop messages
303
- max_attributes=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
304
- max_span_attributes=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
305
- max_event_attributes=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
306
- max_events=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
307
- ),
308
- )
302
+ provider: TracerProvider = None
303
+ default_provider: TracerProvider = get_tracer_provider()
304
+
305
+ if isinstance(default_provider, ProxyTracerProvider):
306
+ provider = TracerProvider(resource=resource)
307
+ trace.set_tracer_provider(provider)
308
+ elif not hasattr(default_provider, "add_span_processor"):
309
+ module_logger.error(
310
+ "Cannot add span processor to the default provider since it doesn't support it"
311
+ )
312
+ return
313
+ else:
314
+ provider = default_provider
309
315
 
310
- return tracer_provider
316
+ return provider
311
317
 
312
318
 
313
319
  def init_instrumentations(
@@ -428,7 +434,10 @@ def init_instrumentations(
428
434
  if init_weaviate_instrumentor():
429
435
  instrument_set = True
430
436
  elif instrument == Instruments.PLAYWRIGHT:
431
- if init_browser_tracing(base_http_url, project_api_key):
437
+ if init_playwright_instrumentor():
438
+ instrument_set = True
439
+ elif instrument == Instruments.BROWSER_USE:
440
+ if init_browser_use_instrumentor():
432
441
  instrument_set = True
433
442
  else:
434
443
  module_logger.warning(
@@ -443,6 +452,32 @@ def init_instrumentations(
443
452
  return instrument_set
444
453
 
445
454
 
455
+ def init_browser_use_instrumentor():
456
+ try:
457
+ if is_package_installed("browser-use"):
458
+ from lmnr.sdk.browser.browser_use_otel import BrowserUseInstrumentor
459
+
460
+ instrumentor = BrowserUseInstrumentor()
461
+ instrumentor.instrument()
462
+ return True
463
+ except Exception as e:
464
+ module_logger.error(f"Error initializing BrowserUse instrumentor: {e}")
465
+ return False
466
+
467
+
468
+ def init_playwright_instrumentor():
469
+ try:
470
+ if is_package_installed("playwright"):
471
+ from lmnr.sdk.browser.playwright_otel import PlaywrightInstrumentor
472
+
473
+ instrumentor = PlaywrightInstrumentor()
474
+ instrumentor.instrument()
475
+ return True
476
+ except Exception as e:
477
+ module_logger.error(f"Error initializing Playwright instrumentor: {e}")
478
+ return False
479
+
480
+
446
481
  def init_openai_instrumentor(should_enrich_metrics: bool):
447
482
  try:
448
483
  if is_package_installed("openai") and is_package_installed(
@@ -1,9 +0,0 @@
1
- from lmnr.openllmetry_sdk.utils.package_check import is_package_installed
2
-
3
-
4
- def init_browser_tracing(http_url: str, project_api_key: str):
5
- if is_package_installed("playwright"):
6
- from .playwright_patch import init_playwright_tracing
7
-
8
- init_playwright_tracing(http_url, project_api_key)
9
- # Other browsers can be added here
@@ -0,0 +1,117 @@
1
+ from lmnr.openllmetry_sdk.decorators.base import json_dumps
2
+ from lmnr.sdk.browser.utils import _with_tracer_wrapper
3
+ from lmnr.sdk.utils import get_input_from_func_args
4
+ from lmnr.version import SDK_VERSION
5
+
6
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
7
+ from opentelemetry.instrumentation.utils import unwrap
8
+ from opentelemetry.trace import get_tracer, Tracer
9
+ from typing import Collection
10
+ from wrapt import wrap_function_wrapper
11
+
12
+ _instruments = ("browser-use >= 0.1.0",)
13
+
14
+ WRAPPED_METHODS = [
15
+ {
16
+ "package": "browser_use.agent.service",
17
+ "object": "Agent",
18
+ "method": "run",
19
+ "span_name": "agent.run",
20
+ "ignore_input": False,
21
+ "ignore_output": True,
22
+ "span_type": "DEFAULT",
23
+ },
24
+ {
25
+ "package": "browser_use.agent.service",
26
+ "object": "Agent",
27
+ "method": "step",
28
+ "span_name": "agent.step",
29
+ "ignore_input": True,
30
+ "ignore_output": True,
31
+ "span_type": "DEFAULT",
32
+ },
33
+ {
34
+ "package": "browser_use.controller.service",
35
+ "object": "Controller",
36
+ "method": "act",
37
+ "span_name": "controller.act",
38
+ "ignore_input": True,
39
+ "ignore_output": False,
40
+ "span_type": "DEFAULT",
41
+ },
42
+ {
43
+ "package": "browser_use.controller.registry.service",
44
+ "object": "Registry",
45
+ "method": "execute_action",
46
+ "ignore_input": False,
47
+ "ignore_output": False,
48
+ "span_type": "TOOL",
49
+ },
50
+ ]
51
+
52
+
53
+ @_with_tracer_wrapper
54
+ async def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
55
+ span_name = to_wrap.get("span_name")
56
+ attributes = {
57
+ "lmnr.span.type": to_wrap.get("span_type"),
58
+ }
59
+ if to_wrap.get("method") == "execute_action":
60
+ span_name = args[0] if len(args) > 0 else kwargs.get("action_name", "action")
61
+ attributes["lmnr.span.input"] = json_dumps(
62
+ {
63
+ "action": span_name,
64
+ "params": args[1] if len(args) > 1 else kwargs.get("params", {}),
65
+ }
66
+ )
67
+ else:
68
+ if not to_wrap.get("ignore_input"):
69
+ attributes["lmnr.span.input"] = json_dumps(
70
+ get_input_from_func_args(wrapped, True, args, kwargs)
71
+ )
72
+ with tracer.start_as_current_span(span_name, attributes=attributes) as span:
73
+ span.set_attributes(attributes)
74
+ result = await wrapped(*args, **kwargs)
75
+ if not to_wrap.get("ignore_output"):
76
+ span.set_attribute("lmnr.span.output", json_dumps(result))
77
+ return result
78
+
79
+
80
+ class BrowserUseInstrumentor(BaseInstrumentor):
81
+ def __init__(self):
82
+ super().__init__()
83
+
84
+ def instrumentation_dependencies(self) -> Collection[str]:
85
+ return _instruments
86
+
87
+ def _instrument(self, **kwargs):
88
+ tracer_provider = kwargs.get("tracer_provider")
89
+ tracer = get_tracer(__name__, SDK_VERSION, tracer_provider)
90
+
91
+ for wrapped_method in WRAPPED_METHODS:
92
+ wrap_package = wrapped_method.get("package")
93
+ wrap_object = wrapped_method.get("object")
94
+ wrap_method = wrapped_method.get("method")
95
+
96
+ try:
97
+ wrap_function_wrapper(
98
+ wrap_package,
99
+ f"{wrap_object}.{wrap_method}",
100
+ _wrap(
101
+ tracer,
102
+ wrapped_method,
103
+ ),
104
+ )
105
+ except ModuleNotFoundError:
106
+ pass # that's ok, we're not instrumenting everything
107
+
108
+ def _uninstrument(self, **kwargs):
109
+ for wrapped_method in WRAPPED_METHODS:
110
+ wrap_package = wrapped_method.get("package")
111
+ wrap_object = wrapped_method.get("object")
112
+ wrap_method = wrapped_method.get("method")
113
+
114
+ unwrap(
115
+ f"{wrap_package}.{wrap_object}" if wrap_object else wrap_package,
116
+ wrap_method,
117
+ )
@@ -0,0 +1,310 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import threading
5
+ import time
6
+ import uuid
7
+
8
+ from lmnr.sdk.browser.utils import (
9
+ INJECT_PLACEHOLDER,
10
+ _with_tracer_wrapper,
11
+ retry_sync,
12
+ retry_async,
13
+ )
14
+ from lmnr.sdk.client import LaminarClient
15
+ from lmnr.version import PYTHON_VERSION, SDK_VERSION
16
+
17
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
18
+ from opentelemetry.instrumentation.utils import unwrap
19
+ from opentelemetry.trace import get_tracer, Tracer, get_current_span
20
+ from typing import Collection
21
+ from wrapt import wrap_function_wrapper
22
+
23
+ try:
24
+ from playwright.async_api import Page
25
+ from playwright.sync_api import Page as SyncPage
26
+ except ImportError as e:
27
+ raise ImportError(
28
+ f"Attempted to import {__file__}, but it is designed "
29
+ "to patch Playwright, which is not installed. Use `pip install playwright` "
30
+ "to install Playwright or remove this import."
31
+ ) from e
32
+
33
+ # all available versions at https://pypi.org/project/playwright/#history
34
+ _instruments = ("playwright >= 1.9.0",)
35
+ logger = logging.getLogger(__name__)
36
+
37
+ WRAPPED_METHODS = [
38
+ {
39
+ "package": "playwright.sync_api",
40
+ "object": "BrowserContext",
41
+ "method": "new_page",
42
+ }
43
+ ]
44
+
45
+ WRAPPED_METHODS_ASYNC = [
46
+ {
47
+ "package": "playwright.async_api",
48
+ "object": "BrowserContext",
49
+ "method": "new_page",
50
+ }
51
+ ]
52
+
53
+ _original_new_page = None
54
+ _original_new_page_async = None
55
+
56
+ current_dir = os.path.dirname(os.path.abspath(__file__))
57
+ with open(os.path.join(current_dir, "rrweb", "rrweb.min.js"), "r") as f:
58
+ RRWEB_CONTENT = f"() => {{ {f.read()} }}"
59
+
60
+
61
+ async def send_events_async(page: Page, session_id: str, trace_id: str):
62
+ """Fetch events from the page and send them to the server"""
63
+ try:
64
+ # Check if function exists first
65
+ has_function = await page.evaluate(
66
+ """
67
+ () => typeof window.lmnrGetAndClearEvents === 'function'
68
+ """
69
+ )
70
+ if not has_function:
71
+ return
72
+
73
+ events = await page.evaluate("window.lmnrGetAndClearEvents()")
74
+ if not events or len(events) == 0:
75
+ return
76
+
77
+ await LaminarClient.send_browser_events(
78
+ session_id, trace_id, events, f"python@{PYTHON_VERSION}"
79
+ )
80
+
81
+ except Exception as e:
82
+ logger.error(f"Error sending events: {e}")
83
+
84
+
85
+ def send_events_sync(page: SyncPage, session_id: str, trace_id: str):
86
+ """Synchronous version of send_events"""
87
+ try:
88
+ # Check if function exists first
89
+ has_function = page.evaluate(
90
+ """
91
+ () => typeof window.lmnrGetAndClearEvents === 'function'
92
+ """
93
+ )
94
+ if not has_function:
95
+ return
96
+
97
+ events = page.evaluate("window.lmnrGetAndClearEvents()")
98
+ if not events or len(events) == 0:
99
+ return
100
+
101
+ LaminarClient.send_browser_events_sync(
102
+ session_id, trace_id, events, f"python@{PYTHON_VERSION}"
103
+ )
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error sending events: {e}")
107
+
108
+
109
+ def inject_rrweb(page: SyncPage):
110
+ try:
111
+ page.wait_for_load_state("domcontentloaded")
112
+
113
+ # Wrap the evaluate call in a try-catch
114
+ try:
115
+ is_loaded = page.evaluate(
116
+ """() => typeof window.lmnrRrweb !== 'undefined'"""
117
+ )
118
+ except Exception as e:
119
+ logger.debug(f"Failed to check if rrweb is loaded: {e}")
120
+ is_loaded = False
121
+
122
+ if not is_loaded:
123
+
124
+ def load_rrweb():
125
+ try:
126
+ page.evaluate(RRWEB_CONTENT)
127
+ page.wait_for_function(
128
+ """(() => typeof window.lmnrRrweb !== 'undefined')""",
129
+ timeout=5000,
130
+ )
131
+ return True
132
+ except Exception as e:
133
+ logger.debug(f"Failed to load rrweb: {e}")
134
+ return False
135
+
136
+ if not retry_sync(
137
+ load_rrweb, delay=1, error_message="Failed to load rrweb"
138
+ ):
139
+ return
140
+
141
+ try:
142
+ page.evaluate(INJECT_PLACEHOLDER)
143
+ except Exception as e:
144
+ logger.debug(f"Failed to inject rrweb placeholder: {e}")
145
+
146
+ except Exception as e:
147
+ logger.error(f"Error during rrweb injection: {e}")
148
+
149
+
150
+ async def inject_rrweb_async(page: Page):
151
+ try:
152
+ await page.wait_for_load_state("domcontentloaded")
153
+
154
+ # Wrap the evaluate call in a try-catch
155
+ try:
156
+ is_loaded = await page.evaluate(
157
+ """() => typeof window.lmnrRrweb !== 'undefined'"""
158
+ )
159
+ except Exception as e:
160
+ logger.debug(f"Failed to check if rrweb is loaded: {e}")
161
+ is_loaded = False
162
+
163
+ if not is_loaded:
164
+
165
+ async def load_rrweb():
166
+ try:
167
+ await page.evaluate(RRWEB_CONTENT)
168
+ await page.wait_for_function(
169
+ """(() => typeof window.lmnrRrweb !== 'undefined')""",
170
+ timeout=5000,
171
+ )
172
+ return True
173
+ except Exception as e:
174
+ logger.debug(f"Failed to load rrweb: {e}")
175
+ return False
176
+
177
+ if not await retry_async(
178
+ load_rrweb, delay=1, error_message="Failed to load rrweb"
179
+ ):
180
+ return
181
+
182
+ try:
183
+ await page.evaluate(INJECT_PLACEHOLDER)
184
+ except Exception as e:
185
+ logger.debug(f"Failed to inject rrweb placeholder: {e}")
186
+
187
+ except Exception as e:
188
+ logger.error(f"Error during rrweb injection: {e}")
189
+
190
+
191
+ def handle_navigation(page: SyncPage, session_id: str, trace_id: str):
192
+ def on_load():
193
+ try:
194
+ inject_rrweb(page)
195
+ except Exception as e:
196
+ logger.error(f"Error in on_load handler: {e}")
197
+
198
+ page.on("load", on_load)
199
+ inject_rrweb(page)
200
+
201
+ def collection_loop():
202
+ while not page.is_closed(): # Stop when page closes
203
+ send_events_sync(page, session_id, trace_id)
204
+ time.sleep(2)
205
+
206
+ thread = threading.Thread(target=collection_loop, daemon=True)
207
+ thread.start()
208
+
209
+
210
+ async def handle_navigation_async(page: Page, session_id: str, trace_id: str):
211
+ async def on_load():
212
+ try:
213
+ await inject_rrweb_async(page)
214
+ except Exception as e:
215
+ logger.error(f"Error in on_load handler: {e}")
216
+
217
+ page.on("load", lambda: asyncio.create_task(on_load()))
218
+ await inject_rrweb_async(page)
219
+
220
+ async def collection_loop():
221
+ try:
222
+ while not page.is_closed(): # Stop when page closes
223
+ await send_events_async(page, session_id, trace_id)
224
+ await asyncio.sleep(2)
225
+ logger.info("Event collection stopped")
226
+ except Exception as e:
227
+ logger.error(f"Event collection stopped: {e}")
228
+
229
+ # Create and store task
230
+ task = asyncio.create_task(collection_loop())
231
+
232
+ # Clean up task when page closes
233
+ page.on("close", lambda: task.cancel())
234
+
235
+
236
+ @_with_tracer_wrapper
237
+ def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
238
+ with tracer.start_as_current_span(
239
+ f"browser_context.{to_wrap.get('method')}"
240
+ ) as span:
241
+ page = wrapped(*args, **kwargs)
242
+ session_id = str(uuid.uuid4().hex)
243
+ trace_id = format(get_current_span().get_span_context().trace_id, "032x")
244
+ span.set_attribute("lmnr.internal.has_browser_session", True)
245
+ handle_navigation(page, session_id, trace_id)
246
+ return page
247
+
248
+
249
+ @_with_tracer_wrapper
250
+ async def _wrap_async(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
251
+ with tracer.start_as_current_span(
252
+ f"browser_context.{to_wrap.get('method')}"
253
+ ) as span:
254
+ page = await wrapped(*args, **kwargs)
255
+ session_id = str(uuid.uuid4().hex)
256
+ trace_id = format(get_current_span().get_span_context().trace_id, "032x")
257
+ span.set_attribute("lmnr.internal.has_browser_session", True)
258
+ await handle_navigation_async(page, session_id, trace_id)
259
+ return page
260
+
261
+
262
+ class PlaywrightInstrumentor(BaseInstrumentor):
263
+ def __init__(self):
264
+ super().__init__()
265
+
266
+ def instrumentation_dependencies(self) -> Collection[str]:
267
+ return _instruments
268
+
269
+ def _instrument(self, **kwargs):
270
+ tracer_provider = kwargs.get("tracer_provider")
271
+ tracer = get_tracer(__name__, SDK_VERSION, tracer_provider)
272
+
273
+ for wrapped_method in WRAPPED_METHODS:
274
+ wrap_package = wrapped_method.get("package")
275
+ wrap_object = wrapped_method.get("object")
276
+ wrap_method = wrapped_method.get("method")
277
+ try:
278
+ wrap_function_wrapper(
279
+ wrap_package,
280
+ f"{wrap_object}.{wrap_method}",
281
+ _wrap(
282
+ tracer,
283
+ wrapped_method,
284
+ ),
285
+ )
286
+ except ModuleNotFoundError:
287
+ pass # that's ok, we're not instrumenting everything
288
+
289
+ for wrapped_method in WRAPPED_METHODS_ASYNC:
290
+ wrap_package = wrapped_method.get("package")
291
+ wrap_object = wrapped_method.get("object")
292
+ wrap_method = wrapped_method.get("method")
293
+ try:
294
+ wrap_function_wrapper(
295
+ wrap_package,
296
+ f"{wrap_object}.{wrap_method}",
297
+ _wrap_async(
298
+ tracer,
299
+ wrapped_method,
300
+ ),
301
+ )
302
+ except ModuleNotFoundError:
303
+ pass # that's ok, we're not instrumenting everything
304
+
305
+ def _uninstrument(self, **kwargs):
306
+ for wrapped_method in [*WRAPPED_METHODS, *WRAPPED_METHODS_ASYNC]:
307
+ wrap_package = wrapped_method.get("package")
308
+ wrap_object = wrapped_method.get("object")
309
+ wrap_method = wrapped_method.get("method")
310
+ unwrap(wrap_package, f"{wrap_object}.{wrap_method}")
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ INJECT_PLACEHOLDER = """
8
+ () => {
9
+ const BATCH_SIZE = 1000; // Maximum events to store in memory
10
+
11
+ window.lmnrRrwebEventsBatch = [];
12
+
13
+ // Utility function to compress individual event data
14
+ async function compressEventData(data) {
15
+ const jsonString = JSON.stringify(data);
16
+ const blob = new Blob([jsonString], { type: 'application/json' });
17
+ const compressedStream = blob.stream().pipeThrough(new CompressionStream('gzip'));
18
+ const compressedResponse = new Response(compressedStream);
19
+ const compressedData = await compressedResponse.arrayBuffer();
20
+ return Array.from(new Uint8Array(compressedData));
21
+ }
22
+
23
+ window.lmnrGetAndClearEvents = () => {
24
+ const events = window.lmnrRrwebEventsBatch;
25
+ window.lmnrRrwebEventsBatch = [];
26
+ return events;
27
+ };
28
+
29
+ // Add heartbeat events
30
+ setInterval(async () => {
31
+ const heartbeat = {
32
+ type: 6,
33
+ data: await compressEventData({ source: 'heartbeat' }),
34
+ timestamp: Date.now()
35
+ };
36
+
37
+ window.lmnrRrwebEventsBatch.push(heartbeat);
38
+
39
+ // Prevent memory issues by limiting batch size
40
+ if (window.lmnrRrwebEventsBatch.length > BATCH_SIZE) {
41
+ window.lmnrRrwebEventsBatch = window.lmnrRrwebEventsBatch.slice(-BATCH_SIZE);
42
+ }
43
+ }, 1000);
44
+
45
+ window.lmnrRrweb.record({
46
+ async emit(event) {
47
+ // Compress the data field
48
+ const compressedEvent = {
49
+ ...event,
50
+ data: await compressEventData(event.data)
51
+ };
52
+ window.lmnrRrwebEventsBatch.push(compressedEvent);
53
+ }
54
+ });
55
+ }
56
+ """
57
+
58
+
59
+ def _with_tracer_wrapper(func):
60
+ """Helper for providing tracer for wrapper functions."""
61
+
62
+ def _with_tracer(tracer, to_wrap):
63
+ def wrapper(wrapped, instance, args, kwargs):
64
+ return func(tracer, to_wrap, wrapped, instance, args, kwargs)
65
+
66
+ return wrapper
67
+
68
+ return _with_tracer
69
+
70
+
71
+ def retry_sync(func, retries=5, delay=0.5, error_message="Operation failed"):
72
+ """Utility function for retry logic in synchronous operations"""
73
+ for attempt in range(retries):
74
+ try:
75
+ result = func()
76
+ if result: # If function returns truthy value, consider it successful
77
+ return result
78
+ if attempt == retries - 1: # Last attempt
79
+ logger.error(f"{error_message} after all retries")
80
+ return None
81
+ except Exception as e:
82
+ if attempt == retries - 1: # Last attempt
83
+ logger.error(f"{error_message}: {e}")
84
+ return None
85
+ time.sleep(delay)
86
+ return None
87
+
88
+
89
+ async def retry_async(func, retries=5, delay=0.5, error_message="Operation failed"):
90
+ """Utility function for retry logic in asynchronous operations"""
91
+ for attempt in range(retries):
92
+ try:
93
+ result = await func()
94
+ if result: # If function returns truthy value, consider it successful
95
+ return result
96
+ if attempt == retries - 1: # Last attempt
97
+ logger.error(f"{error_message} after all retries")
98
+ return None
99
+ except Exception as e:
100
+ if attempt == retries - 1: # Last attempt
101
+ logger.error(f"{error_message}: {e}")
102
+ return None
103
+ await asyncio.sleep(delay)
104
+ return None