Skip to content

Commit 0ed69c1

Browse files
committed
python-bindings: Refactor and update the Python bindings
1 parent 20f0442 commit 0ed69c1

File tree

21 files changed

+764
-154
lines changed

21 files changed

+764
-154
lines changed

meta/bindings/python/papermuncher.py

Lines changed: 0 additions & 141 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .render import render, to_pdf, to_image
2+
from .binding import paper_muncher
3+
from .types import Environment
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Module with internal functions for handling streams and URLs."""
2+
3+
import asyncio
4+
import logging
5+
import os.path
6+
7+
from io import BytesIO
8+
from functools import wraps
9+
from contextlib import asynccontextmanager
10+
from urllib.parse import urlparse
11+
12+
from typing import Iterable, Optional, overload, Union
13+
from .types import Environment, Streamable
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
@overload
19+
def to_stream(
20+
instance : Union[str, bytes, BytesIO],
21+
environment : Optional[Environment] = None,
22+
) -> Iterable[BytesIO]:
23+
...
24+
25+
@overload
26+
def to_stream(
27+
instance : Streamable,
28+
environment : Optional[Environment] = None,
29+
) -> Iterable[Streamable]:
30+
...
31+
32+
@asynccontextmanager
33+
async def to_stream(
34+
instance : Union[Streamable, str, bytes],
35+
environment : Optional[Environment] = None,
36+
) -> Iterable[Streamable]:
37+
"""
38+
Convert various types of input (str, bytes, Streamable) to a stream.
39+
This function handles different types of input and returns a streamable
40+
object. It can also handle URLs and file paths, converting them to
41+
appropriate streamable objects.
42+
Args:
43+
instance (Union[Streamable, str, bytes]): The input to convert to a
44+
stream.
45+
environment (Optional[Environment]): An optional environment object
46+
for handling URLs (this arg is also used to know if PM is piped).
47+
Yields:
48+
Iterable[Streamable]: A streamable object.
49+
"""
50+
def has_all_attrs(obj, attrs):
51+
return all(hasattr(obj, attr) for attr in attrs)
52+
53+
try:
54+
if has_all_attrs(instance, ['read', 'readline']):
55+
yield instance
56+
else:
57+
if isinstance(instance, bytes):
58+
future_stream = instance
59+
elif isinstance(instance, str):
60+
if environment is not None and os.path.isfile(instance):
61+
with open(instance, 'rb') as file_stream:
62+
yield file_stream
63+
future_stream = instance.encode('utf-8')
64+
elif hasattr('__bytes__', instance):
65+
future_stream = bytes(instance)
66+
elif hasattr('__str__', instance):
67+
future_stream = str(instance).encode('utf-8')
68+
else:
69+
raise TypeError(f"Unsupported type: {type(instance)}")
70+
71+
url = urlparse(future_stream)
72+
if environment is not None and url.scheme and url.netloc:
73+
yield await environment.get_asset(future_stream)
74+
else:
75+
stream = BytesIO(future_stream)
76+
stream.seek(0)
77+
yield stream
78+
79+
except Exception as e:
80+
_logger.error("Error converting to stream: %s", e)
81+
raise
82+
else:
83+
if 'stream' in locals():
84+
stream.close()
85+
86+
87+
def with_pmoptions_init(cls):
88+
"""Decorator to override __init__ to support PMOptions passthrough."""
89+
original_init = cls.__init__
90+
91+
@wraps(original_init)
92+
def __init__(self, **kwargs):
93+
# If any value is a PMOptions, use it to populate fields
94+
option_like = next((v for v in kwargs.values() if isinstance(v, cls)), None)
95+
if option_like:
96+
original_init(self, **option_like.__dict__)
97+
else:
98+
original_init(self, **kwargs)
99+
100+
cls.__init__ = __init__
101+
return cls
102+
103+
104+
class SyncStreamWrapper(Streamable):
105+
"""
106+
A synchronous wrapper for an asynchronous stream.
107+
This class allows synchronous code to interact with an
108+
asynchronous stream by providing synchronous methods for reading
109+
and iterating over the stream.
110+
"""
111+
112+
def __init__(self, async_stream: asyncio.StreamReader):
113+
self.async_stream = async_stream
114+
115+
def read(self, size=-1) -> bytes:
116+
return asyncio.run(self._read(size))
117+
118+
async def _read(self, size=-1) -> bytes:
119+
return await self.async_stream.read(size)
120+
121+
def readline(self) -> bytes:
122+
return asyncio.run(self._readline())
123+
124+
async def _readline(self) -> bytes:
125+
return await self.async_stream.readline()
126+
127+
def __iter__(self):
128+
return self
129+
130+
def __next__(self):
131+
chunk = asyncio.run(self.async_stream.read(4096))
132+
if not chunk:
133+
raise StopIteration
134+
return chunk

0 commit comments

Comments
 (0)