lmnr 0.4.61__py3-none-any.whl → 0.4.63__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.
- lmnr/__init__.py +1 -0
- lmnr/openllmetry_sdk/__init__.py +4 -0
- lmnr/openllmetry_sdk/tracing/tracing.py +17 -15
- lmnr/sdk/browser/playwright_patch.py +253 -225
- lmnr/sdk/laminar.py +159 -38
- lmnr/sdk/types.py +86 -0
- lmnr/version.py +42 -1
- {lmnr-0.4.61.dist-info → lmnr-0.4.63.dist-info}/METADATA +1 -1
- {lmnr-0.4.61.dist-info → lmnr-0.4.63.dist-info}/RECORD +12 -12
- {lmnr-0.4.61.dist-info → lmnr-0.4.63.dist-info}/LICENSE +0 -0
- {lmnr-0.4.61.dist-info → lmnr-0.4.63.dist-info}/WHEEL +0 -0
- {lmnr-0.4.61.dist-info → lmnr-0.4.63.dist-info}/entry_points.txt +0 -0
lmnr/__init__.py
CHANGED
@@ -10,6 +10,7 @@ from .sdk.types import (
|
|
10
10
|
TracingLevel,
|
11
11
|
)
|
12
12
|
from .sdk.decorators import observe
|
13
|
+
from .sdk.types import LaminarSpanContext
|
13
14
|
from .openllmetry_sdk import Instruments
|
14
15
|
from .openllmetry_sdk.tracing.attributes import Attributes
|
15
16
|
from opentelemetry.trace import use_span
|
lmnr/openllmetry_sdk/__init__.py
CHANGED
@@ -38,7 +38,7 @@ from opentelemetry.sdk.trace.export import (
|
|
38
38
|
SimpleSpanProcessor,
|
39
39
|
BatchSpanProcessor,
|
40
40
|
)
|
41
|
-
from opentelemetry.trace import
|
41
|
+
from opentelemetry.sdk.trace import SpanLimits
|
42
42
|
|
43
43
|
from typing import Dict, Optional, Set
|
44
44
|
|
@@ -69,6 +69,8 @@ EXCLUDED_URLS = """
|
|
69
69
|
githubusercontent.com,
|
70
70
|
openaipublic.blob.core.windows.net"""
|
71
71
|
|
72
|
+
MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN = 5000
|
73
|
+
|
72
74
|
|
73
75
|
class TracerWrapper(object):
|
74
76
|
resource_attributes: dict = {}
|
@@ -223,6 +225,7 @@ class TracerWrapper(object):
|
|
223
225
|
cls.__span_id_lists = {}
|
224
226
|
|
225
227
|
def flush(self):
|
228
|
+
print("Flushing spans")
|
226
229
|
self.__spans_processor.force_flush()
|
227
230
|
|
228
231
|
def get_tracer(self):
|
@@ -291,22 +294,21 @@ def init_spans_exporter(api_endpoint: str, headers: Dict[str, str]) -> SpanExpor
|
|
291
294
|
)
|
292
295
|
|
293
296
|
|
297
|
+
# TODO: check if it's safer to use the default tracer provider obtained from
|
298
|
+
# get_tracer_provider()
|
294
299
|
def init_tracer_provider(resource: Resource) -> TracerProvider:
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
return
|
306
|
-
else:
|
307
|
-
provider = default_provider
|
300
|
+
tracer_provider = TracerProvider(
|
301
|
+
resource=resource,
|
302
|
+
span_limits=SpanLimits(
|
303
|
+
# this defaults to 128, which causes us to drop messages
|
304
|
+
max_attributes=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
|
305
|
+
max_span_attributes=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
|
306
|
+
max_event_attributes=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
|
307
|
+
max_events=MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN,
|
308
|
+
),
|
309
|
+
)
|
308
310
|
|
309
|
-
return
|
311
|
+
return tracer_provider
|
310
312
|
|
311
313
|
|
312
314
|
def init_instrumentations(
|
@@ -1,9 +1,15 @@
|
|
1
|
-
import opentelemetry
|
2
1
|
import uuid
|
3
2
|
import asyncio
|
4
3
|
import logging
|
5
4
|
import time
|
6
5
|
import os
|
6
|
+
import aiohttp
|
7
|
+
import requests
|
8
|
+
import threading
|
9
|
+
import gzip
|
10
|
+
import json
|
11
|
+
from lmnr.version import SDK_VERSION, PYTHON_VERSION
|
12
|
+
from lmnr import Laminar
|
7
13
|
|
8
14
|
logger = logging.getLogger(__name__)
|
9
15
|
|
@@ -28,80 +34,53 @@ with open(os.path.join(current_dir, "rrweb", "rrweb.min.js"), "r") as f:
|
|
28
34
|
RRWEB_CONTENT = f"() => {{ {f.read()} }}"
|
29
35
|
|
30
36
|
INJECT_PLACEHOLDER = """
|
31
|
-
(
|
32
|
-
const
|
33
|
-
|
34
|
-
const HEARTBEAT_INTERVAL = 1000;
|
35
|
-
|
37
|
+
() => {
|
38
|
+
const BATCH_SIZE = 1000; // Maximum events to store in memory
|
39
|
+
|
36
40
|
window.lmnrRrwebEventsBatch = [];
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
const
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
const compressedStream = await new Response(
|
53
|
-
new Response(uint8Array).body.pipeThrough(cs)
|
54
|
-
).arrayBuffer();
|
55
|
-
|
56
|
-
const compressedArray = new Uint8Array(compressedStream);
|
57
|
-
|
58
|
-
const blob = new Blob([compressedArray], { type: 'application/octet-stream' });
|
59
|
-
|
60
|
-
const response = await fetch(serverUrl, {
|
61
|
-
method: 'POST',
|
62
|
-
headers: {
|
63
|
-
'Content-Type': 'application/json',
|
64
|
-
'Content-Encoding': 'gzip',
|
65
|
-
'Authorization': `Bearer ${projectApiKey}`,
|
66
|
-
'Accept': 'application/json'
|
67
|
-
},
|
68
|
-
body: blob,
|
69
|
-
mode: 'cors',
|
70
|
-
credentials: 'omit'
|
71
|
-
});
|
72
|
-
|
73
|
-
if (!response.ok) {
|
74
|
-
console.error(`HTTP error! status: ${response.status}`);
|
75
|
-
if (response.status === 0) {
|
76
|
-
console.error('Possible CORS issue - check network tab for details');
|
77
|
-
}
|
78
|
-
}
|
79
|
-
|
80
|
-
window.lmnrRrwebEventsBatch = [];
|
81
|
-
} catch (error) {
|
82
|
-
console.error('Failed to send events:', error);
|
83
|
-
}
|
42
|
+
// Utility function to compress individual event data
|
43
|
+
async function compressEventData(data) {
|
44
|
+
const jsonString = JSON.stringify(data);
|
45
|
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
46
|
+
const compressedStream = blob.stream().pipeThrough(new CompressionStream('gzip'));
|
47
|
+
const compressedResponse = new Response(compressedStream);
|
48
|
+
const compressedData = await compressedResponse.arrayBuffer();
|
49
|
+
return Array.from(new Uint8Array(compressedData));
|
50
|
+
}
|
51
|
+
|
52
|
+
window.lmnrGetAndClearEvents = () => {
|
53
|
+
const events = window.lmnrRrwebEventsBatch;
|
54
|
+
window.lmnrRrwebEventsBatch = [];
|
55
|
+
return events;
|
84
56
|
};
|
85
57
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
window.lmnrRrwebEventsBatch.push({
|
58
|
+
// Add heartbeat events
|
59
|
+
setInterval(async () => {
|
60
|
+
const heartbeat = {
|
90
61
|
type: 6,
|
91
|
-
data: { source: 'heartbeat' },
|
62
|
+
data: await compressEventData({ source: 'heartbeat' }),
|
92
63
|
timestamp: Date.now()
|
93
|
-
}
|
94
|
-
|
64
|
+
};
|
65
|
+
|
66
|
+
window.lmnrRrwebEventsBatch.push(heartbeat);
|
67
|
+
|
68
|
+
// Prevent memory issues by limiting batch size
|
69
|
+
if (window.lmnrRrwebEventsBatch.length > BATCH_SIZE) {
|
70
|
+
window.lmnrRrwebEventsBatch = window.lmnrRrwebEventsBatch.slice(-BATCH_SIZE);
|
71
|
+
}
|
72
|
+
}, 1000);
|
95
73
|
|
96
74
|
window.lmnrRrweb.record({
|
97
|
-
emit(event) {
|
98
|
-
|
75
|
+
async emit(event) {
|
76
|
+
// Compress the data field
|
77
|
+
const compressedEvent = {
|
78
|
+
...event,
|
79
|
+
data: await compressEventData(event.data)
|
80
|
+
};
|
81
|
+
window.lmnrRrwebEventsBatch.push(compressedEvent);
|
99
82
|
}
|
100
83
|
});
|
101
|
-
|
102
|
-
window.addEventListener('beforeunload', () => {
|
103
|
-
window.lmnrSendRrwebEventsBatch();
|
104
|
-
});
|
105
84
|
}
|
106
85
|
"""
|
107
86
|
|
@@ -142,199 +121,248 @@ async def retry_async(func, retries=5, delay=0.5, error_message="Operation faile
|
|
142
121
|
return None
|
143
122
|
|
144
123
|
|
145
|
-
def
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
is_loaded = page.evaluate(
|
124
|
+
async def send_events_async(
|
125
|
+
page: Page, http_url: str, project_api_key: str, session_id: str, trace_id: str
|
126
|
+
):
|
127
|
+
"""Fetch events from the page and send them to the server"""
|
128
|
+
try:
|
129
|
+
# Check if function exists first
|
130
|
+
has_function = await page.evaluate(
|
153
131
|
"""
|
154
|
-
() => typeof window.
|
132
|
+
() => typeof window.lmnrGetAndClearEvents === 'function'
|
155
133
|
"""
|
156
134
|
)
|
135
|
+
if not has_function:
|
136
|
+
return
|
157
137
|
|
158
|
-
|
138
|
+
events = await page.evaluate("window.lmnrGetAndClearEvents()")
|
139
|
+
if not events or len(events) == 0:
|
140
|
+
return
|
159
141
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
142
|
+
payload = {
|
143
|
+
"sessionId": session_id,
|
144
|
+
"traceId": trace_id,
|
145
|
+
"events": events,
|
146
|
+
"source": f"python@{PYTHON_VERSION}",
|
147
|
+
"sdkVersion": SDK_VERSION,
|
148
|
+
}
|
149
|
+
|
150
|
+
headers = {
|
151
|
+
"Content-Type": "application/json",
|
152
|
+
"Authorization": f"Bearer {project_api_key}",
|
153
|
+
"Accept": "application/json",
|
154
|
+
}
|
155
|
+
|
156
|
+
async with aiohttp.ClientSession() as session:
|
157
|
+
async with session.post(
|
158
|
+
f"{http_url}/v1/browser-sessions/events",
|
159
|
+
json=payload,
|
160
|
+
headers=headers,
|
161
|
+
) as response:
|
162
|
+
if not response.ok:
|
163
|
+
logger.error(f"Failed to send events: {response.status}")
|
164
|
+
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(f"Error sending events: {e}")
|
167
|
+
|
168
|
+
|
169
|
+
def send_events_sync(
|
170
|
+
page: SyncPage, http_url: str, project_api_key: str, session_id: str, trace_id: str
|
171
|
+
):
|
172
|
+
"""Synchronous version of send_events"""
|
173
|
+
try:
|
174
|
+
# Check if function exists first
|
175
|
+
has_function = page.evaluate(
|
193
176
|
"""
|
194
|
-
)
|
177
|
+
() => typeof window.lmnrGetAndClearEvents === 'function'
|
178
|
+
"""
|
179
|
+
)
|
180
|
+
if not has_function:
|
181
|
+
return
|
195
182
|
|
196
|
-
|
197
|
-
|
198
|
-
):
|
183
|
+
events = page.evaluate("window.lmnrGetAndClearEvents()")
|
184
|
+
if not events or len(events) == 0:
|
199
185
|
return
|
200
186
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
187
|
+
payload = {
|
188
|
+
"sessionId": session_id,
|
189
|
+
"traceId": trace_id,
|
190
|
+
"events": events,
|
191
|
+
"source": f"python@{PYTHON_VERSION}",
|
192
|
+
"sdkVersion": SDK_VERSION,
|
193
|
+
}
|
206
194
|
|
207
|
-
|
208
|
-
|
209
|
-
|
195
|
+
headers = {
|
196
|
+
"Content-Type": "application/json",
|
197
|
+
"Authorization": f"Bearer {project_api_key}",
|
198
|
+
"Accept": "application/json",
|
199
|
+
"Content-Encoding": "gzip", # Add Content-Encoding header
|
200
|
+
}
|
210
201
|
|
211
|
-
#
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
202
|
+
# Compress the payload
|
203
|
+
compressed_payload = gzip.compress(json.dumps(payload).encode("utf-8"))
|
204
|
+
|
205
|
+
response = requests.post(
|
206
|
+
f"{http_url}/v1/browser-sessions/events",
|
207
|
+
data=compressed_payload, # Use data instead of json for raw bytes
|
208
|
+
headers=headers,
|
216
209
|
)
|
210
|
+
if not response.ok:
|
211
|
+
logger.error(f"Failed to send events: {response.status_code}")
|
212
|
+
|
213
|
+
except Exception as e:
|
214
|
+
logger.error(f"Error sending events: {e}")
|
215
|
+
|
216
|
+
|
217
|
+
def init_playwright_tracing(http_url: str, project_api_key: str):
|
217
218
|
|
218
|
-
|
219
|
+
def inject_rrweb(page: SyncPage):
|
220
|
+
try:
|
221
|
+
page.wait_for_load_state("domcontentloaded")
|
219
222
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
225
|
-
timeout=5000,
|
223
|
+
# Wrap the evaluate call in a try-catch
|
224
|
+
try:
|
225
|
+
is_loaded = page.evaluate(
|
226
|
+
"""() => typeof window.lmnrRrweb !== 'undefined'"""
|
226
227
|
)
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
[trace_id, session_id],
|
249
|
-
)
|
250
|
-
return await page.evaluate(
|
251
|
-
"""
|
252
|
-
() => window.lmnrRrwebSessionId && window.lmnrTraceId
|
253
|
-
"""
|
254
|
-
)
|
228
|
+
except Exception as e:
|
229
|
+
logger.debug(f"Failed to check if rrweb is loaded: {e}")
|
230
|
+
is_loaded = False
|
231
|
+
|
232
|
+
if not is_loaded:
|
233
|
+
def load_rrweb():
|
234
|
+
try:
|
235
|
+
page.evaluate(RRWEB_CONTENT)
|
236
|
+
page.wait_for_function(
|
237
|
+
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
238
|
+
timeout=5000,
|
239
|
+
)
|
240
|
+
return True
|
241
|
+
except Exception as e:
|
242
|
+
logger.debug(f"Failed to load rrweb: {e}")
|
243
|
+
return False
|
244
|
+
|
245
|
+
if not retry_sync(
|
246
|
+
load_rrweb, delay=1, error_message="Failed to load rrweb"
|
247
|
+
):
|
248
|
+
return
|
255
249
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
250
|
+
try:
|
251
|
+
page.evaluate(INJECT_PLACEHOLDER)
|
252
|
+
except Exception as e:
|
253
|
+
logger.debug(f"Failed to inject rrweb placeholder: {e}")
|
260
254
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
255
|
+
except Exception as e:
|
256
|
+
logger.error(f"Error during rrweb injection: {e}")
|
257
|
+
|
258
|
+
async def inject_rrweb_async(page: Page):
|
259
|
+
try:
|
260
|
+
await page.wait_for_load_state("domcontentloaded")
|
261
|
+
|
262
|
+
# Wrap the evaluate call in a try-catch
|
263
|
+
try:
|
264
|
+
is_loaded = await page.evaluate(
|
265
|
+
"""() => typeof window.lmnrRrweb !== 'undefined'"""
|
266
|
+
)
|
267
|
+
except Exception as e:
|
268
|
+
logger.debug(f"Failed to check if rrweb is loaded: {e}")
|
269
|
+
is_loaded = False
|
270
|
+
|
271
|
+
if not is_loaded:
|
272
|
+
async def load_rrweb():
|
273
|
+
try:
|
274
|
+
await page.evaluate(RRWEB_CONTENT)
|
275
|
+
await page.wait_for_function(
|
276
|
+
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
277
|
+
timeout=5000,
|
278
|
+
)
|
279
|
+
return True
|
280
|
+
except Exception as e:
|
281
|
+
logger.debug(f"Failed to load rrweb: {e}")
|
282
|
+
return False
|
283
|
+
|
284
|
+
if not await retry_async(
|
285
|
+
load_rrweb, delay=1, error_message="Failed to load rrweb"
|
286
|
+
):
|
287
|
+
return
|
288
|
+
|
289
|
+
try:
|
290
|
+
await page.evaluate(INJECT_PLACEHOLDER)
|
291
|
+
except Exception as e:
|
292
|
+
logger.debug(f"Failed to inject rrweb placeholder: {e}")
|
266
293
|
|
267
|
-
|
294
|
+
except Exception as e:
|
295
|
+
logger.error(f"Error during rrweb injection: {e}")
|
296
|
+
|
297
|
+
def handle_navigation(page: SyncPage, session_id: str, trace_id: str):
|
268
298
|
def on_load():
|
269
|
-
|
299
|
+
try:
|
300
|
+
inject_rrweb(page)
|
301
|
+
except Exception as e:
|
302
|
+
logger.error(f"Error in on_load handler: {e}")
|
270
303
|
|
271
304
|
page.on("load", on_load)
|
272
305
|
inject_rrweb(page)
|
273
306
|
|
274
|
-
|
307
|
+
def collection_loop():
|
308
|
+
while not page.is_closed(): # Stop when page closes
|
309
|
+
send_events_sync(page, http_url, project_api_key, session_id, trace_id)
|
310
|
+
time.sleep(2)
|
311
|
+
|
312
|
+
thread = threading.Thread(target=collection_loop, daemon=True)
|
313
|
+
thread.start()
|
314
|
+
|
315
|
+
async def handle_navigation_async(page: Page, session_id: str, trace_id: str):
|
275
316
|
async def on_load():
|
276
|
-
|
317
|
+
try:
|
318
|
+
await inject_rrweb_async(page)
|
319
|
+
except Exception as e:
|
320
|
+
logger.error(f"Error in on_load handler: {e}")
|
277
321
|
|
278
322
|
page.on("load", lambda: asyncio.create_task(on_load()))
|
279
323
|
await inject_rrweb_async(page)
|
280
324
|
|
281
|
-
|
282
|
-
# Modify CSP to allow required domains
|
283
|
-
async def handle_route(route):
|
325
|
+
async def collection_loop():
|
284
326
|
try:
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
csp = headers[header_name]
|
292
|
-
parts = csp.split(";")
|
293
|
-
for i, part in enumerate(parts):
|
294
|
-
if "connect-src" in part:
|
295
|
-
parts[i] = f"{part.strip()} {http_url}"
|
296
|
-
headers[header_name] = ";".join(parts)
|
297
|
-
|
298
|
-
await route.fulfill(response=response, headers=headers)
|
327
|
+
while not page.is_closed(): # Stop when page closes
|
328
|
+
await send_events_async(
|
329
|
+
page, http_url, project_api_key, session_id, trace_id
|
330
|
+
)
|
331
|
+
await asyncio.sleep(2)
|
332
|
+
logger.info("Event collection stopped")
|
299
333
|
except Exception as e:
|
300
|
-
logger.
|
301
|
-
|
334
|
+
logger.error(f"Event collection stopped: {e}")
|
335
|
+
|
336
|
+
# Create and store task
|
337
|
+
task = asyncio.create_task(collection_loop())
|
302
338
|
|
303
|
-
#
|
304
|
-
|
305
|
-
page = await _original_new_page_async(self, *args, **kwargs)
|
306
|
-
await handle_navigation_async(page)
|
307
|
-
return page
|
339
|
+
# Clean up task when page closes
|
340
|
+
page.on("close", lambda: task.cancel())
|
308
341
|
|
309
342
|
def patched_new_page(self: SyncBrowserContext, *args, **kwargs):
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
# Intercept all navigation requests to modify CSP headers
|
334
|
-
self.route("**/*", handle_route)
|
335
|
-
page = _original_new_page(self, *args, **kwargs)
|
336
|
-
handle_navigation(page)
|
337
|
-
return page
|
343
|
+
with Laminar.start_as_current_span(name="browser_context.new_page") as span:
|
344
|
+
page = _original_new_page(self, *args, **kwargs)
|
345
|
+
|
346
|
+
session_id = str(uuid.uuid4().hex)
|
347
|
+
span.set_attribute("lmnr.internal.has_browser_session", True)
|
348
|
+
|
349
|
+
trace_id = format(span.get_span_context().trace_id, "032x")
|
350
|
+
session_id = str(uuid.uuid4().hex)
|
351
|
+
|
352
|
+
handle_navigation(page, session_id, trace_id)
|
353
|
+
return page
|
354
|
+
|
355
|
+
async def patched_new_page_async(self: BrowserContext, *args, **kwargs):
|
356
|
+
with Laminar.start_as_current_span(name="browser_context.new_page") as span:
|
357
|
+
page = await _original_new_page_async(self, *args, **kwargs)
|
358
|
+
|
359
|
+
session_id = str(uuid.uuid4().hex)
|
360
|
+
|
361
|
+
span.set_attribute("lmnr.internal.has_browser_session", True)
|
362
|
+
trace_id = format(span.get_span_context().trace_id, "032x")
|
363
|
+
session_id = str(uuid.uuid4().hex)
|
364
|
+
await handle_navigation_async(page, session_id, trace_id)
|
365
|
+
return page
|
338
366
|
|
339
367
|
def patch_browser():
|
340
368
|
global _original_new_page, _original_new_page_async
|
lmnr/sdk/laminar.py
CHANGED
@@ -35,6 +35,7 @@ import requests
|
|
35
35
|
import re
|
36
36
|
import urllib.parse
|
37
37
|
import uuid
|
38
|
+
import warnings
|
38
39
|
|
39
40
|
from lmnr.openllmetry_sdk.tracing.attributes import (
|
40
41
|
SESSION_ID,
|
@@ -54,6 +55,7 @@ from .types import (
|
|
54
55
|
InitEvaluationResponse,
|
55
56
|
EvaluationResultDatapoint,
|
56
57
|
GetDatapointsResponse,
|
58
|
+
LaminarSpanContext,
|
57
59
|
PipelineRunError,
|
58
60
|
PipelineRunResponse,
|
59
61
|
NodeInput,
|
@@ -151,6 +153,14 @@ class Laminar:
|
|
151
153
|
cls.__initialized = True
|
152
154
|
cls._initialize_logger()
|
153
155
|
|
156
|
+
# if not is_latest_version():
|
157
|
+
# cls.__logger.warning(
|
158
|
+
# "You are using an older version of the Laminar SDK. "
|
159
|
+
# f"Latest version: {get_latest_pypi_version()}, current version: {SDK_VERSION}.\n"
|
160
|
+
# "Please update to the latest version by running "
|
161
|
+
# "`pip install --upgrade lmnr`."
|
162
|
+
# )
|
163
|
+
|
154
164
|
Traceloop.init(
|
155
165
|
base_http_url=cls.__base_http_url,
|
156
166
|
project_api_key=cls.__project_api_key,
|
@@ -338,8 +348,10 @@ class Laminar:
|
|
338
348
|
Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]
|
339
349
|
] = "DEFAULT",
|
340
350
|
context: Optional[Context] = None,
|
341
|
-
trace_id: Optional[uuid.UUID] = None,
|
342
351
|
labels: Optional[dict[str, str]] = None,
|
352
|
+
parent_span_context: Optional[LaminarSpanContext] = None,
|
353
|
+
# deprecated, use parent_span_context instead
|
354
|
+
trace_id: Optional[uuid.UUID] = None,
|
343
355
|
):
|
344
356
|
"""Start a new span as the current span. Useful for manual
|
345
357
|
instrumentation. If `span_type` is set to `"LLM"`, you should report
|
@@ -362,11 +374,21 @@ class Laminar:
|
|
362
374
|
and response attributes manually. Defaults to "DEFAULT".
|
363
375
|
context (Optional[Context], optional): raw OpenTelemetry context\
|
364
376
|
to attach the span to. Defaults to None.
|
365
|
-
|
366
|
-
|
367
|
-
|
377
|
+
parent_span_context (Optional[LaminarSpanContext], optional): parent\
|
378
|
+
span context to use for the span. Useful for continuing traces\
|
379
|
+
across services. If parent_span_context is a\
|
380
|
+
raw OpenTelemetry span context, or if it is a dictionary or string\
|
381
|
+
obtained from `Laminar.get_laminar_span_context_dict()` or\
|
382
|
+
`Laminar.get_laminar_span_context_str()` respectively, it will be\
|
383
|
+
converted to a `LaminarSpanContext` if possible. See also\
|
384
|
+
`Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
|
385
|
+
`Laminar.get_span_context_str` for more information.
|
386
|
+
Defaults to None.
|
368
387
|
labels (Optional[dict[str, str]], optional): labels to set for the\
|
369
388
|
span. Defaults to None.
|
389
|
+
trace_id (Optional[uuid.UUID], optional): [Deprecated] override\
|
390
|
+
the trace id for the span. If not provided, use the current\
|
391
|
+
trace id. Defaults to None.
|
370
392
|
"""
|
371
393
|
|
372
394
|
if not cls.is_initialized():
|
@@ -376,21 +398,29 @@ class Laminar:
|
|
376
398
|
with get_tracer() as tracer:
|
377
399
|
ctx = context or context_api.get_current()
|
378
400
|
if trace_id is not None:
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
401
|
+
warnings.warn(
|
402
|
+
"trace_id provided to `Laminar.start_as_current_span`"
|
403
|
+
" is deprecated, use parent_span_context instead",
|
404
|
+
DeprecationWarning,
|
405
|
+
stacklevel=2,
|
406
|
+
)
|
407
|
+
if parent_span_context is not None:
|
408
|
+
span_context = LaminarSpanContext.try_to_otel_span_context(
|
409
|
+
parent_span_context, cls.__logger
|
410
|
+
)
|
411
|
+
ctx = trace.set_span_in_context(
|
412
|
+
trace.NonRecordingSpan(span_context), ctx
|
413
|
+
)
|
414
|
+
elif trace_id is not None and isinstance(trace_id, uuid.UUID):
|
415
|
+
span_context = trace.SpanContext(
|
416
|
+
trace_id=int(trace_id),
|
417
|
+
span_id=random.getrandbits(64),
|
418
|
+
is_remote=False,
|
419
|
+
trace_flags=trace.TraceFlags(trace.TraceFlags.SAMPLED),
|
420
|
+
)
|
421
|
+
ctx = trace.set_span_in_context(
|
422
|
+
trace.NonRecordingSpan(span_context), ctx
|
423
|
+
)
|
394
424
|
ctx_token = attach(ctx)
|
395
425
|
label_props = {}
|
396
426
|
try:
|
@@ -478,8 +508,10 @@ class Laminar:
|
|
478
508
|
Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]
|
479
509
|
] = "DEFAULT",
|
480
510
|
context: Optional[Context] = None,
|
481
|
-
|
511
|
+
parent_span_context: Optional[LaminarSpanContext] = None,
|
482
512
|
labels: Optional[dict[str, str]] = None,
|
513
|
+
# deprecated, use parent_span_context instead
|
514
|
+
trace_id: Optional[uuid.UUID] = None,
|
483
515
|
):
|
484
516
|
"""Start a new span. Useful for manual instrumentation.
|
485
517
|
If `span_type` is set to `"LLM"`, you should report usage and response
|
@@ -521,30 +553,48 @@ class Laminar:
|
|
521
553
|
and response attributes manually. Defaults to "DEFAULT".
|
522
554
|
context (Optional[Context], optional): raw OpenTelemetry context\
|
523
555
|
to attach the span to. Defaults to None.
|
524
|
-
|
525
|
-
|
526
|
-
|
556
|
+
parent_span_context (Optional[LaminarSpanContext], optional): parent\
|
557
|
+
span context to use for the span. Useful for continuing traces\
|
558
|
+
across services. If parent_span_context is a\
|
559
|
+
raw OpenTelemetry span context, or if it is a dictionary or string\
|
560
|
+
obtained from `Laminar.get_laminar_span_context_dict()` or\
|
561
|
+
`Laminar.get_laminar_span_context_str()` respectively, it will be\
|
562
|
+
converted to a `LaminarSpanContext` if possible. See also\
|
563
|
+
`Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
|
564
|
+
`Laminar.get_span_context_str` for more information.
|
565
|
+
Defaults to None.
|
527
566
|
labels (Optional[dict[str, str]], optional): labels to set for the\
|
528
567
|
span. Defaults to None.
|
568
|
+
trace_id (Optional[uuid.UUID], optional): Deprecated, use\
|
569
|
+
`parent_span_context` instead. If provided, it will be used to\
|
570
|
+
set the trace id for the span.
|
529
571
|
"""
|
530
572
|
with get_tracer() as tracer:
|
531
573
|
ctx = context or context_api.get_current()
|
532
574
|
if trace_id is not None:
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
575
|
+
warnings.warn(
|
576
|
+
"trace_id provided to `Laminar.start_span`"
|
577
|
+
" is deprecated, use parent_span_context instead",
|
578
|
+
DeprecationWarning,
|
579
|
+
stacklevel=2,
|
580
|
+
)
|
581
|
+
if parent_span_context is not None:
|
582
|
+
span_context = LaminarSpanContext.try_to_otel_span_context(
|
583
|
+
parent_span_context, cls.__logger
|
584
|
+
)
|
585
|
+
ctx = trace.set_span_in_context(
|
586
|
+
trace.NonRecordingSpan(span_context), ctx
|
587
|
+
)
|
588
|
+
elif trace_id is not None and isinstance(trace_id, uuid.UUID):
|
589
|
+
span_context = trace.SpanContext(
|
590
|
+
trace_id=int(trace_id),
|
591
|
+
span_id=random.getrandbits(64),
|
592
|
+
is_remote=False,
|
593
|
+
trace_flags=trace.TraceFlags(trace.TraceFlags.SAMPLED),
|
594
|
+
)
|
595
|
+
ctx = trace.set_span_in_context(
|
596
|
+
trace.NonRecordingSpan(span_context), ctx
|
597
|
+
)
|
548
598
|
label_props = {}
|
549
599
|
try:
|
550
600
|
if labels:
|
@@ -679,6 +729,77 @@ class Laminar:
|
|
679
729
|
else:
|
680
730
|
span.set_attribute(key.value, value)
|
681
731
|
|
732
|
+
@classmethod
|
733
|
+
def get_laminar_span_context(
|
734
|
+
cls, span: Optional[trace.Span] = None
|
735
|
+
) -> Optional[LaminarSpanContext]:
|
736
|
+
"""Get the laminar span context for a given span.
|
737
|
+
If no span is provided, the current active span will be used.
|
738
|
+
"""
|
739
|
+
span = span or trace.get_current_span()
|
740
|
+
if span == trace.INVALID_SPAN:
|
741
|
+
return None
|
742
|
+
return LaminarSpanContext(
|
743
|
+
trace_id=uuid.UUID(int=span.get_span_context().trace_id),
|
744
|
+
span_id=uuid.UUID(int=span.get_span_context().span_id),
|
745
|
+
is_remote=span.get_span_context().is_remote,
|
746
|
+
)
|
747
|
+
|
748
|
+
@classmethod
|
749
|
+
def get_laminar_span_context_dict(
|
750
|
+
cls, span: Optional[trace.Span] = None
|
751
|
+
) -> Optional[dict]:
|
752
|
+
"""Get the laminar span context for a given span as a dictionary.
|
753
|
+
If no span is provided, the current active span will be used.
|
754
|
+
|
755
|
+
This is useful for continuing a trace across services.
|
756
|
+
|
757
|
+
Example:
|
758
|
+
```python
|
759
|
+
# service A:
|
760
|
+
with Laminar.start_as_current_span("service_a"):
|
761
|
+
span_context = Laminar.get_laminar_span_context_dict()
|
762
|
+
# send span_context to service B
|
763
|
+
call_service_b(request, headers={"laminar-span-context": span_context})
|
764
|
+
|
765
|
+
# service B:
|
766
|
+
def call_service_b(request, headers):
|
767
|
+
span_context = LaminarSpanContext.from_dict(headers["laminar-span-context"])
|
768
|
+
with Laminar.start_as_current_span("service_b", parent_span_context=span_context):
|
769
|
+
# rest of the function
|
770
|
+
pass
|
771
|
+
```
|
772
|
+
|
773
|
+
This will result in a trace like:
|
774
|
+
```
|
775
|
+
service_a
|
776
|
+
service_b
|
777
|
+
```
|
778
|
+
"""
|
779
|
+
span_context = cls.get_laminar_span_context(span)
|
780
|
+
if span_context is None:
|
781
|
+
return None
|
782
|
+
return span_context.to_dict()
|
783
|
+
|
784
|
+
@classmethod
|
785
|
+
def get_laminar_span_context_str(
|
786
|
+
cls, span: Optional[trace.Span] = None
|
787
|
+
) -> Optional[str]:
|
788
|
+
span_context = cls.get_laminar_span_context(span)
|
789
|
+
if span_context is None:
|
790
|
+
return None
|
791
|
+
return json.dumps(span_context.to_dict())
|
792
|
+
|
793
|
+
@classmethod
|
794
|
+
def deserialize_laminar_span_context(
|
795
|
+
cls, span_context: Union[dict, str]
|
796
|
+
) -> LaminarSpanContext:
|
797
|
+
return LaminarSpanContext.deserialize(span_context)
|
798
|
+
|
799
|
+
@classmethod
|
800
|
+
def shutdown(cls):
|
801
|
+
Traceloop.flush()
|
802
|
+
|
682
803
|
@classmethod
|
683
804
|
def set_session(
|
684
805
|
cls,
|
lmnr/sdk/types.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
import logging
|
1
2
|
import aiohttp
|
2
3
|
import datetime
|
3
4
|
from enum import Enum
|
5
|
+
import json
|
6
|
+
from opentelemetry.trace import SpanContext, TraceFlags
|
4
7
|
import pydantic
|
5
8
|
from typing import Any, Awaitable, Callable, Optional, Union
|
6
9
|
import uuid
|
@@ -210,3 +213,86 @@ class TracingLevel(Enum):
|
|
210
213
|
OFF = 0
|
211
214
|
META_ONLY = 1
|
212
215
|
ALL = 2
|
216
|
+
|
217
|
+
|
218
|
+
class LaminarSpanContext(pydantic.BaseModel):
|
219
|
+
"""
|
220
|
+
A span context that can be used to continue a trace across services. This
|
221
|
+
is a slightly modified version of the OpenTelemetry span context. For
|
222
|
+
usage examples, see `Laminar.get_laminar_span_context_dict`,
|
223
|
+
`Laminar.get_laminar_span_context_str`, `Laminar.get_span_context`, and
|
224
|
+
`Laminar.deserialize_laminar_span_context`.
|
225
|
+
|
226
|
+
The difference between this and the OpenTelemetry span context is that
|
227
|
+
the `trace_id` and `span_id` are stored as UUIDs instead of integers for
|
228
|
+
easier debugging, and the separate trace flags are not currently stored.
|
229
|
+
"""
|
230
|
+
|
231
|
+
trace_id: uuid.UUID
|
232
|
+
span_id: uuid.UUID
|
233
|
+
is_remote: bool = pydantic.Field(default=False)
|
234
|
+
|
235
|
+
# uuid is not serializable by default, so we need to convert it to a string
|
236
|
+
def to_dict(self):
|
237
|
+
return {
|
238
|
+
"traceId": str(self.trace_id),
|
239
|
+
"spanId": str(self.span_id),
|
240
|
+
"isRemote": self.is_remote,
|
241
|
+
}
|
242
|
+
|
243
|
+
@classmethod
|
244
|
+
def from_dict(cls, data: dict[str, Any]) -> "LaminarSpanContext":
|
245
|
+
return cls(
|
246
|
+
trace_id=uuid.UUID(data["traceId"]),
|
247
|
+
span_id=uuid.UUID(data["spanId"]),
|
248
|
+
is_remote=data["isRemote"],
|
249
|
+
)
|
250
|
+
|
251
|
+
@classmethod
|
252
|
+
def try_to_otel_span_context(
|
253
|
+
cls,
|
254
|
+
span_context: Union["LaminarSpanContext", dict[str, Any], str, SpanContext],
|
255
|
+
logger: Optional[logging.Logger] = None,
|
256
|
+
) -> SpanContext:
|
257
|
+
if logger is None:
|
258
|
+
logger = logging.getLogger(__name__)
|
259
|
+
|
260
|
+
if isinstance(span_context, LaminarSpanContext):
|
261
|
+
return SpanContext(
|
262
|
+
trace_id=span_context.trace_id.int,
|
263
|
+
span_id=span_context.span_id.int,
|
264
|
+
is_remote=span_context.is_remote,
|
265
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
266
|
+
)
|
267
|
+
elif isinstance(span_context, SpanContext) or (
|
268
|
+
isinstance(getattr(span_context, "trace_id", None), int)
|
269
|
+
and isinstance(getattr(span_context, "span_id", None), int)
|
270
|
+
):
|
271
|
+
logger.warning(
|
272
|
+
"span_context provided"
|
273
|
+
" is likely a raw OpenTelemetry span context. Will try to use it. "
|
274
|
+
"Please use `LaminarSpanContext` instead."
|
275
|
+
)
|
276
|
+
return span_context
|
277
|
+
elif isinstance(span_context, dict) or isinstance(span_context, str):
|
278
|
+
try:
|
279
|
+
laminar_span_context = cls.deserialize(span_context)
|
280
|
+
return SpanContext(
|
281
|
+
trace_id=laminar_span_context.trace_id.int,
|
282
|
+
span_id=laminar_span_context.span_id.int,
|
283
|
+
is_remote=laminar_span_context.is_remote,
|
284
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
285
|
+
)
|
286
|
+
except Exception:
|
287
|
+
raise ValueError("Invalid span_context provided")
|
288
|
+
else:
|
289
|
+
raise ValueError("Invalid span_context provided")
|
290
|
+
|
291
|
+
@classmethod
|
292
|
+
def deserialize(cls, data: Union[dict[str, Any], str]) -> "LaminarSpanContext":
|
293
|
+
if isinstance(data, dict):
|
294
|
+
return cls.from_dict(data)
|
295
|
+
elif isinstance(data, str):
|
296
|
+
return cls.from_dict(json.loads(data))
|
297
|
+
else:
|
298
|
+
raise ValueError("Invalid span_context provided")
|
lmnr/version.py
CHANGED
@@ -1,5 +1,46 @@
|
|
1
1
|
import sys
|
2
|
+
import requests
|
3
|
+
from packaging import version
|
2
4
|
|
3
5
|
|
4
|
-
SDK_VERSION = "0.4.
|
6
|
+
SDK_VERSION = "0.4.63"
|
5
7
|
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
|
8
|
+
|
9
|
+
|
10
|
+
def is_latest_version() -> bool:
|
11
|
+
try:
|
12
|
+
return version.parse(SDK_VERSION) >= version.parse(get_latest_pypi_version())
|
13
|
+
except Exception:
|
14
|
+
return True
|
15
|
+
|
16
|
+
|
17
|
+
def get_latest_pypi_version() -> str:
|
18
|
+
"""
|
19
|
+
Get the latest stable version of lmnr package from PyPI.
|
20
|
+
Returns the version string or raises an exception if unable to fetch.
|
21
|
+
"""
|
22
|
+
try:
|
23
|
+
response = requests.get("https://pypi.org/pypi/lmnr/json")
|
24
|
+
response.raise_for_status()
|
25
|
+
|
26
|
+
releases = response.json()["releases"]
|
27
|
+
stable_versions = [
|
28
|
+
ver
|
29
|
+
for ver in releases.keys()
|
30
|
+
if not version.parse(ver).is_prerelease
|
31
|
+
and not version.parse(ver).is_devrelease
|
32
|
+
and not any(release.get("yanked", False) for release in releases[ver])
|
33
|
+
]
|
34
|
+
|
35
|
+
if not stable_versions:
|
36
|
+
# do not scare the user, assume they are on
|
37
|
+
# latest version
|
38
|
+
return SDK_VERSION
|
39
|
+
|
40
|
+
latest_version = max(stable_versions, key=version.parse)
|
41
|
+
return latest_version
|
42
|
+
|
43
|
+
except Exception:
|
44
|
+
# do not scare the user, assume they are on
|
45
|
+
# latest version
|
46
|
+
return SDK_VERSION
|
@@ -1,7 +1,7 @@
|
|
1
|
-
lmnr/__init__.py,sha256=
|
1
|
+
lmnr/__init__.py,sha256=UiFNMZL5IqflaxgAv5FXcbvFmw4xCQbF6RwXlNYmAAU,494
|
2
2
|
lmnr/cli.py,sha256=4J2RZQhHM3jJcjFvBC4PChQTS-ukxykVvI0X6lTkK-o,2918
|
3
3
|
lmnr/openllmetry_sdk/.flake8,sha256=bCxuDlGx3YQ55QHKPiGJkncHanh9qGjQJUujcFa3lAU,150
|
4
|
-
lmnr/openllmetry_sdk/__init__.py,sha256=
|
4
|
+
lmnr/openllmetry_sdk/__init__.py,sha256=Nz20nI8tzeJcoLavPeemqHdHFUQ9-VoB9MBF9m_qoZs,2722
|
5
5
|
lmnr/openllmetry_sdk/config/__init__.py,sha256=5aGdIdo1LffBkNwIBUbqzN6OUCMCrURU4b0rf5LBSI0,300
|
6
6
|
lmnr/openllmetry_sdk/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
7
|
lmnr/openllmetry_sdk/decorators/base.py,sha256=RNyvSreGBwwh8EWaa3O9samrnmKoZHK7eBzGOPT3lHc,7132
|
@@ -10,7 +10,7 @@ lmnr/openllmetry_sdk/tracing/__init__.py,sha256=xT73L1t2si2CM6QmMiTZ7zn-dKKYBLNr
|
|
10
10
|
lmnr/openllmetry_sdk/tracing/attributes.py,sha256=cLBmSp4AMv9E91Ck3yD5Z1Qx1L5ZRV-80VJxFA-sO0Q,1426
|
11
11
|
lmnr/openllmetry_sdk/tracing/content_allow_list.py,sha256=3feztm6PBWNelc8pAZUcQyEGyeSpNiVKjOaDk65l2ps,846
|
12
12
|
lmnr/openllmetry_sdk/tracing/context_manager.py,sha256=rdSus-p-TaevQ8hIAhfbnZr5dTqRvACDkzXGDpflncY,306
|
13
|
-
lmnr/openllmetry_sdk/tracing/tracing.py,sha256=
|
13
|
+
lmnr/openllmetry_sdk/tracing/tracing.py,sha256=TaVNtw3kQNP-5QxKVXOw2wbSe21G8e8-nAbajXxFXPI,34113
|
14
14
|
lmnr/openllmetry_sdk/utils/__init__.py,sha256=pNhf0G3vTd5ccoc03i1MXDbricSaiqCbi1DLWhSekK8,604
|
15
15
|
lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
|
16
16
|
lmnr/openllmetry_sdk/utils/json_encoder.py,sha256=dK6b_axr70IYL7Vv-bu4wntvDDuyntoqsHaddqX7P58,463
|
@@ -18,19 +18,19 @@ lmnr/openllmetry_sdk/utils/package_check.py,sha256=_-Fu9Zbp9tOyy27_-Rul7tDc8JaXY
|
|
18
18
|
lmnr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
20
|
lmnr/sdk/browser/__init__.py,sha256=NSP5sB-dm-f0FP70_GMvVrNFwc5rHf7SW0_Oisyo3cE,343
|
21
|
-
lmnr/sdk/browser/playwright_patch.py,sha256=
|
21
|
+
lmnr/sdk/browser/playwright_patch.py,sha256=wfbkFXYMl6VmsxzLfaArmuCKtASnd3hUtlL7Shj6lss,13071
|
22
22
|
lmnr/sdk/browser/rrweb/rrweb.min.js,sha256=X5pgaoX1j_OjKTqGQgKB-83xUSuydNLQa-Kkh1AAZYM,140485
|
23
23
|
lmnr/sdk/datasets.py,sha256=hJcQcwTJbtA4COoVG3god4xll9TBSDMfvrhKmMfanjg,1567
|
24
24
|
lmnr/sdk/decorators.py,sha256=g0VBqUEMCPRbgjgGHauVuKK1wHEd9rkiGzlYUYrcml4,2336
|
25
25
|
lmnr/sdk/eval_control.py,sha256=G6Fg3Xx_KWv72iBaWlNMdyRTF2bZFQnwJ68sJNSpIcY,177
|
26
26
|
lmnr/sdk/evaluations.py,sha256=WmPAgQVm2C83xsG-qMdPrMuvCMFMoVdO37LqZtLc8xw,18702
|
27
|
-
lmnr/sdk/laminar.py,sha256=
|
27
|
+
lmnr/sdk/laminar.py,sha256=1KkEq8DDogobtrFwgTOo71hDptX1oMMkDcYn973S-eU,38717
|
28
28
|
lmnr/sdk/log.py,sha256=nt_YMmPw1IRbGy0b7q4rTtP4Yo3pQfNxqJPXK3nDSNQ,2213
|
29
|
-
lmnr/sdk/types.py,sha256=
|
29
|
+
lmnr/sdk/types.py,sha256=NUVAIZAjjyWB6Ze7naa3Vx9g14Em6OtIXsSNSCJWISE,9892
|
30
30
|
lmnr/sdk/utils.py,sha256=sD1YEqhdPaHweY2VGmjMF9MC-X7Ikdc49E01D-HF77E,3377
|
31
|
-
lmnr/version.py,sha256=
|
32
|
-
lmnr-0.4.
|
33
|
-
lmnr-0.4.
|
34
|
-
lmnr-0.4.
|
35
|
-
lmnr-0.4.
|
36
|
-
lmnr-0.4.
|
31
|
+
lmnr/version.py,sha256=p-p1byXt34iI6GQ66e7VKFuYQ2oQw2NJDAhhok0qD4M,1328
|
32
|
+
lmnr-0.4.63.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
|
33
|
+
lmnr-0.4.63.dist-info/METADATA,sha256=4W-ucM1arTuxaQnThJempZ0ByTlp2gUkIR7pgaYHYGo,13827
|
34
|
+
lmnr-0.4.63.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
35
|
+
lmnr-0.4.63.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
|
36
|
+
lmnr-0.4.63.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|