dataroom-client 1.0.4.post12.dev0__tar.gz → 1.0.5.post2.dev0__tar.gz

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,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: dataroom-client
3
- Version: 1.0.4.post12.dev0
3
+ Version: 1.0.5.post2.dev0
4
4
  Summary: A python client to interface with the Dataroom backend API
5
5
  Author: Ales Kocjancic
6
6
  Author-email: hi@ales.io
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
13
14
  Requires-Dist: httpx (>=0.28.0,<1)
14
15
  Description-Content-Type: text/markdown
15
16
 
@@ -3,6 +3,7 @@ import functools
3
3
  import inspect
4
4
  import threading
5
5
  import atexit
6
+ import queue
6
7
  from datetime import datetime
7
8
  import json as json_module
8
9
  import logging
@@ -1814,6 +1815,17 @@ class AsyncRunner:
1814
1815
  future = asyncio.run_coroutine_threadsafe(coro, cls._loop)
1815
1816
  return future.result()
1816
1817
 
1818
+ @classmethod
1819
+ def submit(cls, coro):
1820
+ """
1821
+ Schedule a coroutine on the shared background event loop and return the Future.
1822
+
1823
+ Unlike run(), this does not wait for the result; useful for producers feeding queues.
1824
+ """
1825
+ if cls._thread is None:
1826
+ cls._initialize()
1827
+ return asyncio.run_coroutine_threadsafe(coro, cls._loop)
1828
+
1817
1829
  @classmethod
1818
1830
  def shutdown(cls) -> None:
1819
1831
  """
@@ -1866,6 +1878,9 @@ class DataRoomClientSync:
1866
1878
  result = attr(*args, **kwargs)
1867
1879
  if inspect.isawaitable(result):
1868
1880
  return AsyncRunner.run(result)
1881
+ # If the result is an async generator or async iterable, wrap it into a blocking iterator
1882
+ if inspect.isasyncgen(result) or hasattr(result, "__aiter__"):
1883
+ return self._wrap_async_iterable(result)
1869
1884
  return result
1870
1885
 
1871
1886
  return sync_wrapper
@@ -1886,4 +1901,69 @@ class DataRoomClientSync:
1886
1901
  @return: A DataRoomFile instance containing the downloaded image.
1887
1902
  """
1888
1903
  # Class methods are not covered by the automatic wrapping of async methods in __getattr__.
1889
- return AsyncRunner.run(DataRoomClient.download_image_from_url(*args, **kwargs))
1904
+ return AsyncRunner.run(DataRoomClient.download_image_from_url(*args, **kwargs))
1905
+
1906
+ @staticmethod
1907
+ def _wrap_async_iterable(async_iterable):
1908
+ """
1909
+ Convert an AsyncIterable into a synchronous, blocking Python iterator.
1910
+ Items are streamed via a thread-safe queue from a background task.
1911
+ """
1912
+ sentinel = object()
1913
+ q: queue.Queue = queue.Queue(maxsize=10)
1914
+ stop_flag = {"stop": False}
1915
+
1916
+ async def aclose_safe(ait):
1917
+ aclose = getattr(ait, "aclose", None)
1918
+ if aclose is not None:
1919
+ try:
1920
+ await aclose()
1921
+ except Exception: # pragma: no cover - best effort cleanup
1922
+ pass
1923
+
1924
+ async def producer():
1925
+ try:
1926
+ async for item in async_iterable:
1927
+ if stop_flag["stop"]:
1928
+ await aclose_safe(async_iterable)
1929
+ break
1930
+ while True:
1931
+ try:
1932
+ q.put_nowait(item)
1933
+ break
1934
+ except queue.Full:
1935
+ if stop_flag["stop"]:
1936
+ await aclose_safe(async_iterable)
1937
+ return
1938
+ await asyncio.sleep(0.01)
1939
+ except Exception as e:
1940
+ # pass exception to consumer then terminate
1941
+ try:
1942
+ q.put_nowait(e)
1943
+ except queue.Full:
1944
+ # If full, block briefly in thread to ensure delivery
1945
+ q.put(e)
1946
+ finally:
1947
+ # Signal completion
1948
+ try:
1949
+ q.put_nowait(sentinel)
1950
+ except queue.Full:
1951
+ q.put(sentinel)
1952
+
1953
+ # Start the producer without blocking
1954
+ AsyncRunner.submit(producer())
1955
+
1956
+ def iterator():
1957
+ try:
1958
+ while True:
1959
+ item = q.get()
1960
+ if item is sentinel:
1961
+ break
1962
+ if isinstance(item, Exception):
1963
+ raise item
1964
+ yield item
1965
+ finally:
1966
+ # Signal producer to stop; it will close the async generator promptly
1967
+ stop_flag["stop"] = True
1968
+
1969
+ return iterator()
@@ -6,7 +6,7 @@ authors = [
6
6
  ]
7
7
  readme = "README.md"
8
8
  dynamic = []
9
- version = "1.0.4.post12.dev0"
9
+ version = "1.0.5.post2.dev0"
10
10
 
11
11
  [tool.poetry]
12
12