| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- import json
- import re
- import aiohttp
- import codecs
- from darabonba.event import Event
- from io import BytesIO, StringIO
- from typing import Any, BinaryIO, Generator, AsyncGenerator, Dict
- # define WRITEABLE
- sse_line_pattern = re.compile('(?P<name>[^:]*):?( ?(?P<value>.*))?')
- class BaseStream:
- def __init__(self, size=1024):
- self.size = size
- def read(self, size=1024):
- raise NotImplementedError('read method must be overridden')
- def __len__(self):
- raise NotImplementedError('__len__ method must be overridden')
- def __next__(self):
- raise NotImplementedError('__next__ method must be overridden')
- def __iter__(self):
- return self
- class _ReadableMc(type):
- def __instancecheck__(self, instance):
- if hasattr(instance, 'read') and hasattr(instance, '__iter__'):
- return True
- class READABLE(metaclass=_ReadableMc):
- pass
- class SyncSSEResponseWrapper:
- def __init__(self, session, response):
- self.session = session
- self.response = response
- self._closed = False
-
- def close(self):
- if not self._closed:
- self.response.close()
- self.session.close()
- self._closed = True
-
- def __iter__(self):
- return self._read_chunks()
-
- def _read_chunks(self):
- try:
- for chunk in self.response.iter_content(chunk_size=8192):
- yield chunk
- finally:
- self.close()
-
- def read(self) -> bytes:
- try:
- return self.response.content
- finally:
- self.close()
- class SSEResponseWrapper:
- def __init__(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse):
- self.session = session
- self.response = response
- self._closed = False
- self._content_cache = None
-
- async def close(self):
- if not self._closed:
- self.response.close()
- await self.session.close()
- self._closed = True
-
- def __aiter__(self):
- return self._read_chunks()
-
- async def _read_chunks(self):
- try:
- async for chunk in self.response.content.iter_chunked(8192):
- yield chunk
- finally:
- await self.close()
-
- async def read(self) -> bytes:
- if self._content_cache is not None:
- return self._content_cache
-
- try:
- content = await self.response.read()
- self._content_cache = content
- return content
- finally:
- await self.close()
- class _WriteableMc(type):
- def __instancecheck__(self, instance):
- if hasattr(instance, 'write'):
- return True
- class WRITABLE(metaclass=_WriteableMc):
- pass
- STREAM_CLASS = (READABLE, WRITABLE)
- class Stream:
- def __init__(self, data=None):
- self.data = data if data is not None else b''
- self.position = 0
- @staticmethod
- def __read_part(f, size=1024):
- while True:
- part = f.read(size)
- if part:
- yield part
- else:
- return
- @staticmethod
- def __to_string(
- val: bytes,
- ) -> str:
- """
- Convert a bytes to string(utf8)
- @return: the return string
- """
- if isinstance(val, str):
- return val
- elif isinstance(val, bytes):
- return val.decode('utf-8')
- else:
- return str(val)
- @staticmethod
- def __parse_json(
- val: str,
- ) -> Any:
- """
- Parse it by JSON format
- @return: the parsed result
- """
- try:
- return json.loads(val)
- except ValueError:
- raise RuntimeError(f'Failed to parse the value as json format, Value: "{val}".')
- @staticmethod
- def read_as_bytes(stream) -> bytes:
- """
- Read data from a readable stream, and compose it to a bytes
- @param stream: the readable stream
- @return: the bytes result
- """
- if isinstance(stream, SyncSSEResponseWrapper):
- return stream.read()
- elif isinstance(stream, READABLE):
- b = b''
- for part in Stream.__read_part(stream, 1024):
- b += part
- return b
- elif isinstance(stream, bytes):
- return stream
- else:
- return bytes(stream, encoding='utf-8')
-
- @staticmethod
- async def read_as_bytes_async(stream) -> bytes:
- """
- Read data from a readable stream, and compose it to a bytes
- @param stream: the readable stream
- @return: the bytes result
- """
- if isinstance(stream, bytes):
- return stream
- elif isinstance(stream, str):
- return bytes(stream, encoding='utf-8')
- else:
- return await stream.read()
-
- @staticmethod
- def read_as_json(stream) -> Any:
- """
- Read data from a readable stream, and parse it by JSON format
- @param stream: the readable stream
- @return: the parsed result
- """
- return Stream.__parse_json(Stream.read_as_string(stream))
- @staticmethod
- async def read_as_json_async(stream) -> Any:
- """
- Read data from a readable stream, and parse it by JSON format
- @param stream: the readable stream
- @return: the parsed result
- """
- return Stream.__parse_json(
- await Stream.read_as_string_async(stream)
- )
- @staticmethod
- def read_as_string(stream) -> str:
- """
- Read data from a readable stream, and compose it to a string
- @param stream: the readable stream
- @return: the string result
- """
- buff = Stream.read_as_bytes(stream)
- return Stream.__to_string(buff)
-
- @staticmethod
- async def read_as_string_async(stream) -> str:
- """
- Read data from a readable stream, and compose it to a string
- @param stream: the readable stream
- @return: the string result
- """
- buff = await Stream.read_as_bytes_async(stream)
- return Stream.__to_string(buff)
-
- @staticmethod
- def read_as_sse(stream) -> Generator[Event, None, None]:
- """
- Read events from SSE stream (synchronous version)
- """
- if isinstance(stream, SyncSSEResponseWrapper):
- for event in Stream._parse_sse_stream_sync(stream):
- yield Event(
- id=event.get('id'),
- data=event.get('data'),
- event=event.get('event'),
- retry=event.get('retry'))
- elif hasattr(stream, 'iter_content'):
- # Read directly from the content stream of requests response object
- for event in Stream._parse_sse_stream_from_response_sync(stream):
- yield Event(
- id=event.get('id'),
- data=event.get('data'),
- event=event.get('event'),
- retry=event.get('retry'))
- else:
- for event in Stream._parse_sse_stream_sync(stream):
- yield Event(
- id=event.get('id'),
- data=event.get('data'),
- event=event.get('event'),
- retry=event.get('retry'))
- @staticmethod
- async def read_as_sse_async(stream) -> AsyncGenerator[Event, None]:
- """
- Read events from SSE stream
- """
- if isinstance(stream, SSEResponseWrapper):
- async for event in Stream._parse_sse_stream(stream):
- yield Event(
- id = event.get('id'),
- data = event.get('data'),
- event= event.get('event'),
- retry = event.get('retry'))
- elif hasattr(stream, 'content'):
- # Read directly from the content stream of aiohttp response object
- async for event in Stream._parse_sse_stream_from_response(stream):
- yield Event(
- id = event.get('id'),
- data = event.get('data'),
- event= event.get('event'),
- retry = event.get('retry'))
- else:
- async for event in Stream._parse_sse_stream(stream):
- yield Event(
- id = event.get('id'),
- data = event.get('data'),
- event= event.get('event'),
- retry = event.get('retry'))
- def read(self, size=None):
- if size is None:
- return self.data[self.position:]
-
- start = self.position
- end = min(start + size, len(self.data))
- self.position = end
- return self.data[start:end]
- def write(self, data):
- if isinstance(data, (bytes, str)):
- self.data = data
- else:
- raise TypeError("Data should be bytes or string.")
- def pipe(self, output_stream, buffer_size=1024):
- if not isinstance(output_stream, Stream):
- raise TypeError("Output stream should be an instance of Stream.")
-
- while True:
- chunk = self.read(buffer_size)
- if not chunk:
- break
- output_stream.write(chunk)
-
- @staticmethod
- def to_readable(
- value: Any,
- ) -> BinaryIO:
- """
- Assert a value, if it is a readable, return it, otherwise throws
- @return: the readable value
- """
- if isinstance(value, str):
- value = value.encode('utf-8')
- if isinstance(value, bytes):
- value = BytesIO(value)
- elif not isinstance(value, READABLE):
- raise ValueError(f'The value is not a readable')
- return value
- @staticmethod
- def to_writeable(
- value: Any,
- ) -> WRITABLE:
- """
- Assert a value, if it is a writeable, return it, otherwise throws
- @return: the writeable value
- """
- if isinstance(value, str):
- value = StringIO(value)
- elif isinstance(value, bytes):
- value = BytesIO(value)
- elif not isinstance(value, WRITABLE):
- raise ValueError(f'The value is not a writeable')
- return value
-
- @staticmethod
- async def _parse_sse_stream(wrapper: SSEResponseWrapper) -> AsyncGenerator[Dict[str, Any], None]:
- """
- Analyze SSE stream data
- """
- buffer = ""
- current_event = Event()
-
- MAX_BUFFER_SIZE = 1024 * 1024 # 1MB
- dec = codecs.getincrementaldecoder('utf-8')()
-
- async for chunk in wrapper:
- try:
- chunk_str = dec.decode(chunk)
- except UnicodeDecodeError:
- chunk_str = chunk.decode('utf-8', errors='replace')
-
- if len(buffer) + len(chunk_str) > MAX_BUFFER_SIZE:
- import logging
- logging.warning("SSE stream data too large, skipping chunk")
- continue
-
- buffer += chunk_str
- while '\n' in buffer:
- line, buffer = buffer.split('\n', 1)
- line = line.rstrip('\r') # Remove \r
-
- if not line.strip():
- if current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
- current_event = Event()
- continue
-
- if line.startswith(':'):
- continue
-
- if ':' in line:
- match = sse_line_pattern.match(line)
- if match:
- name = match.group('name').strip()
- value = match.group('value').strip()
-
- if name == 'event':
- current_event.event = value
- elif name == 'id':
- current_event.id = value
- elif name == 'data':
- if current_event.data is None:
- current_event.data = value
- else:
- current_event.data += '\n' + value
- elif name == 'retry':
- try:
- current_event.retry = int(value)
- except ValueError:
- pass
- else:
- if current_event.data is None:
- current_event.data = line
- else:
- current_event.data += '\n' + line
- if buffer.strip() and current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
- @staticmethod
- async def _parse_sse_stream_from_response(response) -> AsyncGenerator[Dict[str, Any], None]:
- buffer = ""
- current_event = Event()
- async for chunk in response.content.iter_chunked(8192):
- try:
- chunk_str = chunk.decode('utf-8')
- except UnicodeDecodeError:
- continue
-
- buffer += chunk_str
-
- while '\n' in buffer:
- line, buffer = buffer.split('\n', 1)
- line = line.rstrip('\r')
-
- if not line.strip():
- if current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
- current_event = Event()
- continue
-
- if line.startswith(':'):
- continue
-
- if ':' in line:
- match = sse_line_pattern.match(line)
- if match:
- name = match.group('name').strip()
- value = match.group('value').strip()
-
- if name == 'event':
- current_event.event = value
- elif name == 'id':
- current_event.id = value
- elif name == 'data':
- if current_event.data is None:
- current_event.data = value
- else:
- current_event.data += '\n' + value
- elif name == 'retry':
- try:
- current_event.retry = int(value)
- except ValueError:
- pass
- else:
- if current_event.data is None:
- current_event.data = line
- else:
- current_event.data += '\n' + line
- if buffer.strip() and current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
-
- @staticmethod
- def _parse_sse_stream_sync(wrapper: SyncSSEResponseWrapper) -> Generator[Dict[str, Any], None, None]:
- """
- Analyze SSE stream data (synchronous version)
- """
- buffer = ""
- current_event = Event()
- for chunk in wrapper:
- # Decoding byte data into strings
- try:
- chunk_str = chunk.decode('utf-8')
- except UnicodeDecodeError:
- # If decoding fails, skip this chunk
- continue
-
- buffer += chunk_str
-
- # Split processing by row
- while '\n' in buffer:
- line, buffer = buffer.split('\n', 1)
- line = line.rstrip('\r') # Remove \r
-
- if not line.strip():
- if current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
- current_event = Event()
- continue
-
- # Skip comment lines
- if line.startswith(':'):
- continue
-
- if ':' in line:
- match = sse_line_pattern.match(line)
- if match:
- name = match.group('name').strip()
- value = match.group('value').strip()
-
- if name == 'event':
- current_event.event = value
- elif name == 'id':
- current_event.id = value
- elif name == 'data':
- if current_event.data is None:
- current_event.data = value
- else:
- current_event.data += '\n' + value
- elif name == 'retry':
- try:
- current_event.retry = int(value)
- except ValueError:
- pass
- else:
- if current_event.data is None:
- current_event.data = line
- else:
- current_event.data += '\n' + line
- if buffer.strip() and current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
- @staticmethod
- def _parse_sse_stream_from_response_sync(response) -> Generator[Dict[str, Any], None, None]:
- """
- Parse SSE stream from requests response object (synchronous version)
- """
- buffer = ""
- current_event = Event()
- for chunk in response.iter_content(chunk_size=8192):
- try:
- chunk_str = chunk.decode('utf-8')
- except UnicodeDecodeError:
- continue
-
- buffer += chunk_str
-
- while '\n' in buffer:
- line, buffer = buffer.split('\n', 1)
- line = line.rstrip('\r')
-
- if not line.strip():
- if current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
- current_event = Event()
- continue
-
- if line.startswith(':'):
- continue
-
- if ':' in line:
- match = sse_line_pattern.match(line)
- if match:
- name = match.group('name').strip()
- value = match.group('value').strip()
-
- if name == 'event':
- current_event.event = value
- elif name == 'id':
- current_event.id = value
- elif name == 'data':
- if current_event.data is None:
- current_event.data = value
- else:
- current_event.data += '\n' + value
- elif name == 'retry':
- try:
- current_event.retry = int(value)
- except ValueError:
- pass
- else:
- if current_event.data is None:
- current_event.data = line
- else:
- current_event.data += '\n' + line
- if buffer.strip() and current_event.data is not None:
- yield {
- 'id': current_event.id,
- 'event': current_event.event or 'message',
- 'data': current_event.data,
- 'retry': current_event.retry
- }
|