pax_global_header00006660000000000000000000000064152440010330014502gustar00rootroot0000000000000052 comment=46fb99385f7833044bba753f9e0c4fca1f1afe4f python-chainstream-1.0.2/000077500000000000000000000000001524400103300153175ustar00rootroot00000000000000python-chainstream-1.0.2/.github/000077500000000000000000000000001524400103300166575ustar00rootroot00000000000000python-chainstream-1.0.2/.github/workflows/000077500000000000000000000000001524400103300207145ustar00rootroot00000000000000python-chainstream-1.0.2/.github/workflows/ci.yml000066400000000000000000000012301524400103300220260ustar00rootroot00000000000000name: CI on: [ push, pull_request ] jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Install dependencies (Ubuntu) if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt-get -y install python3 - name: Install dependencies (macOS) if: ${{ matrix.os == 'macos-latest' }} run: | # Prepend optional brew binary directories to PATH echo "/usr/local/opt/python/libexec/bin" >> $GITHUB_PATH - name: Build run: | pip install pytest mypy pylint tox tox --skip-missing-interpreters python-chainstream-1.0.2/.gitignore000066400000000000000000000020731524400103300173110ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python MANIFEST env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Test outputs tests-output # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject python-chainstream-1.0.2/HACKING.md000066400000000000000000000001301524400103300166770ustar00rootroot00000000000000To work on or package `chainstream`, see the `Makefile` for how it is built and tested. python-chainstream-1.0.2/Makefile000066400000000000000000000004161524400103300167600ustar00rootroot00000000000000# Makefile for maintainer tasks dist: tox && \ git diff --exit-code && \ rm -rf ./dist && \ mkdir dist && \ python -m build release: dist twine upload dist/* && \ git tag v$$(grep version pyproject.toml | grep -o "[0-9.]\+") && \ git push --tags .PHONY: dist python-chainstream-1.0.2/README.md000066400000000000000000000010461524400103300165770ustar00rootroot00000000000000# chainstream Chainstream provides the class `ChainStream`, a subclass of `io.RawIOBase`, to chain I/O streams together into a single stream. See below for example usage, and the module help for more information. Chainstream is distributed under the CC-BY-SA version 4.0; see https://creativecommons.org/licenses/by-sa/4.0/ ## Example ``` from chainstream import ChainStream def generate_open_file_streams(): for file in filenames: yield open(file, 'rb') f = io.BufferedReader(ChainStream(generate_open_file_streams())) f.read() ``` python-chainstream-1.0.2/chainstream/000077500000000000000000000000001524400103300176155ustar00rootroot00000000000000python-chainstream-1.0.2/chainstream/__init__.py000066400000000000000000000044721524400103300217350ustar00rootroot00000000000000""" © Reuben Thomas 2023. This package is distributed under CC-BY-SA 4.0. See https://creativecommons.org/licenses/by-sa/4.0/ Adapted from: https://stackoverflow.com/questions/24528278 Note: the original code is licensed under CC-BY-SA 3.0, which is upwards-compatible with 4.0. """ from __future__ import annotations import io from typing import Optional, IO, List, TYPE_CHECKING if TYPE_CHECKING: from _typeshed import WriteableBuffer class ChainStream(io.RawIOBase): """ Chain an iterable of IO[bytes] together into a single buffered stream. Usage: def generate_open_file_streams(): for file in filenames: yield open(file, 'rb') f = io.BufferedReader(ChainStream(generate_open_file_streams())) f.read() """ def __init__(self, streams: List[IO[bytes]]): super().__init__() self.leftover = b'' self.stream_iter = iter(streams) try: self.stream: Optional[IO[bytes]] = next(self.stream_iter) except StopIteration: self.stream = None def readable(self) -> bool: return True def _read_next_chunk(self, max_length: int) -> bytes: # Return 0 or more bytes from the current stream, first returning all # leftover bytes. If the stream is closed returns b'' if self.leftover: return self.leftover if self.stream is not None: data = self.stream.read(max_length) assert data is not None # FIXME: allow stream to be non-blocking return data return b'' def readinto(self, b: WriteableBuffer) -> int: mem = memoryview(b) # Allow slicing of the buffer buffer_length = len(mem) chunk = self._read_next_chunk(buffer_length) while len(chunk) == 0: # move to next stream if self.stream is not None: self.stream.close() try: self.stream = next(self.stream_iter) chunk = self._read_next_chunk(buffer_length) except StopIteration: # No more streams to chain together self.stream = None return 0 # indicate EOF output, self.leftover = chunk[:buffer_length], chunk[buffer_length:] mem[:len(output)] = output return len(output) python-chainstream-1.0.2/chainstream/py.typed000066400000000000000000000001071524400103300213120ustar00rootroot00000000000000# Marker file for PEP 561. The chainstream package uses inline types. python-chainstream-1.0.2/mypy.ini000066400000000000000000000000251524400103300170130ustar00rootroot00000000000000[mypy] strict = true python-chainstream-1.0.2/pyproject.toml000066400000000000000000000015771524400103300202450ustar00rootroot00000000000000[project] name = "chainstream" version = "1.0.2" description = "Chain I/O streams together" license = {text = "CC-BY-SA 4.0"} authors = [ {name = "Reuben Thomas", email = "rrt@c3d.org"}, ] readme = "README.md" urls = {Homepage = "https://github.com/rrthomas/chainstream"} classifiers = [ "Programming Language :: Python :: 3", ] requires-python = ">=3.9" [tool.setuptools.package-data] "*" = ["py.typed"] [tool.pylint.MASTER] init-import = "yes" [tool.pylint.main] # Use multiple processes to speed up Pylint: 0 = auto-detect. jobs = 0 [tool.pylint.format] disable = [ "fixme", "consider-using-f-string", "missing-function-docstring", "missing-module-docstring", "too-many-arguments", "too-many-locals", "too-many-branches", "too-many-positional-arguments", "too-many-statements", "global-statement", ] enable = [ "useless-suppression", ] python-chainstream-1.0.2/test.py000066400000000000000000000003251524400103300166500ustar00rootroot00000000000000import io from chainstream import ChainStream with open("README.md", 'rb') as f: with open("tox.ini", 'rb') as g: h = io.BufferedReader(ChainStream([f, g])) print(h.read()) h.close() python-chainstream-1.0.2/tox.ini000066400000000000000000000002641524400103300166340ustar00rootroot00000000000000[tox] envlist = py39,py310,py311,py312,py313 [testenv] deps = mypy pylint setenv = TOPDIR = {toxinidir} commands = mypy . pylint --disable=C,fixme chainstream