lmnr 0.4.61__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.
@@ -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
- ([baseUrl, projectApiKey]) => {
32
- const serverUrl = `${baseUrl}/v1/browser-sessions/events`;
33
- const FLUSH_INTERVAL = 1000;
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
- window.lmnrSendRrwebEventsBatch = async () => {
39
- if (window.lmnrRrwebEventsBatch.length === 0) return;
40
-
41
- const eventsPayload = {
42
- sessionId: window.lmnrRrwebSessionId,
43
- traceId: window.lmnrTraceId,
44
- events: window.lmnrRrwebEventsBatch
45
- };
46
-
47
- try {
48
- const jsonString = JSON.stringify(eventsPayload);
49
- const uint8Array = new TextEncoder().encode(jsonString);
50
-
51
- const cs = new CompressionStream('gzip');
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
- setInterval(() => window.lmnrSendRrwebEventsBatch(), FLUSH_INTERVAL);
87
-
88
- setInterval(() => {
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
- }, HEARTBEAT_INTERVAL);
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
- window.lmnrRrwebEventsBatch.push(event);
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 init_playwright_tracing(http_url: str, project_api_key: str):
146
-
147
- def inject_rrweb(page: SyncPage):
148
- # Wait for the page to be in a ready state first
149
- page.wait_for_load_state("domcontentloaded")
150
-
151
- # First check if rrweb is already loaded
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.lmnrRrweb !== 'undefined'
132
+ () => typeof window.lmnrGetAndClearEvents === 'function'
155
133
  """
156
134
  )
135
+ if not has_function:
136
+ return
157
137
 
158
- if not is_loaded:
138
+ events = await page.evaluate("window.lmnrGetAndClearEvents()")
139
+ if not events or len(events) == 0:
140
+ return
159
141
 
160
- def load_rrweb():
161
- page.evaluate(RRWEB_CONTENT)
162
- # Verify script loaded successfully
163
- page.wait_for_function(
164
- """(() => typeof window.lmnrRrweb !== 'undefined')""",
165
- timeout=5000,
166
- )
167
- return True
168
-
169
- if not retry_sync(
170
- load_rrweb, delay=1, error_message="Failed to load rrweb"
171
- ):
172
- return
173
-
174
- # Get current trace ID from active span
175
- current_span = opentelemetry.trace.get_current_span()
176
- if current_span.is_recording():
177
- current_span.set_attribute("lmnr.internal.has_browser_session", True)
178
-
179
- trace_id = format(current_span.get_span_context().trace_id, "032x")
180
- session_id = str(uuid.uuid4().hex)
181
-
182
- def set_window_vars():
183
- page.evaluate(
184
- """([traceId, sessionId]) => {
185
- window.lmnrRrwebSessionId = sessionId;
186
- window.lmnrTraceId = traceId;
187
- }""",
188
- [trace_id, session_id],
189
- )
190
- return page.evaluate(
191
- """
192
- () => window.lmnrRrwebSessionId && window.lmnrTraceId
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
- if not retry_sync(
197
- set_window_vars, error_message="Failed to set window variables"
198
- ):
183
+ events = page.evaluate("window.lmnrGetAndClearEvents()")
184
+ if not events or len(events) == 0:
199
185
  return
200
186
 
201
- # Update the recording setup to include trace ID
202
- page.evaluate(
203
- INJECT_PLACEHOLDER,
204
- [http_url, project_api_key],
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
- async def inject_rrweb_async(page: Page):
208
- # Wait for the page to be in a ready state first
209
- await page.wait_for_load_state("domcontentloaded")
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
- # First check if rrweb is already loaded
212
- is_loaded = await page.evaluate(
213
- """
214
- () => typeof window.lmnrRrweb !== 'undefined'
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
- if not is_loaded:
219
+ def inject_rrweb(page: SyncPage):
220
+ try:
221
+ page.wait_for_load_state("domcontentloaded")
219
222
 
220
- async def load_rrweb():
221
- await page.evaluate(RRWEB_CONTENT)
222
- # Verify script loaded successfully
223
- await page.wait_for_function(
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
- return True
228
-
229
- if not await retry_async(
230
- load_rrweb, delay=1, error_message="Failed to load rrweb"
231
- ):
232
- return
233
-
234
- # Get current trace ID from active span
235
- current_span = opentelemetry.trace.get_current_span()
236
- if current_span.is_recording():
237
- current_span.set_attribute("lmnr.internal.has_browser_session", True)
238
-
239
- trace_id = format(current_span.get_span_context().trace_id, "032x")
240
- session_id = str(uuid.uuid4().hex)
241
-
242
- async def set_window_vars():
243
- await page.evaluate(
244
- """([traceId, sessionId]) => {
245
- window.lmnrRrwebSessionId = sessionId;
246
- window.lmnrTraceId = traceId;
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
- if not await retry_async(
257
- set_window_vars, error_message="Failed to set window variables"
258
- ):
259
- return
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
- # Update the recording setup to include trace ID
262
- await page.evaluate(
263
- INJECT_PLACEHOLDER,
264
- [http_url, project_api_key],
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
- def handle_navigation(page: SyncPage):
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
- inject_rrweb(page)
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
- async def handle_navigation_async(page: Page):
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
- await inject_rrweb_async(page)
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
- async def patched_new_page_async(self: BrowserContext, *args, **kwargs):
282
- # Modify CSP to allow required domains
283
- async def handle_route(route):
325
+ async def collection_loop():
284
326
  try:
285
- response = await route.fetch()
286
- headers = dict(response.headers)
287
-
288
- # Find and modify CSP header
289
- for header_name in headers:
290
- if header_name.lower() == "content-security-policy":
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.debug(f"Error handling route: {e}")
301
- await route.continue_()
334
+ logger.error(f"Event collection stopped: {e}")
335
+
336
+ # Create and store task
337
+ task = asyncio.create_task(collection_loop())
302
338
 
303
- # Intercept all navigation requests to modify CSP headers
304
- await self.route("**/*", handle_route)
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
- # Modify CSP to allow required domains
311
- def handle_route(route):
312
- try:
313
- response = route.fetch()
314
- headers = dict(response.headers)
315
-
316
- # Find and modify CSP header
317
- for header_name in headers:
318
- if header_name.lower() == "content-security-policy":
319
- csp = headers[header_name]
320
- parts = csp.split(";")
321
- for i, part in enumerate(parts):
322
- if "connect-src" in part:
323
- parts[i] = f"{part.strip()} {http_url}"
324
- if not any("connect-src" in part for part in parts):
325
- parts.append(f" connect-src 'self' {http_url}")
326
- headers[header_name] = ";".join(parts)
327
-
328
- route.fulfill(response=response, headers=headers)
329
- except Exception as e:
330
- logger.debug(f"Error handling route: {e}")
331
- route.continue_()
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.61"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lmnr
3
- Version: 0.4.61
3
+ Version: 0.4.62
4
4
  Summary: Python SDK for Laminar
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -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=URg6zOlidIXHnkVu4OpMc8-kvm51j8rb5BNUtuNxDXM,12091
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=LzhIiBgtOi3GHrG5laR64q4r1mHZBcugyz_xhYbFn5U,33448
27
+ lmnr/sdk/laminar.py,sha256=0t6hSryO8tNALe9gf32O56K2l2e7j1LoGZv6twkitk0,33910
28
28
  lmnr/sdk/log.py,sha256=nt_YMmPw1IRbGy0b7q4rTtP4Yo3pQfNxqJPXK3nDSNQ,2213
29
29
  lmnr/sdk/types.py,sha256=Y4msdSM_IvQ5LOfV2jvk4R0-6skW5Ilml466a6swul4,6506
30
30
  lmnr/sdk/utils.py,sha256=sD1YEqhdPaHweY2VGmjMF9MC-X7Ikdc49E01D-HF77E,3377
31
- lmnr/version.py,sha256=uQK5Bt__rGV1lmIQ4s11hI7eSOu7SczAWFNzRe-9DUU,106
32
- lmnr-0.4.61.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
33
- lmnr-0.4.61.dist-info/METADATA,sha256=Nvo3FOzpnISMf8jYxsJI-ULPWCuhOMTWcwVb-FZXx-A,13827
34
- lmnr-0.4.61.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
35
- lmnr-0.4.61.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
36
- lmnr-0.4.61.dist-info/RECORD,,
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,,
File without changes
File without changes