lmnr 0.4.66__py3-none-any.whl → 0.5.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.
Files changed (37) hide show
  1. lmnr/__init__.py +30 -0
  2. lmnr/openllmetry_sdk/__init__.py +4 -15
  3. lmnr/openllmetry_sdk/tracing/attributes.py +0 -1
  4. lmnr/openllmetry_sdk/tracing/tracing.py +24 -9
  5. lmnr/sdk/browser/browser_use_otel.py +4 -4
  6. lmnr/sdk/browser/playwright_otel.py +213 -228
  7. lmnr/sdk/browser/pw_utils.py +289 -0
  8. lmnr/sdk/browser/utils.py +18 -53
  9. lmnr/sdk/client/asynchronous/async_client.py +157 -0
  10. lmnr/sdk/client/asynchronous/resources/__init__.py +13 -0
  11. lmnr/sdk/client/asynchronous/resources/agent.py +215 -0
  12. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  13. lmnr/sdk/client/asynchronous/resources/browser_events.py +40 -0
  14. lmnr/sdk/client/asynchronous/resources/evals.py +64 -0
  15. lmnr/sdk/client/asynchronous/resources/pipeline.py +89 -0
  16. lmnr/sdk/client/asynchronous/resources/semantic_search.py +60 -0
  17. lmnr/sdk/client/synchronous/resources/__init__.py +7 -0
  18. lmnr/sdk/client/synchronous/resources/agent.py +209 -0
  19. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  20. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  21. lmnr/sdk/client/synchronous/resources/evals.py +102 -0
  22. lmnr/sdk/client/synchronous/resources/pipeline.py +89 -0
  23. lmnr/sdk/client/synchronous/resources/semantic_search.py +60 -0
  24. lmnr/sdk/client/synchronous/sync_client.py +170 -0
  25. lmnr/sdk/datasets.py +7 -2
  26. lmnr/sdk/evaluations.py +53 -27
  27. lmnr/sdk/laminar.py +22 -175
  28. lmnr/sdk/types.py +121 -23
  29. lmnr/sdk/utils.py +10 -0
  30. lmnr/version.py +6 -6
  31. {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/METADATA +88 -38
  32. lmnr-0.5.0.dist-info/RECORD +55 -0
  33. lmnr/sdk/client.py +0 -313
  34. lmnr-0.4.66.dist-info/RECORD +0 -39
  35. {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/LICENSE +0 -0
  36. {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/WHEEL +0 -0
  37. {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,28 +1,29 @@
1
- import asyncio
2
1
  import logging
3
- import os
4
- import threading
5
- import time
6
2
  import uuid
7
3
 
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
4
+ from lmnr.sdk.browser.pw_utils import handle_navigation_async, handle_navigation_sync
5
+ from lmnr.sdk.browser.utils import with_tracer_and_client_wrapper
6
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
7
+ from lmnr.sdk.client.synchronous.sync_client import LaminarClient
8
+ from lmnr.version import __version__
16
9
 
17
10
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
18
11
  from opentelemetry.instrumentation.utils import unwrap
19
- from opentelemetry.trace import get_tracer, Tracer, get_current_span
12
+ from opentelemetry.trace import (
13
+ get_tracer,
14
+ Tracer,
15
+ get_current_span,
16
+ Span,
17
+ INVALID_SPAN,
18
+ set_span_in_context,
19
+ )
20
+ from opentelemetry.context import get_current
20
21
  from typing import Collection
21
22
  from wrapt import wrap_function_wrapper
22
23
 
23
24
  try:
24
- from playwright.async_api import Page
25
- from playwright.sync_api import Page as SyncPage
25
+ from playwright.async_api import Browser
26
+ from playwright.sync_api import Browser as SyncBrowser
26
27
  except ImportError as e:
27
28
  raise ImportError(
28
29
  f"Attempted to import {__file__}, but it is designed "
@@ -34,241 +35,216 @@ except ImportError as e:
34
35
  _instruments = ("playwright >= 1.9.0",)
35
36
  logger = logging.getLogger(__name__)
36
37
 
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()
38
+ _context_spans: dict[str, Span] = {}
208
39
 
209
40
 
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):
41
+ @with_tracer_and_client_wrapper
42
+ def _wrap_new_page(
43
+ tracer: Tracer, client: LaminarClient, to_wrap, wrapped, instance, args, kwargs
44
+ ):
238
45
  with tracer.start_as_current_span(
239
- f"browser_context.{to_wrap.get('method')}"
46
+ f"{to_wrap.get('object')}.{to_wrap.get('method')}"
240
47
  ) as span:
241
48
  page = wrapped(*args, **kwargs)
242
49
  session_id = str(uuid.uuid4().hex)
243
50
  trace_id = format(get_current_span().get_span_context().trace_id, "032x")
244
51
  span.set_attribute("lmnr.internal.has_browser_session", True)
245
- handle_navigation(page, session_id, trace_id)
52
+ handle_navigation_sync(page, session_id, trace_id, client)
246
53
  return page
247
54
 
248
55
 
249
- @_with_tracer_wrapper
250
- async def _wrap_async(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
56
+ @with_tracer_and_client_wrapper
57
+ async def _wrap_new_page_async(
58
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
59
+ ):
251
60
  with tracer.start_as_current_span(
252
- f"browser_context.{to_wrap.get('method')}"
61
+ f"{to_wrap.get('object')}.{to_wrap.get('method')}"
253
62
  ) as span:
254
63
  page = await wrapped(*args, **kwargs)
255
64
  session_id = str(uuid.uuid4().hex)
256
- trace_id = format(get_current_span().get_span_context().trace_id, "032x")
65
+ trace_id = format(span.get_span_context().trace_id, "032x")
257
66
  span.set_attribute("lmnr.internal.has_browser_session", True)
258
- await handle_navigation_async(page, session_id, trace_id)
67
+ await handle_navigation_async(page, session_id, trace_id, client)
259
68
  return page
260
69
 
261
70
 
71
+ @with_tracer_and_client_wrapper
72
+ def _wrap_new_browser_sync(
73
+ tracer: Tracer, client: LaminarClient, to_wrap, wrapped, instance, args, kwargs
74
+ ):
75
+ global _context_spans
76
+ browser: SyncBrowser = wrapped(*args, **kwargs)
77
+ session_id = str(uuid.uuid4().hex)
78
+ for context in browser.contexts:
79
+ span = get_current_span()
80
+ if span == INVALID_SPAN:
81
+ span = tracer.start_span(
82
+ name=f"{to_wrap.get('object')}.{to_wrap.get('method')}"
83
+ )
84
+ set_span_in_context(span, get_current())
85
+ _context_spans[id(context)] = span
86
+ span.set_attribute("lmnr.internal.has_browser_session", True)
87
+ for page in context.pages:
88
+ trace_id = format(span.get_span_context().trace_id, "032x")
89
+ handle_navigation_sync(page, session_id, trace_id, client)
90
+ return browser
91
+
92
+
93
+ @with_tracer_and_client_wrapper
94
+ async def _wrap_new_browser_async(
95
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
96
+ ):
97
+ global _context_spans
98
+ browser: Browser = await wrapped(*args, **kwargs)
99
+ session_id = str(uuid.uuid4().hex)
100
+ for context in browser.contexts:
101
+ span = get_current_span()
102
+ if span == INVALID_SPAN:
103
+ span = tracer.start_span(
104
+ name=f"{to_wrap.get('object')}.{to_wrap.get('method')}"
105
+ )
106
+ set_span_in_context(span, get_current())
107
+ _context_spans[id(context)] = span
108
+ span.set_attribute("lmnr.internal.has_browser_session", True)
109
+ for page in context.pages:
110
+ trace_id = format(span.get_span_context().trace_id, "032x")
111
+ await handle_navigation_async(page, session_id, trace_id, client)
112
+ return browser
113
+
114
+
115
+ @with_tracer_and_client_wrapper
116
+ def _wrap_close_browser_sync(
117
+ tracer: Tracer,
118
+ client: LaminarClient,
119
+ to_wrap,
120
+ wrapped,
121
+ instance: SyncBrowser,
122
+ args,
123
+ kwargs,
124
+ ):
125
+ global _context_spans
126
+ for context in instance.contexts:
127
+ key = id(context)
128
+ span = _context_spans.get(key)
129
+ if span:
130
+ if span.is_recording():
131
+ span.end()
132
+ _context_spans.pop(key)
133
+ return wrapped(*args, **kwargs)
134
+
135
+
136
+ @with_tracer_and_client_wrapper
137
+ async def _wrap_close_browser_async(
138
+ tracer: Tracer,
139
+ client: AsyncLaminarClient,
140
+ to_wrap,
141
+ wrapped,
142
+ instance: Browser,
143
+ args,
144
+ kwargs,
145
+ ):
146
+ global _context_spans
147
+ for context in instance.contexts:
148
+ key = id(context)
149
+ span = _context_spans.get(key)
150
+ if span:
151
+ if span.is_recording():
152
+ span.end()
153
+ _context_spans.pop(key)
154
+ return await wrapped(*args, **kwargs)
155
+
156
+
157
+ WRAPPED_METHODS = [
158
+ {
159
+ "package": "playwright.sync_api",
160
+ "object": "BrowserContext",
161
+ "method": "new_page",
162
+ "wrapper": _wrap_new_page,
163
+ },
164
+ {
165
+ "package": "playwright.sync_api",
166
+ "object": "Browser",
167
+ "method": "new_page",
168
+ "wrapper": _wrap_new_page,
169
+ },
170
+ {
171
+ "package": "playwright.sync_api",
172
+ "object": "BrowserType",
173
+ "method": "launch",
174
+ "wrapper": _wrap_new_browser_sync,
175
+ },
176
+ {
177
+ "package": "playwright.sync_api",
178
+ "object": "BrowserType",
179
+ "method": "connect",
180
+ "wrapper": _wrap_new_browser_sync,
181
+ },
182
+ {
183
+ "package": "playwright.sync_api",
184
+ "object": "BrowserType",
185
+ "method": "connect_over_cdp",
186
+ "wrapper": _wrap_new_browser_sync,
187
+ },
188
+ {
189
+ "package": "playwright.sync_api",
190
+ "object": "Browser",
191
+ "method": "close",
192
+ "wrapper": _wrap_close_browser_sync,
193
+ },
194
+ ]
195
+
196
+ WRAPPED_METHODS_ASYNC = [
197
+ {
198
+ "package": "playwright.async_api",
199
+ "object": "BrowserContext",
200
+ "method": "new_page",
201
+ "wrapper": _wrap_new_page_async,
202
+ },
203
+ {
204
+ "package": "playwright.async_api",
205
+ "object": "Browser",
206
+ "method": "new_page",
207
+ "wrapper": _wrap_new_page_async,
208
+ },
209
+ {
210
+ "package": "playwright.async_api",
211
+ "object": "BrowserType",
212
+ "method": "launch",
213
+ "wrapper": _wrap_new_browser_async,
214
+ },
215
+ {
216
+ "package": "playwright.async_api",
217
+ "object": "BrowserType",
218
+ "method": "connect",
219
+ "wrapper": _wrap_new_browser_async,
220
+ },
221
+ {
222
+ "package": "playwright.async_api",
223
+ "object": "BrowserType",
224
+ "method": "connect_over_cdp",
225
+ "wrapper": _wrap_new_browser_async,
226
+ },
227
+ {
228
+ "package": "playwright.async_api",
229
+ "object": "Browser",
230
+ "method": "close",
231
+ "wrapper": _wrap_close_browser_async,
232
+ },
233
+ ]
234
+
235
+
262
236
  class PlaywrightInstrumentor(BaseInstrumentor):
263
- def __init__(self):
237
+ def __init__(self, client: LaminarClient, async_client: AsyncLaminarClient):
264
238
  super().__init__()
239
+ self.client = client
240
+ self.async_client = async_client
265
241
 
266
242
  def instrumentation_dependencies(self) -> Collection[str]:
267
243
  return _instruments
268
244
 
269
245
  def _instrument(self, **kwargs):
270
246
  tracer_provider = kwargs.get("tracer_provider")
271
- tracer = get_tracer(__name__, SDK_VERSION, tracer_provider)
247
+ tracer = get_tracer(__name__, __version__, tracer_provider)
272
248
 
273
249
  for wrapped_method in WRAPPED_METHODS:
274
250
  wrap_package = wrapped_method.get("package")
@@ -278,14 +254,16 @@ class PlaywrightInstrumentor(BaseInstrumentor):
278
254
  wrap_function_wrapper(
279
255
  wrap_package,
280
256
  f"{wrap_object}.{wrap_method}",
281
- _wrap(
257
+ wrapped_method.get("wrapper")(
282
258
  tracer,
259
+ self.client,
283
260
  wrapped_method,
284
261
  ),
285
262
  )
286
263
  except ModuleNotFoundError:
287
- pass # that's ok, we're not instrumenting everything
264
+ pass # that's ok, we don't want to fail if some module is missing
288
265
 
266
+ # Wrap async methods
289
267
  for wrapped_method in WRAPPED_METHODS_ASYNC:
290
268
  wrap_package = wrapped_method.get("package")
291
269
  wrap_object = wrapped_method.get("object")
@@ -294,17 +272,24 @@ class PlaywrightInstrumentor(BaseInstrumentor):
294
272
  wrap_function_wrapper(
295
273
  wrap_package,
296
274
  f"{wrap_object}.{wrap_method}",
297
- _wrap_async(
275
+ wrapped_method.get("wrapper")(
298
276
  tracer,
277
+ self.async_client,
299
278
  wrapped_method,
300
279
  ),
301
280
  )
302
281
  except ModuleNotFoundError:
303
- pass # that's ok, we're not instrumenting everything
282
+ pass # that's ok, we don't want to fail if some module is missing
304
283
 
305
284
  def _uninstrument(self, **kwargs):
306
- for wrapped_method in [*WRAPPED_METHODS, *WRAPPED_METHODS_ASYNC]:
285
+ # Unwrap methods
286
+ global _context_spans
287
+ for wrapped_method in WRAPPED_METHODS + WRAPPED_METHODS_ASYNC:
307
288
  wrap_package = wrapped_method.get("package")
308
289
  wrap_object = wrapped_method.get("object")
309
290
  wrap_method = wrapped_method.get("method")
310
291
  unwrap(wrap_package, f"{wrap_object}.{wrap_method}")
292
+ for span in _context_spans.values():
293
+ if span.is_recording():
294
+ span.end()
295
+ _context_spans = {}