lmnr 0.4.60__py3-none-any.whl → 0.4.62__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/openllmetry_sdk/utils/package_check.py +1 -1
- lmnr/sdk/browser/playwright_patch.py +253 -225
- lmnr/sdk/laminar.py +9 -0
- lmnr/version.py +42 -1
- {lmnr-0.4.60.dist-info → lmnr-0.4.62.dist-info}/METADATA +1 -1
- {lmnr-0.4.60.dist-info → lmnr-0.4.62.dist-info}/RECORD +9 -10
- {lmnr-0.4.60.dist-info → lmnr-0.4.62.dist-info}/WHEEL +1 -1
- lmnr/openllmetry_sdk/.python-version +0 -1
- {lmnr-0.4.60.dist-info → lmnr-0.4.62.dist-info}/LICENSE +0 -0
- {lmnr-0.4.60.dist-info → lmnr-0.4.62.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
from importlib.metadata import distributions
|
2
2
|
|
3
|
-
installed_packages = {dist.metadata
|
3
|
+
installed_packages = {dist.metadata.get("Name", "").lower() for dist in distributions()}
|
4
4
|
|
5
5
|
|
6
6
|
def is_package_installed(package_name: str) -> bool:
|
@@ -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
@@ -13,6 +13,7 @@ from lmnr.openllmetry_sdk.config import MAX_MANUAL_SPAN_PAYLOAD_SIZE
|
|
13
13
|
from lmnr.openllmetry_sdk.decorators.base import json_dumps
|
14
14
|
from opentelemetry import context as context_api, trace
|
15
15
|
from opentelemetry.context import attach, detach
|
16
|
+
from lmnr.version import SDK_VERSION, get_latest_pypi_version, is_latest_version
|
16
17
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
17
18
|
OTLPSpanExporter,
|
18
19
|
Compression,
|
@@ -151,6 +152,14 @@ class Laminar:
|
|
151
152
|
cls.__initialized = True
|
152
153
|
cls._initialize_logger()
|
153
154
|
|
155
|
+
# if not is_latest_version():
|
156
|
+
# cls.__logger.warning(
|
157
|
+
# "You are using an older version of the Laminar SDK. "
|
158
|
+
# f"Latest version: {get_latest_pypi_version()}, current version: {SDK_VERSION}.\n"
|
159
|
+
# "Please update to the latest version by running "
|
160
|
+
# "`pip install --upgrade lmnr`."
|
161
|
+
# )
|
162
|
+
|
154
163
|
Traceloop.init(
|
155
164
|
base_http_url=cls.__base_http_url,
|
156
165
|
project_api_key=cls.__project_api_key,
|
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.62"
|
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,6 @@
|
|
1
1
|
lmnr/__init__.py,sha256=Bqxs-8Mh4h69pOHURgBCgo9EW1GwChebxP6wUX2-bsU,452
|
2
2
|
lmnr/cli.py,sha256=4J2RZQhHM3jJcjFvBC4PChQTS-ukxykVvI0X6lTkK-o,2918
|
3
3
|
lmnr/openllmetry_sdk/.flake8,sha256=bCxuDlGx3YQ55QHKPiGJkncHanh9qGjQJUujcFa3lAU,150
|
4
|
-
lmnr/openllmetry_sdk/.python-version,sha256=9OLQBQVbD4zE4cJsPePhnAfV_snrPSoqEQw-PXgPMOs,6
|
5
4
|
lmnr/openllmetry_sdk/__init__.py,sha256=TpFNPrRosz-BUpWdfT9ROiZPTGA_JshNwqOfiXlR0MU,2643
|
6
5
|
lmnr/openllmetry_sdk/config/__init__.py,sha256=5aGdIdo1LffBkNwIBUbqzN6OUCMCrURU4b0rf5LBSI0,300
|
7
6
|
lmnr/openllmetry_sdk/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -15,23 +14,23 @@ lmnr/openllmetry_sdk/tracing/tracing.py,sha256=aQsQRI4NKl3K3e0W7TZvbNqtdiWngbmj7
|
|
15
14
|
lmnr/openllmetry_sdk/utils/__init__.py,sha256=pNhf0G3vTd5ccoc03i1MXDbricSaiqCbi1DLWhSekK8,604
|
16
15
|
lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
|
17
16
|
lmnr/openllmetry_sdk/utils/json_encoder.py,sha256=dK6b_axr70IYL7Vv-bu4wntvDDuyntoqsHaddqX7P58,463
|
18
|
-
lmnr/openllmetry_sdk/utils/package_check.py,sha256=
|
17
|
+
lmnr/openllmetry_sdk/utils/package_check.py,sha256=_-Fu9Zbp9tOyy27_-Rul7tDc8JaXYR2FmqF8SWOXSCc,244
|
19
18
|
lmnr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
19
|
lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
20
|
lmnr/sdk/browser/__init__.py,sha256=NSP5sB-dm-f0FP70_GMvVrNFwc5rHf7SW0_Oisyo3cE,343
|
22
|
-
lmnr/sdk/browser/playwright_patch.py,sha256=
|
21
|
+
lmnr/sdk/browser/playwright_patch.py,sha256=wfbkFXYMl6VmsxzLfaArmuCKtASnd3hUtlL7Shj6lss,13071
|
23
22
|
lmnr/sdk/browser/rrweb/rrweb.min.js,sha256=X5pgaoX1j_OjKTqGQgKB-83xUSuydNLQa-Kkh1AAZYM,140485
|
24
23
|
lmnr/sdk/datasets.py,sha256=hJcQcwTJbtA4COoVG3god4xll9TBSDMfvrhKmMfanjg,1567
|
25
24
|
lmnr/sdk/decorators.py,sha256=g0VBqUEMCPRbgjgGHauVuKK1wHEd9rkiGzlYUYrcml4,2336
|
26
25
|
lmnr/sdk/eval_control.py,sha256=G6Fg3Xx_KWv72iBaWlNMdyRTF2bZFQnwJ68sJNSpIcY,177
|
27
26
|
lmnr/sdk/evaluations.py,sha256=WmPAgQVm2C83xsG-qMdPrMuvCMFMoVdO37LqZtLc8xw,18702
|
28
|
-
lmnr/sdk/laminar.py,sha256=
|
27
|
+
lmnr/sdk/laminar.py,sha256=0t6hSryO8tNALe9gf32O56K2l2e7j1LoGZv6twkitk0,33910
|
29
28
|
lmnr/sdk/log.py,sha256=nt_YMmPw1IRbGy0b7q4rTtP4Yo3pQfNxqJPXK3nDSNQ,2213
|
30
29
|
lmnr/sdk/types.py,sha256=Y4msdSM_IvQ5LOfV2jvk4R0-6skW5Ilml466a6swul4,6506
|
31
30
|
lmnr/sdk/utils.py,sha256=sD1YEqhdPaHweY2VGmjMF9MC-X7Ikdc49E01D-HF77E,3377
|
32
|
-
lmnr/version.py,sha256=
|
33
|
-
lmnr-0.4.
|
34
|
-
lmnr-0.4.
|
35
|
-
lmnr-0.4.
|
36
|
-
lmnr-0.4.
|
37
|
-
lmnr-0.4.
|
31
|
+
lmnr/version.py,sha256=g4oOxngHz-K-Zl-f8U2Q90d4OC8L6Kb9cwsj7eCX60A,1328
|
32
|
+
lmnr-0.4.62.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
|
33
|
+
lmnr-0.4.62.dist-info/METADATA,sha256=S5FLOzyb86OdZw1Z9AQ_5gPpR-Lwl_RYCe-RqbhocXc,13827
|
34
|
+
lmnr-0.4.62.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
35
|
+
lmnr-0.4.62.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
|
36
|
+
lmnr-0.4.62.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
3.9.5
|
File without changes
|
File without changes
|