lmnr 0.5.3__py3-none-any.whl → 0.6.1__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 (33) hide show
  1. lmnr/__init__.py +6 -1
  2. lmnr/opentelemetry_lib/__init__.py +23 -36
  3. lmnr/opentelemetry_lib/decorators/__init__.py +219 -0
  4. lmnr/opentelemetry_lib/tracing/__init__.py +158 -1
  5. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +398 -0
  6. lmnr/opentelemetry_lib/tracing/attributes.py +14 -7
  7. lmnr/opentelemetry_lib/tracing/context_properties.py +53 -0
  8. lmnr/opentelemetry_lib/tracing/exporter.py +60 -0
  9. lmnr/opentelemetry_lib/tracing/instruments.py +121 -0
  10. lmnr/opentelemetry_lib/tracing/processor.py +96 -0
  11. lmnr/opentelemetry_lib/tracing/{context_manager.py → tracer.py} +6 -1
  12. lmnr/opentelemetry_lib/utils/package_check.py +3 -1
  13. lmnr/sdk/browser/browser_use_otel.py +1 -1
  14. lmnr/sdk/browser/playwright_otel.py +22 -7
  15. lmnr/sdk/browser/pw_utils.py +25 -9
  16. lmnr/sdk/client/asynchronous/resources/agent.py +3 -1
  17. lmnr/sdk/client/synchronous/resources/agent.py +3 -1
  18. lmnr/sdk/decorators.py +4 -2
  19. lmnr/sdk/evaluations.py +3 -3
  20. lmnr/sdk/laminar.py +28 -31
  21. lmnr/sdk/utils.py +2 -3
  22. lmnr/version.py +1 -1
  23. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/METADATA +65 -62
  24. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/RECORD +27 -28
  25. lmnr/opentelemetry_lib/config/__init__.py +0 -12
  26. lmnr/opentelemetry_lib/decorators/base.py +0 -210
  27. lmnr/opentelemetry_lib/instruments.py +0 -42
  28. lmnr/opentelemetry_lib/tracing/content_allow_list.py +0 -24
  29. lmnr/opentelemetry_lib/tracing/tracing.py +0 -1016
  30. lmnr/opentelemetry_lib/utils/in_memory_span_exporter.py +0 -61
  31. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/LICENSE +0 -0
  32. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/WHEEL +0 -0
  33. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,96 @@
1
+ import uuid
2
+
3
+ from typing import Optional, Union
4
+ from opentelemetry.sdk.trace.export import (
5
+ SpanProcessor,
6
+ SpanExporter,
7
+ BatchSpanProcessor,
8
+ SimpleSpanProcessor,
9
+ )
10
+ from opentelemetry.trace import Span
11
+ from opentelemetry.context import Context, get_value, get_current, set_value
12
+
13
+ from lmnr.opentelemetry_lib.tracing.attributes import (
14
+ SPAN_IDS_PATH,
15
+ SPAN_INSTRUMENTATION_SOURCE,
16
+ SPAN_LANGUAGE_VERSION,
17
+ SPAN_PATH,
18
+ SPAN_SDK_VERSION,
19
+ )
20
+ from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
21
+ from lmnr.opentelemetry_lib.tracing.context_properties import (
22
+ _set_association_properties_attributes,
23
+ )
24
+ from lmnr.version import PYTHON_VERSION, __version__
25
+
26
+
27
+ class LaminarSpanProcessor(SpanProcessor):
28
+ instance: Union[BatchSpanProcessor, SimpleSpanProcessor]
29
+ __span_id_to_path: dict[int, list[str]] = {}
30
+ __span_id_lists: dict[int, list[str]] = {}
31
+
32
+ def __init__(
33
+ self,
34
+ base_url: Optional[str] = None,
35
+ port: Optional[int] = None,
36
+ api_key: Optional[str] = None,
37
+ timeout_seconds: int = 30,
38
+ force_http: bool = False,
39
+ max_export_batch_size: int = 512,
40
+ disable_batch: bool = False,
41
+ exporter: Optional[SpanExporter] = None,
42
+ ):
43
+ self.exporter = exporter or LaminarSpanExporter(
44
+ base_url=base_url,
45
+ port=port,
46
+ api_key=api_key,
47
+ timeout_seconds=timeout_seconds,
48
+ force_http=force_http,
49
+ )
50
+ self.instance = (
51
+ SimpleSpanProcessor(self.exporter)
52
+ if disable_batch
53
+ else BatchSpanProcessor(
54
+ self.exporter, max_export_batch_size=max_export_batch_size
55
+ )
56
+ )
57
+
58
+ def on_start(self, span: Span, parent_context: Optional[Context] = None):
59
+ span_path_in_context = get_value("span_path", parent_context or get_current())
60
+ parent_span_path = span_path_in_context or (
61
+ self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
62
+ )
63
+ parent_span_ids_path = (
64
+ self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
65
+ )
66
+ span_path = parent_span_path + [span.name] if parent_span_path else [span.name]
67
+ span_ids_path = parent_span_ids_path + [
68
+ str(uuid.UUID(int=span.get_span_context().span_id))
69
+ ]
70
+ span.set_attribute(SPAN_PATH, span_path)
71
+ span.set_attribute(SPAN_IDS_PATH, span_ids_path)
72
+ set_value("span_path", span_path, get_current())
73
+ self.__span_id_to_path[span.get_span_context().span_id] = span_path
74
+ self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
75
+
76
+ span.set_attribute(SPAN_INSTRUMENTATION_SOURCE, "python")
77
+ span.set_attribute(SPAN_SDK_VERSION, __version__)
78
+ span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
79
+
80
+ association_properties = get_value("association_properties")
81
+ if association_properties is not None:
82
+ _set_association_properties_attributes(span, association_properties)
83
+ self.instance.on_start(span, parent_context)
84
+
85
+ def on_end(self, span: Span):
86
+ self.instance.on_end(span)
87
+
88
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
89
+ return self.instance.force_flush(timeout_millis)
90
+
91
+ def shutdown(self):
92
+ self.instance.shutdown()
93
+
94
+ def clear(self):
95
+ self.__span_id_to_path = {}
96
+ self.__span_id_lists = {}
@@ -1,6 +1,11 @@
1
1
  from contextlib import contextmanager
2
2
 
3
- from lmnr.opentelemetry_lib.tracing.tracing import TracerWrapper
3
+ from opentelemetry import trace
4
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
5
+
6
+
7
+ def get_laminar_tracer_provider() -> trace.TracerProvider:
8
+ return TracerWrapper.instance.__tracer_provider or trace.get_tracer_provider()
4
9
 
5
10
 
6
11
  @contextmanager
@@ -1,6 +1,8 @@
1
1
  from importlib.metadata import distributions
2
2
 
3
- installed_packages = {dist.metadata.get("Name", "").lower() for dist in distributions()}
3
+ installed_packages = {
4
+ (dist.name or dist.metadata.get("Name", "")).lower() for dist in distributions()
5
+ }
4
6
 
5
7
 
6
8
  def is_package_installed(package_name: str) -> bool:
@@ -1,4 +1,4 @@
1
- from lmnr.opentelemetry_lib.decorators.base import json_dumps
1
+ from lmnr.opentelemetry_lib.decorators import json_dumps
2
2
  from lmnr.sdk.browser.utils import with_tracer_wrapper
3
3
  from lmnr.sdk.utils import get_input_from_func_args
4
4
  from lmnr.version import __version__
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import uuid
3
3
 
4
+ from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
4
5
  from lmnr.sdk.browser.pw_utils import handle_navigation_async, handle_navigation_sync
5
6
  from lmnr.sdk.browser.utils import with_tracer_and_client_wrapper
6
7
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
@@ -22,17 +23,31 @@ from typing import Collection
22
23
  from wrapt import wrap_function_wrapper
23
24
 
24
25
  try:
25
- from playwright.async_api import Browser, BrowserContext, Page
26
- from playwright.sync_api import (
27
- Browser as SyncBrowser,
28
- BrowserContext as SyncBrowserContext,
29
- Page as SyncPage,
30
- )
26
+ if is_package_installed("playwright"):
27
+ from playwright.async_api import Browser, BrowserContext, Page
28
+ from playwright.sync_api import (
29
+ Browser as SyncBrowser,
30
+ BrowserContext as SyncBrowserContext,
31
+ Page as SyncPage,
32
+ )
33
+ elif is_package_installed("patchright"):
34
+ from patchright.async_api import Browser, BrowserContext, Page
35
+ from patchright.sync_api import (
36
+ Browser as SyncBrowser,
37
+ BrowserContext as SyncBrowserContext,
38
+ Page as SyncPage,
39
+ )
40
+ else:
41
+ raise ImportError(
42
+ "Attempted to import lmnr.sdk.browser.playwright_otel, but neither "
43
+ "playwright nor patchright is installed. Use `pip install playwright` "
44
+ "or `pip install patchright` to install one of the supported browsers."
45
+ )
31
46
  except ImportError as e:
32
47
  raise ImportError(
33
48
  f"Attempted to import {__file__}, but it is designed "
34
49
  "to patch Playwright, which is not installed. Use `pip install playwright` "
35
- "to install Playwright or remove this import."
50
+ "or `pip install patchright` to install Playwright or remove this import."
36
51
  ) from e
37
52
 
38
53
  # all available versions at https://pypi.org/project/playwright/#history
@@ -6,19 +6,31 @@ import threading
6
6
 
7
7
  from opentelemetry import trace
8
8
 
9
+ from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
9
10
  from lmnr.sdk.decorators import observe
10
11
  from lmnr.sdk.browser.utils import retry_sync, retry_async
11
12
  from lmnr.sdk.client.synchronous.sync_client import LaminarClient
12
13
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
13
14
 
15
+
14
16
  try:
15
- from playwright.async_api import Page
16
- from playwright.sync_api import Page as SyncPage
17
+ if is_package_installed("playwright"):
18
+ from playwright.async_api import Page
19
+ from playwright.sync_api import Page as SyncPage
20
+ elif is_package_installed("patchright"):
21
+ from patchright.async_api import Page
22
+ from patchright.sync_api import Page as SyncPage
23
+ else:
24
+ raise ImportError(
25
+ "Attempted to import lmnr.sdk.browser.pw_utils, but neither "
26
+ "playwright nor patchright is installed. Use `pip install playwright` "
27
+ "or `pip install patchright` to install one of the supported browsers."
28
+ )
17
29
  except ImportError as e:
18
30
  raise ImportError(
19
- f"Attempted to import {__file__}, but it is designed "
20
- "to patch Playwright, which is not installed. Use `pip install playwright` "
21
- "to install Playwright or remove this import."
31
+ "Attempted to import lmnr.sdk.browser.pw_utils, but neither "
32
+ "playwright nor patchright is installed. Use `pip install playwright` "
33
+ "or `pip install patchright` to install one of the supported browsers."
22
34
  ) from e
23
35
 
24
36
  logger = logging.getLogger(__name__)
@@ -100,14 +112,16 @@ async def send_events_async(
100
112
  """Fetch events from the page and send them to the server"""
101
113
  try:
102
114
  # Check if function exists first
103
- events = await page.evaluate("""
115
+ events = await page.evaluate(
116
+ """
104
117
  () => {
105
118
  if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
106
119
  return [];
107
120
  }
108
121
  return window.lmnrGetAndClearEvents();
109
122
  }
110
- """)
123
+ """
124
+ )
111
125
 
112
126
  if not events or len(events) == 0:
113
127
  return
@@ -127,14 +141,16 @@ def send_events_sync(
127
141
  ):
128
142
  """Synchronous version of send_events"""
129
143
  try:
130
- events = page.evaluate("""
144
+ events = page.evaluate(
145
+ """
131
146
  () => {
132
147
  if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
133
148
  return [];
134
149
  }
135
150
  return window.lmnrGetAndClearEvents();
136
151
  }
137
- """)
152
+ """
153
+ )
138
154
  if not events or len(events) == 0:
139
155
  return
140
156
 
@@ -278,6 +278,8 @@ class AsyncAgent(BaseAsyncResource):
278
278
  json=request.model_dump(by_alias=True),
279
279
  headers=self._headers(),
280
280
  ) as response:
281
+ if response.status_code != 200:
282
+ raise RuntimeError(await response.read())
281
283
  async for line in response.aiter_lines():
282
284
  line = str(line)
283
285
  if line.startswith("[DONE]"):
@@ -288,7 +290,7 @@ class AsyncAgent(BaseAsyncResource):
288
290
  if line:
289
291
  chunk = RunAgentResponseChunk.model_validate_json(line)
290
292
  yield chunk.root
291
- if chunk.root.chunk_type in ["finalOutput", "error"]:
293
+ if chunk.root.chunk_type in ["finalOutput", "error", "timeout"]:
292
294
  break
293
295
 
294
296
  async def __run_non_streaming(self, request: RunAgentRequest) -> AgentOutput:
@@ -270,6 +270,8 @@ class Agent(BaseResource):
270
270
  json=request.model_dump(by_alias=True),
271
271
  headers=self._headers(),
272
272
  ) as response:
273
+ if response.status_code != 200:
274
+ raise RuntimeError(response.read())
273
275
  for line in response.iter_lines():
274
276
  line = str(line)
275
277
  if line.startswith("[DONE]"):
@@ -280,7 +282,7 @@ class Agent(BaseResource):
280
282
  if line:
281
283
  chunk = RunAgentResponseChunk.model_validate_json(line)
282
284
  yield chunk.root
283
- if chunk.root.chunk_type in ["finalOutput", "error"]:
285
+ if chunk.root.chunk_type in ["finalOutput", "error", "timeout"]:
284
286
  break
285
287
 
286
288
  def __run_non_streaming(self, request: RunAgentRequest) -> AgentOutput:
lmnr/sdk/decorators.py CHANGED
@@ -1,4 +1,4 @@
1
- from lmnr.opentelemetry_lib.decorators.base import (
1
+ from lmnr.opentelemetry_lib.decorators import (
2
2
  entity_method,
3
3
  aentity_method,
4
4
  )
@@ -8,7 +8,9 @@ from typing import Callable, Literal, Optional, TypeVar, Union, cast
8
8
  from typing_extensions import ParamSpec
9
9
 
10
10
  from lmnr.opentelemetry_lib.tracing.attributes import SESSION_ID
11
- from lmnr.opentelemetry_lib.tracing.tracing import update_association_properties
11
+ from lmnr.opentelemetry_lib.tracing.context_properties import (
12
+ update_association_properties,
13
+ )
12
14
 
13
15
  from .utils import is_async
14
16
 
lmnr/sdk/evaluations.py CHANGED
@@ -1,11 +1,11 @@
1
1
  import asyncio
2
2
  import re
3
3
  import uuid
4
- import dotenv
4
+
5
5
  from tqdm import tqdm
6
6
  from typing import Any, Awaitable, Optional, Set, Union
7
7
 
8
- from lmnr.opentelemetry_lib.instruments import Instruments
8
+ from lmnr.opentelemetry_lib.tracing.instruments import Instruments
9
9
  from lmnr.opentelemetry_lib.tracing.attributes import SPAN_TYPE
10
10
 
11
11
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
@@ -111,7 +111,7 @@ class Evaluation:
111
111
  trace_export_timeout_seconds: Optional[int] = None,
112
112
  ):
113
113
  """
114
- Initializes an instance of the Evaluations class.
114
+ Initializes an instance of the Evaluation class.
115
115
 
116
116
  Parameters:
117
117
  data (Union[List[EvaluationDatapoint|dict], EvaluationDataset]):\
lmnr/sdk/laminar.py CHANGED
@@ -1,21 +1,17 @@
1
1
  from contextlib import contextmanager
2
2
  from contextvars import Context
3
3
  from lmnr.opentelemetry_lib import TracerManager
4
- from lmnr.opentelemetry_lib.instruments import Instruments
5
- from lmnr.opentelemetry_lib.tracing import get_tracer
4
+ from lmnr.opentelemetry_lib.tracing.instruments import Instruments
5
+ from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
6
6
  from lmnr.opentelemetry_lib.tracing.attributes import (
7
7
  ASSOCIATION_PROPERTIES,
8
8
  Attributes,
9
9
  SPAN_TYPE,
10
10
  )
11
- from lmnr.opentelemetry_lib.config import MAX_MANUAL_SPAN_PAYLOAD_SIZE
12
- from lmnr.opentelemetry_lib.decorators.base import json_dumps
11
+ from lmnr.opentelemetry_lib import MAX_MANUAL_SPAN_PAYLOAD_SIZE
12
+ from lmnr.opentelemetry_lib.decorators import json_dumps
13
13
  from opentelemetry import context as context_api, trace
14
14
  from opentelemetry.context import attach, detach
15
- from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
16
- OTLPSpanExporter,
17
- Compression,
18
- )
19
15
  from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
20
16
  from opentelemetry.util.types import AttributeValue
21
17
 
@@ -34,7 +30,7 @@ from lmnr.opentelemetry_lib.tracing.attributes import (
34
30
  SPAN_OUTPUT,
35
31
  TRACE_TYPE,
36
32
  )
37
- from lmnr.opentelemetry_lib.tracing.tracing import (
33
+ from lmnr.opentelemetry_lib.tracing.context_properties import (
38
34
  get_association_properties,
39
35
  remove_association_properties,
40
36
  set_association_properties,
@@ -52,8 +48,6 @@ from .types import (
52
48
 
53
49
 
54
50
  class Laminar:
55
- __base_http_url: str
56
- __base_grpc_url: str
57
51
  __project_api_key: Optional[str] = None
58
52
  __initialized: bool = False
59
53
 
@@ -65,9 +59,12 @@ class Laminar:
65
59
  http_port: Optional[int] = None,
66
60
  grpc_port: Optional[int] = None,
67
61
  instruments: Optional[Set[Instruments]] = None,
62
+ disabled_instruments: Optional[Set[Instruments]] = None,
68
63
  disable_batch: bool = False,
69
64
  max_export_batch_size: Optional[int] = None,
70
65
  export_timeout_seconds: Optional[int] = None,
66
+ set_global_tracer_provider: bool = True,
67
+ otel_logger_level: int = logging.ERROR,
71
68
  ):
72
69
  """Initialize Laminar context across the application.
73
70
  This method must be called before using any other Laminar methods or
@@ -92,6 +89,8 @@ class Laminar:
92
89
  enable. Defaults to all instruments. You can pass\
93
90
  an empty set to disable all instruments. Read more:\
94
91
  https://docs.lmnr.ai/tracing/automatic-instrumentation
92
+ disabled_instruments (Optional[Set[Instruments]], optional): Instruments to\
93
+ disable. Defaults to None.
95
94
  disable_batch (bool, optional): If set to True, spans will be sent\
96
95
  immediately to the backend. Useful for debugging, but\
97
96
  may cause performance overhead in production.
@@ -100,6 +99,13 @@ class Laminar:
100
99
  exporter. Defaults to 30 seconds (unlike the\
101
100
  OpenTelemetry default of 10 seconds).
102
101
  Defaults to None.
102
+ set_global_tracer_provider (bool, optional): If set to True, the\
103
+ Laminar tracer provider will be set as the global\
104
+ tracer provider. OpenTelemetry allows only one tracer\
105
+ provider per app, so set this to False, if you are using\
106
+ another tracing library. Setting this to False may break\
107
+ some external instrumentations, e.g. LiteLLM.
108
+ Defaults to True.
103
109
 
104
110
  Raises:
105
111
  ValueError: If project API key is not set
@@ -116,6 +122,8 @@ class Laminar:
116
122
 
117
123
  url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
118
124
  url = url.rstrip("/")
125
+ if not url.startswith("http"):
126
+ url = f"https://{url}"
119
127
  if match := re.search(r":(\d{1,5})$", url):
120
128
  url = url[: -len(match.group(0))]
121
129
  if http_port is None:
@@ -124,36 +132,25 @@ class Laminar:
124
132
  else:
125
133
  cls.__logger.info(f"Using HTTP port passed as an argument: {http_port}")
126
134
 
127
- cls.__base_http_url = f"{url}:{http_port or 443}"
128
- cls.__base_grpc_url = f"{url}:{grpc_port or 8443}"
129
-
130
135
  cls.__initialized = True
136
+
131
137
  if not os.getenv("OTEL_ATTRIBUTE_COUNT_LIMIT"):
132
138
  # each message is at least 2 attributes: role and content,
133
139
  # but the default attribute limit is 128, so raise it
134
140
  os.environ["OTEL_ATTRIBUTE_COUNT_LIMIT"] = "10000"
135
141
 
136
- # if not is_latest_version():
137
- # cls.__logger.warning(
138
- # "You are using an older version of the Laminar SDK. "
139
- # f"Latest version: {get_latest_pypi_version()}, current version: {SDK_VERSION}.\n"
140
- # "Please update to the latest version by running "
141
- # "`pip install --upgrade lmnr`."
142
- # )
143
-
144
142
  TracerManager.init(
145
- base_http_url=cls.__base_http_url,
143
+ base_url=url,
144
+ http_port=http_port or 443,
145
+ port=grpc_port or 8443,
146
146
  project_api_key=cls.__project_api_key,
147
- exporter=OTLPSpanExporter(
148
- endpoint=cls.__base_grpc_url,
149
- headers={"authorization": f"Bearer {cls.__project_api_key}"},
150
- compression=Compression.Gzip,
151
- # default timeout is 10 seconds, increase it to 30 seconds
152
- timeout=export_timeout_seconds or 30,
153
- ),
154
147
  instruments=instruments,
148
+ block_instruments=disabled_instruments,
155
149
  disable_batch=disable_batch,
156
150
  max_export_batch_size=max_export_batch_size,
151
+ timeout_seconds=export_timeout_seconds,
152
+ set_global_tracer_provider=set_global_tracer_provider,
153
+ otel_logger_level=otel_logger_level,
157
154
  )
158
155
 
159
156
  @classmethod
@@ -680,7 +677,7 @@ class Laminar:
680
677
  """Set the metadata for the current trace.
681
678
 
682
679
  Args:
683
- metadata (dict[str, str]): Metadata to set for the trace. Willl be\
680
+ metadata (dict[str, str]): Metadata to set for the trace. Will be\
684
681
  sent as attributes, so must be json serializable.
685
682
  """
686
683
  props = {f"metadata.{k}": json_dumps(v) for k, v in metadata.items()}
lmnr/sdk/utils.py CHANGED
@@ -37,10 +37,9 @@ def is_async(func: typing.Callable) -> bool:
37
37
  return True
38
38
 
39
39
  # Fallback: check if the function's code object contains 'async'.
40
- # This is for cases when a decorator did not properly use
40
+ # This is for cases when a decorator (not ours) did not properly use
41
41
  # `functools.wraps` or `functools.update_wrapper`
42
- CO_COROUTINE = inspect.CO_COROUTINE
43
- return (func.__code__.co_flags & CO_COROUTINE) != 0
42
+ return (func.__code__.co_flags & inspect.CO_COROUTINE) != 0
44
43
 
45
44
 
46
45
  def is_async_iterator(o: typing.Any) -> bool:
lmnr/version.py CHANGED
@@ -3,7 +3,7 @@ import httpx
3
3
  from packaging import version
4
4
 
5
5
 
6
- __version__ = "0.5.3"
6
+ __version__ = "0.6.1"
7
7
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
8
8
 
9
9