stream.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. import json
  2. import re
  3. import aiohttp
  4. import codecs
  5. from darabonba.event import Event
  6. from io import BytesIO, StringIO
  7. from typing import Any, BinaryIO, Generator, AsyncGenerator, Dict
  8. # define WRITEABLE
  9. sse_line_pattern = re.compile('(?P<name>[^:]*):?( ?(?P<value>.*))?')
  10. class BaseStream:
  11. def __init__(self, size=1024):
  12. self.size = size
  13. def read(self, size=1024):
  14. raise NotImplementedError('read method must be overridden')
  15. def __len__(self):
  16. raise NotImplementedError('__len__ method must be overridden')
  17. def __next__(self):
  18. raise NotImplementedError('__next__ method must be overridden')
  19. def __iter__(self):
  20. return self
  21. class _ReadableMc(type):
  22. def __instancecheck__(self, instance):
  23. if hasattr(instance, 'read') and hasattr(instance, '__iter__'):
  24. return True
  25. class READABLE(metaclass=_ReadableMc):
  26. pass
  27. class SyncSSEResponseWrapper:
  28. def __init__(self, session, response):
  29. self.session = session
  30. self.response = response
  31. self._closed = False
  32. def close(self):
  33. if not self._closed:
  34. self.response.close()
  35. self.session.close()
  36. self._closed = True
  37. def __iter__(self):
  38. return self._read_chunks()
  39. def _read_chunks(self):
  40. try:
  41. for chunk in self.response.iter_content(chunk_size=8192):
  42. yield chunk
  43. finally:
  44. self.close()
  45. def read(self) -> bytes:
  46. try:
  47. return self.response.content
  48. finally:
  49. self.close()
  50. class SSEResponseWrapper:
  51. def __init__(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse):
  52. self.session = session
  53. self.response = response
  54. self._closed = False
  55. self._content_cache = None
  56. async def close(self):
  57. if not self._closed:
  58. self.response.close()
  59. await self.session.close()
  60. self._closed = True
  61. def __aiter__(self):
  62. return self._read_chunks()
  63. async def _read_chunks(self):
  64. try:
  65. async for chunk in self.response.content.iter_chunked(8192):
  66. yield chunk
  67. finally:
  68. await self.close()
  69. async def read(self) -> bytes:
  70. if self._content_cache is not None:
  71. return self._content_cache
  72. try:
  73. content = await self.response.read()
  74. self._content_cache = content
  75. return content
  76. finally:
  77. await self.close()
  78. class _WriteableMc(type):
  79. def __instancecheck__(self, instance):
  80. if hasattr(instance, 'write'):
  81. return True
  82. class WRITABLE(metaclass=_WriteableMc):
  83. pass
  84. STREAM_CLASS = (READABLE, WRITABLE)
  85. class Stream:
  86. def __init__(self, data=None):
  87. self.data = data if data is not None else b''
  88. self.position = 0
  89. @staticmethod
  90. def __read_part(f, size=1024):
  91. while True:
  92. part = f.read(size)
  93. if part:
  94. yield part
  95. else:
  96. return
  97. @staticmethod
  98. def __to_string(
  99. val: bytes,
  100. ) -> str:
  101. """
  102. Convert a bytes to string(utf8)
  103. @return: the return string
  104. """
  105. if isinstance(val, str):
  106. return val
  107. elif isinstance(val, bytes):
  108. return val.decode('utf-8')
  109. else:
  110. return str(val)
  111. @staticmethod
  112. def __parse_json(
  113. val: str,
  114. ) -> Any:
  115. """
  116. Parse it by JSON format
  117. @return: the parsed result
  118. """
  119. try:
  120. return json.loads(val)
  121. except ValueError:
  122. raise RuntimeError(f'Failed to parse the value as json format, Value: "{val}".')
  123. @staticmethod
  124. def read_as_bytes(stream) -> bytes:
  125. """
  126. Read data from a readable stream, and compose it to a bytes
  127. @param stream: the readable stream
  128. @return: the bytes result
  129. """
  130. if isinstance(stream, SyncSSEResponseWrapper):
  131. return stream.read()
  132. elif isinstance(stream, READABLE):
  133. b = b''
  134. for part in Stream.__read_part(stream, 1024):
  135. b += part
  136. return b
  137. elif isinstance(stream, bytes):
  138. return stream
  139. else:
  140. return bytes(stream, encoding='utf-8')
  141. @staticmethod
  142. async def read_as_bytes_async(stream) -> bytes:
  143. """
  144. Read data from a readable stream, and compose it to a bytes
  145. @param stream: the readable stream
  146. @return: the bytes result
  147. """
  148. if isinstance(stream, bytes):
  149. return stream
  150. elif isinstance(stream, str):
  151. return bytes(stream, encoding='utf-8')
  152. else:
  153. return await stream.read()
  154. @staticmethod
  155. def read_as_json(stream) -> Any:
  156. """
  157. Read data from a readable stream, and parse it by JSON format
  158. @param stream: the readable stream
  159. @return: the parsed result
  160. """
  161. return Stream.__parse_json(Stream.read_as_string(stream))
  162. @staticmethod
  163. async def read_as_json_async(stream) -> Any:
  164. """
  165. Read data from a readable stream, and parse it by JSON format
  166. @param stream: the readable stream
  167. @return: the parsed result
  168. """
  169. return Stream.__parse_json(
  170. await Stream.read_as_string_async(stream)
  171. )
  172. @staticmethod
  173. def read_as_string(stream) -> str:
  174. """
  175. Read data from a readable stream, and compose it to a string
  176. @param stream: the readable stream
  177. @return: the string result
  178. """
  179. buff = Stream.read_as_bytes(stream)
  180. return Stream.__to_string(buff)
  181. @staticmethod
  182. async def read_as_string_async(stream) -> str:
  183. """
  184. Read data from a readable stream, and compose it to a string
  185. @param stream: the readable stream
  186. @return: the string result
  187. """
  188. buff = await Stream.read_as_bytes_async(stream)
  189. return Stream.__to_string(buff)
  190. @staticmethod
  191. def read_as_sse(stream) -> Generator[Event, None, None]:
  192. """
  193. Read events from SSE stream (synchronous version)
  194. """
  195. if isinstance(stream, SyncSSEResponseWrapper):
  196. for event in Stream._parse_sse_stream_sync(stream):
  197. yield Event(
  198. id=event.get('id'),
  199. data=event.get('data'),
  200. event=event.get('event'),
  201. retry=event.get('retry'))
  202. elif hasattr(stream, 'iter_content'):
  203. # Read directly from the content stream of requests response object
  204. for event in Stream._parse_sse_stream_from_response_sync(stream):
  205. yield Event(
  206. id=event.get('id'),
  207. data=event.get('data'),
  208. event=event.get('event'),
  209. retry=event.get('retry'))
  210. else:
  211. for event in Stream._parse_sse_stream_sync(stream):
  212. yield Event(
  213. id=event.get('id'),
  214. data=event.get('data'),
  215. event=event.get('event'),
  216. retry=event.get('retry'))
  217. @staticmethod
  218. async def read_as_sse_async(stream) -> AsyncGenerator[Event, None]:
  219. """
  220. Read events from SSE stream
  221. """
  222. if isinstance(stream, SSEResponseWrapper):
  223. async for event in Stream._parse_sse_stream(stream):
  224. yield Event(
  225. id = event.get('id'),
  226. data = event.get('data'),
  227. event= event.get('event'),
  228. retry = event.get('retry'))
  229. elif hasattr(stream, 'content'):
  230. # Read directly from the content stream of aiohttp response object
  231. async for event in Stream._parse_sse_stream_from_response(stream):
  232. yield Event(
  233. id = event.get('id'),
  234. data = event.get('data'),
  235. event= event.get('event'),
  236. retry = event.get('retry'))
  237. else:
  238. async for event in Stream._parse_sse_stream(stream):
  239. yield Event(
  240. id = event.get('id'),
  241. data = event.get('data'),
  242. event= event.get('event'),
  243. retry = event.get('retry'))
  244. def read(self, size=None):
  245. if size is None:
  246. return self.data[self.position:]
  247. start = self.position
  248. end = min(start + size, len(self.data))
  249. self.position = end
  250. return self.data[start:end]
  251. def write(self, data):
  252. if isinstance(data, (bytes, str)):
  253. self.data = data
  254. else:
  255. raise TypeError("Data should be bytes or string.")
  256. def pipe(self, output_stream, buffer_size=1024):
  257. if not isinstance(output_stream, Stream):
  258. raise TypeError("Output stream should be an instance of Stream.")
  259. while True:
  260. chunk = self.read(buffer_size)
  261. if not chunk:
  262. break
  263. output_stream.write(chunk)
  264. @staticmethod
  265. def to_readable(
  266. value: Any,
  267. ) -> BinaryIO:
  268. """
  269. Assert a value, if it is a readable, return it, otherwise throws
  270. @return: the readable value
  271. """
  272. if isinstance(value, str):
  273. value = value.encode('utf-8')
  274. if isinstance(value, bytes):
  275. value = BytesIO(value)
  276. elif not isinstance(value, READABLE):
  277. raise ValueError(f'The value is not a readable')
  278. return value
  279. @staticmethod
  280. def to_writeable(
  281. value: Any,
  282. ) -> WRITABLE:
  283. """
  284. Assert a value, if it is a writeable, return it, otherwise throws
  285. @return: the writeable value
  286. """
  287. if isinstance(value, str):
  288. value = StringIO(value)
  289. elif isinstance(value, bytes):
  290. value = BytesIO(value)
  291. elif not isinstance(value, WRITABLE):
  292. raise ValueError(f'The value is not a writeable')
  293. return value
  294. @staticmethod
  295. async def _parse_sse_stream(wrapper: SSEResponseWrapper) -> AsyncGenerator[Dict[str, Any], None]:
  296. """
  297. Analyze SSE stream data
  298. """
  299. buffer = ""
  300. current_event = Event()
  301. MAX_BUFFER_SIZE = 1024 * 1024 # 1MB
  302. dec = codecs.getincrementaldecoder('utf-8')()
  303. async for chunk in wrapper:
  304. try:
  305. chunk_str = dec.decode(chunk)
  306. except UnicodeDecodeError:
  307. chunk_str = chunk.decode('utf-8', errors='replace')
  308. if len(buffer) + len(chunk_str) > MAX_BUFFER_SIZE:
  309. import logging
  310. logging.warning("SSE stream data too large, skipping chunk")
  311. continue
  312. buffer += chunk_str
  313. while '\n' in buffer:
  314. line, buffer = buffer.split('\n', 1)
  315. line = line.rstrip('\r') # Remove \r
  316. if not line.strip():
  317. if current_event.data is not None:
  318. yield {
  319. 'id': current_event.id,
  320. 'event': current_event.event or 'message',
  321. 'data': current_event.data,
  322. 'retry': current_event.retry
  323. }
  324. current_event = Event()
  325. continue
  326. if line.startswith(':'):
  327. continue
  328. if ':' in line:
  329. match = sse_line_pattern.match(line)
  330. if match:
  331. name = match.group('name').strip()
  332. value = match.group('value').strip()
  333. if name == 'event':
  334. current_event.event = value
  335. elif name == 'id':
  336. current_event.id = value
  337. elif name == 'data':
  338. if current_event.data is None:
  339. current_event.data = value
  340. else:
  341. current_event.data += '\n' + value
  342. elif name == 'retry':
  343. try:
  344. current_event.retry = int(value)
  345. except ValueError:
  346. pass
  347. else:
  348. if current_event.data is None:
  349. current_event.data = line
  350. else:
  351. current_event.data += '\n' + line
  352. if buffer.strip() and current_event.data is not None:
  353. yield {
  354. 'id': current_event.id,
  355. 'event': current_event.event or 'message',
  356. 'data': current_event.data,
  357. 'retry': current_event.retry
  358. }
  359. @staticmethod
  360. async def _parse_sse_stream_from_response(response) -> AsyncGenerator[Dict[str, Any], None]:
  361. buffer = ""
  362. current_event = Event()
  363. async for chunk in response.content.iter_chunked(8192):
  364. try:
  365. chunk_str = chunk.decode('utf-8')
  366. except UnicodeDecodeError:
  367. continue
  368. buffer += chunk_str
  369. while '\n' in buffer:
  370. line, buffer = buffer.split('\n', 1)
  371. line = line.rstrip('\r')
  372. if not line.strip():
  373. if current_event.data is not None:
  374. yield {
  375. 'id': current_event.id,
  376. 'event': current_event.event or 'message',
  377. 'data': current_event.data,
  378. 'retry': current_event.retry
  379. }
  380. current_event = Event()
  381. continue
  382. if line.startswith(':'):
  383. continue
  384. if ':' in line:
  385. match = sse_line_pattern.match(line)
  386. if match:
  387. name = match.group('name').strip()
  388. value = match.group('value').strip()
  389. if name == 'event':
  390. current_event.event = value
  391. elif name == 'id':
  392. current_event.id = value
  393. elif name == 'data':
  394. if current_event.data is None:
  395. current_event.data = value
  396. else:
  397. current_event.data += '\n' + value
  398. elif name == 'retry':
  399. try:
  400. current_event.retry = int(value)
  401. except ValueError:
  402. pass
  403. else:
  404. if current_event.data is None:
  405. current_event.data = line
  406. else:
  407. current_event.data += '\n' + line
  408. if buffer.strip() and current_event.data is not None:
  409. yield {
  410. 'id': current_event.id,
  411. 'event': current_event.event or 'message',
  412. 'data': current_event.data,
  413. 'retry': current_event.retry
  414. }
  415. @staticmethod
  416. def _parse_sse_stream_sync(wrapper: SyncSSEResponseWrapper) -> Generator[Dict[str, Any], None, None]:
  417. """
  418. Analyze SSE stream data (synchronous version)
  419. """
  420. buffer = ""
  421. current_event = Event()
  422. for chunk in wrapper:
  423. # Decoding byte data into strings
  424. try:
  425. chunk_str = chunk.decode('utf-8')
  426. except UnicodeDecodeError:
  427. # If decoding fails, skip this chunk
  428. continue
  429. buffer += chunk_str
  430. # Split processing by row
  431. while '\n' in buffer:
  432. line, buffer = buffer.split('\n', 1)
  433. line = line.rstrip('\r') # Remove \r
  434. if not line.strip():
  435. if current_event.data is not None:
  436. yield {
  437. 'id': current_event.id,
  438. 'event': current_event.event or 'message',
  439. 'data': current_event.data,
  440. 'retry': current_event.retry
  441. }
  442. current_event = Event()
  443. continue
  444. # Skip comment lines
  445. if line.startswith(':'):
  446. continue
  447. if ':' in line:
  448. match = sse_line_pattern.match(line)
  449. if match:
  450. name = match.group('name').strip()
  451. value = match.group('value').strip()
  452. if name == 'event':
  453. current_event.event = value
  454. elif name == 'id':
  455. current_event.id = value
  456. elif name == 'data':
  457. if current_event.data is None:
  458. current_event.data = value
  459. else:
  460. current_event.data += '\n' + value
  461. elif name == 'retry':
  462. try:
  463. current_event.retry = int(value)
  464. except ValueError:
  465. pass
  466. else:
  467. if current_event.data is None:
  468. current_event.data = line
  469. else:
  470. current_event.data += '\n' + line
  471. if buffer.strip() and current_event.data is not None:
  472. yield {
  473. 'id': current_event.id,
  474. 'event': current_event.event or 'message',
  475. 'data': current_event.data,
  476. 'retry': current_event.retry
  477. }
  478. @staticmethod
  479. def _parse_sse_stream_from_response_sync(response) -> Generator[Dict[str, Any], None, None]:
  480. """
  481. Parse SSE stream from requests response object (synchronous version)
  482. """
  483. buffer = ""
  484. current_event = Event()
  485. for chunk in response.iter_content(chunk_size=8192):
  486. try:
  487. chunk_str = chunk.decode('utf-8')
  488. except UnicodeDecodeError:
  489. continue
  490. buffer += chunk_str
  491. while '\n' in buffer:
  492. line, buffer = buffer.split('\n', 1)
  493. line = line.rstrip('\r')
  494. if not line.strip():
  495. if current_event.data is not None:
  496. yield {
  497. 'id': current_event.id,
  498. 'event': current_event.event or 'message',
  499. 'data': current_event.data,
  500. 'retry': current_event.retry
  501. }
  502. current_event = Event()
  503. continue
  504. if line.startswith(':'):
  505. continue
  506. if ':' in line:
  507. match = sse_line_pattern.match(line)
  508. if match:
  509. name = match.group('name').strip()
  510. value = match.group('value').strip()
  511. if name == 'event':
  512. current_event.event = value
  513. elif name == 'id':
  514. current_event.id = value
  515. elif name == 'data':
  516. if current_event.data is None:
  517. current_event.data = value
  518. else:
  519. current_event.data += '\n' + value
  520. elif name == 'retry':
  521. try:
  522. current_event.retry = int(value)
  523. except ValueError:
  524. pass
  525. else:
  526. if current_event.data is None:
  527. current_event.data = line
  528. else:
  529. current_event.data += '\n' + line
  530. if buffer.strip() and current_event.data is not None:
  531. yield {
  532. 'id': current_event.id,
  533. 'event': current_event.event or 'message',
  534. 'data': current_event.data,
  535. 'retry': current_event.retry
  536. }