././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1483567275.0 aiofile-3.9.0/LICENCE.md0000644000000000000000000002437713033270253011460 0ustar00Apache License ============== _Version 2.0, January 2004_ _<>_ ### Terms and Conditions for use, reproduction, and distribution #### 1. Definitions “License” shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. “Licensor” shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. “Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means **(i)** the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the outstanding shares, or **(iii)** beneficial ownership of such entity. “You” (or “Your”) shall mean an individual or Legal Entity exercising permissions granted by this License. “Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. “Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. “Work” shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). “Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. “Contribution” shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as “Not a Contribution.” “Contributor” shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. #### 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. #### 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. #### 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: * **(a)** You must give any other recipients of the Work or Derivative Works a copy of this License; and * **(b)** You must cause any modified files to carry prominent notices stating that You changed the files; and * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. #### 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. #### 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. #### 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. #### 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. #### 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. _END OF TERMS AND CONDITIONS_ ### APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets `[]` replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same “printed page” as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728383963.7079215 aiofile-3.9.0/README.md0000644000000000000000000003043514701205734011350 0ustar00# AIOFile [![Github Actions](https://github.com/mosquito/aiofile/workflows/tox/badge.svg)](https://github.com/mosquito/aiofile/actions?query=branch%3Amaster) [![Latest Version](https://img.shields.io/pypi/v/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![Wheel](https://img.shields.io/pypi/wheel/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![Python Versions](https://img.shields.io/pypi/pyversions/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![License](https://img.shields.io/pypi/l/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![Coverage Status](https://coveralls.io/repos/github/mosquito/aiofile/badge.svg?branch=master)](https://coveralls.io/github/mosquito/aiofile?branch=master) Real asynchronous file operations with asyncio support. ## Status Development - Stable ## Features * Since version 2.0.0 using [caio](https://pypi.org/project/caio), which contains linux `libaio` and two thread-based implementations (c-based and pure-python). * AIOFile has no internal pointer. You should pass `offset` and `chunk_size` for each operation or use helpers (Reader or Writer). The simplest way is to use `async_open` for creating object with file-like interface. * For Linux using implementation based on [libaio](https://pagure.io/libaio). * For POSIX (MacOS X and optional Linux) using implementation based on [threadpool](https://github.com/mbrossard/threadpool/). * Otherwise using pure-python thread-based implementation. * Implementation chooses automatically depending on system compatibility. ## Limitations * Linux native AIO implementation is not able to open special files. Asynchronous operations against special fs like `/proc/` `/sys/` are not supported by the kernel. It's not a aiofile's or caio issue. In these cases, you might switch to thread-based implementations (see [Troubleshooting](#troubleshooting) section). However, when used on supported file systems, the linux implementation has a smaller overhead and is preferred but it's not a silver bullet. ## Code examples All code examples require Python 3.6+. ### High-level API #### `async_open` helper Helper mimics python file-like objects, it returns file-like objects with similar but async methods. Supported methods: * `async def read(length = -1)` - reading chunk from file, when length is `-1`, will be reading file to the end. * `async def write(data)` - writing chunk to file * `def seek(offset)` - setting file pointer position * `def tell()` - returns current file pointer position * `async def readline(size=-1, newline="\n")` - read chunks until newline or EOF. Since version 3.7.0 `__aiter__` returns `LineReader`. This method is suboptimal for small lines because it doesn't reuse read buffer. When you want to read file by lines please avoid using `async_open` use `LineReader` instead. * `def __aiter__() -> LineReader` - iterator over lines. * `def iter_chunked(chunk_size: int = 32768) -> Reader` - iterator over chunks. * `.file` property contains AIOFile object Basic example: ```python import asyncio from pathlib import Path from tempfile import gettempdir from aiofile import async_open tmp_filename = Path(gettempdir()) / "hello.txt" async def main(): async with async_open(tmp_filename, 'w+') as afp: await afp.write("Hello ") await afp.write("world") afp.seek(0) print(await afp.read()) await afp.write("Hello from\nasync world") print(await afp.readline()) print(await afp.readline()) asyncio.run(main()) ``` Example without context manager: ```python import asyncio import atexit import os from tempfile import mktemp from aiofile import async_open TMP_NAME = mktemp() atexit.register(os.unlink, TMP_NAME) async def main(): afp = await async_open(TMP_NAME, "w") await afp.write("Hello") await afp.close() asyncio.run(main()) assert open(TMP_NAME, "r").read() == "Hello" ``` Concatenate example program (`cat`): ```python import asyncio import sys from argparse import ArgumentParser from pathlib import Path from aiofile import async_open parser = ArgumentParser( description="Read files line by line using asynchronous io API" ) parser.add_argument("file_name", nargs="+", type=Path) async def main(arguments): for src in arguments.file_name: async with async_open(src, "r") as afp: async for line in afp: sys.stdout.write(line) asyncio.run(main(parser.parse_args())) ``` Copy file example program (`cp`): ```python import asyncio from argparse import ArgumentParser from pathlib import Path from aiofile import async_open parser = ArgumentParser( description="Copying files using asynchronous io API" ) parser.add_argument("source", type=Path) parser.add_argument("dest", type=Path) parser.add_argument("--chunk-size", type=int, default=65535) async def main(arguments): async with async_open(arguments.source, "rb") as src, \ async_open(arguments.dest, "wb") as dest: async for chunk in src.iter_chunked(arguments.chunk_size): await dest.write(chunk) asyncio.run(main(parser.parse_args())) ``` Example with opening already open file pointer: ```python import asyncio from typing import IO, Any from aiofile import async_open async def main(fp: IO[Any]): async with async_open(fp) as afp: await afp.write("Hello from\nasync world") print(await afp.readline()) with open("test.txt", "w+") as fp: asyncio.run(main(fp)) ``` Linux native aio doesn't support reading and writing special files (e.g. procfs/sysfs/unix pipes/etc.), so you can perform operations with these files using compatible context objects. ```python import asyncio from aiofile import async_open from caio import thread_aio_asyncio from contextlib import AsyncExitStack async def main(): async with AsyncExitStack() as stack: # Custom context should be reused ctx = await stack.enter_async_context( thread_aio_asyncio.AsyncioContext() ) # Open special file with custom context src = await stack.enter_async_context( async_open("/proc/cpuinfo", "r", context=ctx) ) # Open regular file with default context dest = await stack.enter_async_context( async_open("/tmp/cpuinfo", "w") ) # Copying file content line by line async for line in src: await dest.write(line) asyncio.run(main()) ``` ### Low-level API The `AIOFile` class is a low-level interface for asynchronous file operations, and the read and write methods accept an `offset=0` in bytes at which the operation will be performed. This allows you to do many independent IO operations on a once open file without moving the virtual carriage. For example, you may make 10 concurrent HTTP requests by specifying the `Range` header, and asynchronously write one opened file, while the offsets must either be calculated manually, or use 10 instances of `Writer` with specified initial offsets. In order to provide sequential reading and writing, there is `Writer`, `Reader` and `LineReader`. Keep in mind `async_open` is not the same as AIOFile, it provides a similar interface for file operations, it simulates methods like read or write as it is implemented in the built-in open. ```python import asyncio from aiofile import AIOFile async def main(): async with AIOFile("hello.txt", 'w+') as afp: payload = "Hello world\n" await asyncio.gather( *[afp.write(payload, offset=i * len(payload)) for i in range(10)] ) await afp.fsync() assert await afp.read(len(payload) * 10) == payload * 10 asyncio.run(main()) ``` The Low-level API in fact is just little bit sugared `caio` API. ```python import asyncio from aiofile import AIOFile async def main(): async with AIOFile("/tmp/hello.txt", 'w+') as afp: await afp.write("Hello ") await afp.write("world", offset=7) await afp.fsync() print(await afp.read()) asyncio.run(main()) ``` #### `Reader` and `Writer` When you want to read or write file linearly following example might be helpful. ```python import asyncio from aiofile import AIOFile, Reader, Writer async def main(): async with AIOFile("/tmp/hello.txt", 'w+') as afp: writer = Writer(afp) reader = Reader(afp, chunk_size=8) await writer("Hello") await writer(" ") await writer("World") await afp.fsync() async for chunk in reader: print(chunk) asyncio.run(main()) ``` #### `LineReader` - read file line by line LineReader is a helper that is very effective when you want to read a file linearly and line by line. It contains a buffer and will read the fragments of the file chunk by chunk into the buffer, where it will try to find lines. The default chunk size is 4KB. ```python import asyncio from aiofile import AIOFile, LineReader, Writer async def main(): async with AIOFile("/tmp/hello.txt", 'w+') as afp: writer = Writer(afp) await writer("Hello") await writer(" ") await writer("World") await writer("\n") await writer("\n") await writer("From async world") await afp.fsync() async for line in LineReader(afp): print(line) asyncio.run(main()) ``` When you want to read file by lines please avoid to use `async_open` use `LineReader` instead. ## More examples Useful examples with `aiofile` ### Async CSV Dict Reader ```python import asyncio import io from csv import DictReader from aiofile import AIOFile, LineReader class AsyncDictReader: def __init__(self, afp, **kwargs): self.buffer = io.BytesIO() self.file_reader = LineReader( afp, line_sep=kwargs.pop('line_sep', '\n'), chunk_size=kwargs.pop('chunk_size', 4096), offset=kwargs.pop('offset', 0), ) self.reader = DictReader( io.TextIOWrapper( self.buffer, encoding=kwargs.pop('encoding', 'utf-8'), errors=kwargs.pop('errors', 'replace'), ), **kwargs, ) self.line_num = 0 def __aiter__(self): return self async def __anext__(self): if self.line_num == 0: header = await self.file_reader.readline() self.buffer.write(header) line = await self.file_reader.readline() if not line: raise StopAsyncIteration self.buffer.write(line) self.buffer.seek(0) try: result = next(self.reader) except StopIteration as e: raise StopAsyncIteration from e self.buffer.seek(0) self.buffer.truncate(0) self.line_num = self.reader.line_num return result async def main(): async with AIOFile('sample.csv', 'rb') as afp: async for item in AsyncDictReader(afp): print(item) asyncio.run(main()) ``` ## Troubleshooting The caio `linux` implementation works normal for modern linux kernel versions and file systems. So you may have problems specific for your environment. It's not a bug and might be resolved some ways: 1. Upgrade the kernel 2. Use compatible file systems 3. Use threads based or pure python implementation. The caio since version 0.7.0 contains some ways to do this. 1. In runtime use the environment variable `CAIO_IMPL` with possible values: * `linux` - use native linux kernels aio mechanism * `thread` - use thread based implementation written in C * `python` - use pure python implementation 2. File `default_implementation` located near `__init__.py` in caio installation path. It's useful for distros package maintainers. This file might contains comments (lines starts with `#` symbol) and the first line should be one of `linux` `thread` or `python`. 3. You might manually manage contexts: ```python import asyncio from aiofile import async_open from caio import linux_aio_asyncio, thread_aio_asyncio async def main(): linux_ctx = linux_aio_asyncio.AsyncioContext() threads_ctx = thread_aio_asyncio.AsyncioContext() async with async_open("/tmp/test.txt", "w", context=linux_ctx) as afp: await afp.write("Hello") async with async_open("/tmp/test.txt", "r", context=threads_ctx) as afp: print(await afp.read()) asyncio.run(main()) ``` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728382758.574391 aiofile-3.9.0/aiofile/__init__.py0000644000000000000000000000115214701203447013603 0ustar00from .aio import AIOFile from .utils import ( BinaryFileWrapper, FileIOWrapperBase, LineReader, Reader, TextFileWrapper, Writer, async_open, ) from .version import ( __author__, __version__, author_info, package_info, package_license, project_home, team_email, version_info, ) __all__ = ( "AIOFile", "BinaryFileWrapper", "FileIOWrapperBase", "LineReader", "Reader", "TextFileWrapper", "Writer", "__author__", "__version__", "async_open", "author_info", "package_info", "package_license", "project_home", "team_email", "version_info", ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728383963.708356 aiofile-3.9.0/aiofile/aio.py0000644000000000000000000002167314701205734012627 0ustar00import asyncio import os from collections import namedtuple from concurrent.futures import Executor from functools import partial from os import strerror from pathlib import Path from typing import ( Any, Awaitable, BinaryIO, Callable, Dict, Generator, Optional, TextIO, TypeVar, Union, ) from weakref import finalize import caio from caio.asyncio_base import AsyncioContextBase _T = TypeVar("_T") AIO_FILE_NOT_OPENED = -1 AIO_FILE_CLOSED = -2 FileIOType = Union[TextIO, BinaryIO] FileMode = namedtuple( "FileMode", ( "readable", "writable", "plus", "appending", "created", "flags", "binary", ), ) def parse_mode(mode: str) -> FileMode: # noqa: C901 """ Rewritten from `cpython fileno`_ .. _cpython fileio: https://bit.ly/2JY2cnp """ flags = os.O_RDONLY rwa = False writable = False readable = False plus = False appending = False created = False binary = False for m in mode: if m == "x": rwa = True created = True writable = True flags |= os.O_EXCL | os.O_CREAT if m == "r": if rwa: raise Exception("Bad mode") rwa = True readable = True if m == "w": if rwa: raise Exception("Bad mode") rwa = True writable = True flags |= os.O_CREAT | os.O_TRUNC if m == "a": if rwa: raise Exception("Bad mode") rwa = True writable = True appending = True flags |= os.O_CREAT | os.O_APPEND if m == "+": if plus: raise Exception("Bad mode") readable = True writable = True plus = True if m == "b": binary = True if hasattr(os, "O_BINARY"): flags |= os.O_BINARY if readable and writable: flags |= os.O_RDWR elif readable: flags |= os.O_RDONLY else: flags |= os.O_WRONLY return FileMode( readable=readable, writable=writable, plus=plus, appending=appending, created=created, flags=flags, binary=binary, ) class AIOFile: _file_obj: Optional[FileIOType] _file_obj_owner: bool _encoding: str _executor: Optional[Executor] mode: FileMode __open_result: "Optional[asyncio.Future[FileIOType]]" def __init__( self, filename: Union[str, Path], mode: str = "r", encoding: str = "utf-8", context: Optional[AsyncioContextBase] = None, executor: Optional[Executor] = None, ): self.__context = context or get_default_context() self.__open_result = None self._fname = str(filename) self._open_mode = mode self.mode = parse_mode(mode) self._file_obj = None self._file_obj_owner = True self._encoding = encoding self._executor = executor @classmethod def from_fp(cls, fp: FileIOType, **kwargs: Any) -> "AIOFile": afp = cls(fp.name, fp.mode, **kwargs) afp._file_obj = fp afp._open_mode = fp.mode afp._file_obj_owner = False return afp def _run_in_thread( self, func: "Callable[..., _T]", *args: Any, **kwargs: Any, ) -> "asyncio.Future[_T]": return self.__context.loop.run_in_executor( self._executor, partial(func, *args, **kwargs), ) @property def name(self) -> str: return self._fname @property def loop(self) -> asyncio.AbstractEventLoop: return self.__context.loop @property def encoding(self) -> str: return self._encoding async def open(self) -> Optional[int]: if self._file_obj is not None: if self._file_obj.closed: raise asyncio.InvalidStateError("AIOFile closed") return None if self.__open_result is None: self.__open_result = self._run_in_thread( open, self._fname, self._open_mode, ) self._file_obj = await self.__open_result self.__open_result = None return self._file_obj.fileno() await self.__open_result return None def __repr__(self) -> str: return "" % self._fname async def close(self) -> None: if self._file_obj is None or not self._file_obj_owner: return if self.mode.writable: await self.fdsync() await self._run_in_thread(self._file_obj.close) def fileno(self) -> int: if self._file_obj is None: raise asyncio.InvalidStateError("AIOFile closed") return self._file_obj.fileno() def __await__(self) -> Generator[None, Any, "AIOFile"]: yield from self.open().__await__() return self async def __aenter__(self) -> "AIOFile": await self.open() return self def __aexit__(self, *args: Any) -> Awaitable[Any]: return asyncio.get_event_loop().create_task(self.close()) async def read(self, size: int = -1, offset: int = 0) -> Union[bytes, str]: data = await self.read_bytes(size, offset) return data if self.mode.binary else self.decode_bytes(data) async def read_bytes(self, size: int = -1, offset: int = 0) -> bytes: if size < -1: raise ValueError("Unsupported value %d for size" % size) if size == -1: size = ( await self._run_in_thread( os.stat, self.fileno(), ) ).st_size return await self.__context.read(size, self.fileno(), offset) async def write(self, data: Union[str, bytes], offset: int = 0) -> int: if self.mode.binary: if not isinstance(data, bytes): raise ValueError("Data must be bytes in binary mode") bytes_data = data else: if not isinstance(data, str): raise ValueError("Data must be str in text mode") bytes_data = self.encode_bytes(data) return await self.write_bytes(bytes_data, offset) def encode_bytes(self, data: str) -> bytes: return data.encode(self._encoding) def decode_bytes(self, data: bytes) -> str: return data.decode(self._encoding) async def write_bytes(self, data: bytes, offset: int = 0) -> int: data_size = len(data) if data_size == 0: return 0 # data can be written partially, see write(2) # (https://www.man7.org/linux/man-pages/man2/write.2.html) # for example, it can happen when a disk quota or a resource limit # is exceeded (in that case subsequent call will return a # corresponding error) or write has been interrupted by # an incoming signal # behaviour here in regard to continue trying to write remaining data # corresponds to the behaviour of io.BufferedIOBase # (https://docs.python.org/3/library/io.html#io.BufferedIOBase.write) # which used by object returned open() with `buffering` argument >= 1 # (effectively the default) written = 0 while written < data_size: res = await self.__context.write( data[written:], self.fileno(), offset + written, ) if res == 0: raise RuntimeError( "Write operation returned 0", self, offset, written, ) elif res < 0: # fix for linux_aio implementation bug in caio<=0.6.1 # (https://github.com/mosquito/caio/pull/7) # and safeguard against future similar issues errno = -res raise OSError(errno, strerror(errno), self._fname) written += res return written async def fsync(self) -> None: return await self.__context.fsync(self.fileno()) async def fdsync(self) -> None: return await self.__context.fdsync(self.fileno()) def truncate(self, length: int = 0) -> Awaitable[None]: return self._run_in_thread( os.ftruncate, self.fileno(), length, ) ContextStoreType = Dict[asyncio.AbstractEventLoop, caio.AsyncioContext] DEFAULT_CONTEXT_STORE: ContextStoreType = {} def create_context( max_requests: int = caio.AsyncioContext.MAX_REQUESTS_DEFAULT, ) -> caio.AsyncioContext: loop = asyncio.get_event_loop() context = caio.AsyncioContext(max_requests, loop=loop) def finalizer() -> None: context.close() DEFAULT_CONTEXT_STORE.pop(context, None) finalize(loop, finalizer) DEFAULT_CONTEXT_STORE[loop] = context return context def get_default_context() -> caio.AsyncioContext: loop = asyncio.get_event_loop() context = DEFAULT_CONTEXT_STORE.get(loop) if context is not None: return context return create_context() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597262590.0 aiofile-3.9.0/aiofile/py.typed0000644000000000000000000000000113715045376013172 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728383963.7088778 aiofile-3.9.0/aiofile/utils.py0000644000000000000000000002455714701205734013223 0ustar00import asyncio import collections.abc import io import os from abc import ABC, abstractmethod from pathlib import Path from types import MappingProxyType from typing import Any, Generator, Tuple, Union from .aio import AIOFile, FileIOType ENCODING_MAP = MappingProxyType({ "utf-8": 4, "utf-16": 8, "UTF-8": 4, "UTF-16": 8, }) async def unicode_reader( afp: AIOFile, chunk_size: int, offset: int, encoding: str = "utf-8", ) -> Tuple[int, str]: if chunk_size < 0: chunk_bytes = await afp.read_bytes(-1, offset) return len(chunk_bytes), chunk_bytes.decode(encoding=encoding) last_error = None for retry in range(ENCODING_MAP.get(encoding, 4)): chunk_bytes = await afp.read_bytes(chunk_size + retry, offset) try: chunk = chunk_bytes.decode(encoding=encoding) break except UnicodeDecodeError as e: last_error = e else: raise last_error # type: ignore chunk_size = len(chunk_bytes) return chunk_size, chunk class Reader(collections.abc.AsyncIterable): __slots__ = "_chunk_size", "__offset", "file", "__lock", "encoding" CHUNK_SIZE = 32 * 1024 def __init__( self, aio_file: AIOFile, offset: int = 0, chunk_size: int = CHUNK_SIZE, ): self.__lock = asyncio.Lock() self.__offset = int(offset) self._chunk_size = int(chunk_size) self.file = aio_file self.encoding = self.file.encoding async def read_chunk(self) -> Union[str, bytes]: async with self.__lock: if self.file.mode.binary: chunk = await self.file.read_bytes( self._chunk_size, self.__offset, ) # type: Union[str, bytes] chunk_size = len(chunk) else: chunk_size, chunk = await unicode_reader( self.file, self._chunk_size, self.__offset, encoding=self.encoding, ) self.__offset += chunk_size return chunk async def __anext__(self) -> Union[str, bytes]: chunk = await self.read_chunk() if not chunk: raise StopAsyncIteration(chunk) return chunk def __aiter__(self) -> "Reader": return self class Writer: __slots__ = "__chunk_size", "__offset", "__aio_file", "__lock" def __init__(self, aio_file: AIOFile, offset: int = 0): self.__offset = int(offset) self.__aio_file = aio_file self.__lock = asyncio.Lock() async def __call__(self, data: Union[str, bytes]) -> None: async with self.__lock: if isinstance(data, str): data = self.__aio_file.encode_bytes(data) await self.__aio_file.write_bytes(data, self.__offset) self.__offset += len(data) class LineReader(collections.abc.AsyncIterable): CHUNK_SIZE = 4192 def __init__( self, aio_file: AIOFile, offset: int = 0, chunk_size: int = CHUNK_SIZE, line_sep: str = "\n", ): self.__reader = Reader(aio_file, chunk_size=chunk_size, offset=offset) self._buffer = ( io.BytesIO() if aio_file.mode.binary else io.StringIO() ) # type: Any self.linesep = ( aio_file.encode_bytes(line_sep) if aio_file.mode.binary else line_sep ) async def readline(self) -> Union[str, bytes]: while True: line = self._buffer.readline() if line and line.endswith(self.linesep): return line buffer_remainder = line + self._buffer.read() self._buffer.truncate(0) self._buffer.seek(0) # No line in buffer, read more data chunk = await self.__reader.read_chunk() if not chunk: # No more data to read, return any remaining content in the buffer return buffer_remainder # We have more data to read, write it to the buffer and handle it in the next iteration self._buffer.write(buffer_remainder) self._buffer.write(chunk) self._buffer.seek(0) async def __anext__(self) -> Union[bytes, str]: line = await self.readline() if not line: # We are finished, close the buffer and raise StopAsyncIteration self._buffer.close() raise StopAsyncIteration(line) return line def __aiter__(self) -> "LineReader": return self class FileIOWrapperBase(ABC): _READLINE_CHUNK_SIZE = 4192 def __init__(self, afp: AIOFile, *, offset: int = 0): self._offset = offset self._lock = asyncio.Lock() self.file = afp if self.file.mode.appending: try: self._offset = os.stat(afp.name).st_size except FileNotFoundError: self._offset = 0 @abstractmethod async def read(self, length: int = -1) -> Any: raise NotImplementedError @abstractmethod async def write(self, data: Any) -> int: raise NotImplementedError @abstractmethod async def readline( self, size: int = -1, newline: Any = ..., ) -> Union[str, bytes]: raise NotImplementedError def seek(self, offset: int) -> None: self._offset = offset def tell(self) -> int: return self._offset async def flush(self, sync_metadata: bool = False) -> None: if sync_metadata: await self.file.fsync() else: await self.file.fdsync() async def close(self) -> None: await self.file.close() def __await__(self) -> Generator[None, None, "FileIOWrapperBase"]: yield from self.file.__await__() return self async def __aenter__(self) -> "FileIOWrapperBase": await self.file.open() return self async def __aexit__(self, *_: Any) -> None: await self.close() def __aiter__(self) -> LineReader: return LineReader(self.file) def iter_chunked(self, chunk_size: int = Reader.CHUNK_SIZE) -> Reader: return Reader(self.file, chunk_size=chunk_size, offset=self._offset) class BinaryFileWrapper(FileIOWrapperBase): def __init__(self, afp: AIOFile): if not afp.mode.binary: raise ValueError("Expected file in binary mode") super().__init__(afp) async def __read(self, length: int) -> bytes: data = await self.file.read_bytes(length, self._offset) self._offset += len(data) return data async def read(self, length: int = -1) -> bytes: async with self._lock: return await self.__read(length) async def write(self, data: bytes) -> int: async with self._lock: operation = self.file.write_bytes(data, self._offset) self._offset += len(data) await operation return len(data) async def readline(self, size: int = -1, newline: bytes = b"\n") -> bytes: async with self._lock: offset = self._offset with io.BytesIO() as fp: while True: chunk = await self.__read(self._READLINE_CHUNK_SIZE) if chunk: if newline not in chunk: fp.write(chunk) continue fp.write(chunk) if 0 < size <= fp.tell(): fp.seek(size) fp.truncate(size) return fp.getvalue() fp.seek(0) line = fp.readline() self._offset = offset + fp.tell() return line class TextFileWrapper(FileIOWrapperBase): def __init__(self, afp: AIOFile): if afp.mode.binary: raise ValueError("Expected file in text mode") super().__init__(afp) self.encoding = self.file.encoding async def __read(self, length: int) -> str: chunk_size = 0 offset = self._offset chunk = "" while length < 0 or length > len(chunk): part_offset, part = await unicode_reader( self.file, length, offset, self.encoding, ) if not part: break chunk += part offset += part_offset if chunk_size > length > 0: chunk = chunk[:length] offset = length self._offset = offset return chunk async def read(self, length: int = -1) -> str: async with self._lock: return await self.__read(length) async def write(self, data: str) -> int: async with self._lock: data_bytes = data.encode(self.encoding) operation = self.file.write_bytes(data_bytes, self._offset) self._offset += len(data_bytes) await operation return len(data_bytes) async def readline(self, size: int = -1, newline: str = "\n") -> str: async with self._lock: offset = self._offset with io.StringIO() as fp: while True: chunk = await self.__read(self._READLINE_CHUNK_SIZE) if chunk: if newline not in chunk: fp.write(chunk) continue fp.write(chunk) if 0 < size <= fp.tell(): fp.seek(size) fp.truncate(size) return fp.getvalue() fp.seek(0) line = fp.readline() self._offset = offset + len( line.encode(encoding=self.encoding), ) return line def async_open( file_specifier: Union[str, Path, FileIOType], mode: str = "r", *args: Any, **kwargs: Any, ) -> Union[BinaryFileWrapper, TextFileWrapper]: if isinstance(file_specifier, (str, Path)): afp = AIOFile(str(file_specifier), mode, *args, **kwargs) else: if args: raise ValueError("Arguments denied when IO[Any] opening.") afp = AIOFile.from_fp(file_specifier, **kwargs) if not afp.mode.binary: return TextFileWrapper(afp) return BinaryFileWrapper(afp) __all__ = ( "BinaryFileWrapper", "FileIOWrapperBase", "LineReader", "Reader", "TextFileWrapper", "Writer", "async_open", "unicode_reader", ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728383963.709268 aiofile-3.9.0/aiofile/version.py0000644000000000000000000000074314701205734013537 0ustar00import importlib.metadata package_metadata = importlib.metadata.metadata("aiofile") __author__ = package_metadata["Author"] __version__ = package_metadata["Version"] author_info = [(package_metadata["Author"], package_metadata["Author-email"])] package_info = package_metadata["Summary"] package_license = package_metadata["License"] project_home = package_metadata["Home-page"] team_email = package_metadata["Author-email"] version_info = tuple(map(int, __version__.split("."))) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728383963.7103572 aiofile-3.9.0/pyproject.toml0000644000000000000000000000457614701205734013014 0ustar00[tool.poetry] name = "aiofile" version = "3.9.0" description = "Asynchronous file operations." license = "Apache-2.0" authors = ["Dmitry Orlov "] readme = "README.md" homepage = "http://github.com/mosquito/aiofile" keywords = ["aio", "python", "asyncio", "fileio", "io"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Natural Language :: Russian", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries", "Topic :: System", "Topic :: System :: Operating System", ] packages = [ { include = "aiofile" }, ] [tool.poetry.dependencies] python = ">=3.8,<4" caio = "~0.9.0" [tool.poetry.group.dev.dependencies] markdown-pytest = "^0.3.2" pytest = ">=8.2.0,<8.3.0" aiomisc-pytest = "^1.2.1" pytest-cov = "^5.0.0" coveralls = "<4" pylama = "^8.4.1" setuptools = "^75.1.0" mypy = "^1.11.2" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.mypy] check_untyped_defs = true disallow_any_generics = false disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true follow_imports = "silent" no_implicit_reexport = true strict_optional = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true files = [ "aiofile", "tests", ] [[tool.mypy.overrides]] module = ["tests.*"] check_untyped_defs = true disallow_incomplete_defs = false disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false warn_unused_ignores = false [tool.pylama] skip = ["*env*", ".*", "*build*"] [tool.pylama.pycodestyle] max_line_length = 80 aiofile-3.9.0/PKG-INFO0000644000000000000000000003333600000000000011127 0ustar00Metadata-Version: 2.1 Name: aiofile Version: 3.9.0 Summary: Asynchronous file operations. Home-page: http://github.com/mosquito/aiofile License: Apache-2.0 Keywords: aio,python,asyncio,fileio,io Author: Dmitry Orlov Author-email: me@mosquito.su Requires-Python: >=3.8,<4 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Natural Language :: Russian Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: System Classifier: Topic :: System :: Operating System Requires-Dist: caio (>=0.9.0,<0.10.0) Description-Content-Type: text/markdown # AIOFile [![Github Actions](https://github.com/mosquito/aiofile/workflows/tox/badge.svg)](https://github.com/mosquito/aiofile/actions?query=branch%3Amaster) [![Latest Version](https://img.shields.io/pypi/v/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![Wheel](https://img.shields.io/pypi/wheel/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![Python Versions](https://img.shields.io/pypi/pyversions/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![License](https://img.shields.io/pypi/l/aiofile.svg)](https://pypi.python.org/pypi/aiofile/) [![Coverage Status](https://coveralls.io/repos/github/mosquito/aiofile/badge.svg?branch=master)](https://coveralls.io/github/mosquito/aiofile?branch=master) Real asynchronous file operations with asyncio support. ## Status Development - Stable ## Features * Since version 2.0.0 using [caio](https://pypi.org/project/caio), which contains linux `libaio` and two thread-based implementations (c-based and pure-python). * AIOFile has no internal pointer. You should pass `offset` and `chunk_size` for each operation or use helpers (Reader or Writer). The simplest way is to use `async_open` for creating object with file-like interface. * For Linux using implementation based on [libaio](https://pagure.io/libaio). * For POSIX (MacOS X and optional Linux) using implementation based on [threadpool](https://github.com/mbrossard/threadpool/). * Otherwise using pure-python thread-based implementation. * Implementation chooses automatically depending on system compatibility. ## Limitations * Linux native AIO implementation is not able to open special files. Asynchronous operations against special fs like `/proc/` `/sys/` are not supported by the kernel. It's not a aiofile's or caio issue. In these cases, you might switch to thread-based implementations (see [Troubleshooting](#troubleshooting) section). However, when used on supported file systems, the linux implementation has a smaller overhead and is preferred but it's not a silver bullet. ## Code examples All code examples require Python 3.6+. ### High-level API #### `async_open` helper Helper mimics python file-like objects, it returns file-like objects with similar but async methods. Supported methods: * `async def read(length = -1)` - reading chunk from file, when length is `-1`, will be reading file to the end. * `async def write(data)` - writing chunk to file * `def seek(offset)` - setting file pointer position * `def tell()` - returns current file pointer position * `async def readline(size=-1, newline="\n")` - read chunks until newline or EOF. Since version 3.7.0 `__aiter__` returns `LineReader`. This method is suboptimal for small lines because it doesn't reuse read buffer. When you want to read file by lines please avoid using `async_open` use `LineReader` instead. * `def __aiter__() -> LineReader` - iterator over lines. * `def iter_chunked(chunk_size: int = 32768) -> Reader` - iterator over chunks. * `.file` property contains AIOFile object Basic example: ```python import asyncio from pathlib import Path from tempfile import gettempdir from aiofile import async_open tmp_filename = Path(gettempdir()) / "hello.txt" async def main(): async with async_open(tmp_filename, 'w+') as afp: await afp.write("Hello ") await afp.write("world") afp.seek(0) print(await afp.read()) await afp.write("Hello from\nasync world") print(await afp.readline()) print(await afp.readline()) asyncio.run(main()) ``` Example without context manager: ```python import asyncio import atexit import os from tempfile import mktemp from aiofile import async_open TMP_NAME = mktemp() atexit.register(os.unlink, TMP_NAME) async def main(): afp = await async_open(TMP_NAME, "w") await afp.write("Hello") await afp.close() asyncio.run(main()) assert open(TMP_NAME, "r").read() == "Hello" ``` Concatenate example program (`cat`): ```python import asyncio import sys from argparse import ArgumentParser from pathlib import Path from aiofile import async_open parser = ArgumentParser( description="Read files line by line using asynchronous io API" ) parser.add_argument("file_name", nargs="+", type=Path) async def main(arguments): for src in arguments.file_name: async with async_open(src, "r") as afp: async for line in afp: sys.stdout.write(line) asyncio.run(main(parser.parse_args())) ``` Copy file example program (`cp`): ```python import asyncio from argparse import ArgumentParser from pathlib import Path from aiofile import async_open parser = ArgumentParser( description="Copying files using asynchronous io API" ) parser.add_argument("source", type=Path) parser.add_argument("dest", type=Path) parser.add_argument("--chunk-size", type=int, default=65535) async def main(arguments): async with async_open(arguments.source, "rb") as src, \ async_open(arguments.dest, "wb") as dest: async for chunk in src.iter_chunked(arguments.chunk_size): await dest.write(chunk) asyncio.run(main(parser.parse_args())) ``` Example with opening already open file pointer: ```python import asyncio from typing import IO, Any from aiofile import async_open async def main(fp: IO[Any]): async with async_open(fp) as afp: await afp.write("Hello from\nasync world") print(await afp.readline()) with open("test.txt", "w+") as fp: asyncio.run(main(fp)) ``` Linux native aio doesn't support reading and writing special files (e.g. procfs/sysfs/unix pipes/etc.), so you can perform operations with these files using compatible context objects. ```python import asyncio from aiofile import async_open from caio import thread_aio_asyncio from contextlib import AsyncExitStack async def main(): async with AsyncExitStack() as stack: # Custom context should be reused ctx = await stack.enter_async_context( thread_aio_asyncio.AsyncioContext() ) # Open special file with custom context src = await stack.enter_async_context( async_open("/proc/cpuinfo", "r", context=ctx) ) # Open regular file with default context dest = await stack.enter_async_context( async_open("/tmp/cpuinfo", "w") ) # Copying file content line by line async for line in src: await dest.write(line) asyncio.run(main()) ``` ### Low-level API The `AIOFile` class is a low-level interface for asynchronous file operations, and the read and write methods accept an `offset=0` in bytes at which the operation will be performed. This allows you to do many independent IO operations on a once open file without moving the virtual carriage. For example, you may make 10 concurrent HTTP requests by specifying the `Range` header, and asynchronously write one opened file, while the offsets must either be calculated manually, or use 10 instances of `Writer` with specified initial offsets. In order to provide sequential reading and writing, there is `Writer`, `Reader` and `LineReader`. Keep in mind `async_open` is not the same as AIOFile, it provides a similar interface for file operations, it simulates methods like read or write as it is implemented in the built-in open. ```python import asyncio from aiofile import AIOFile async def main(): async with AIOFile("hello.txt", 'w+') as afp: payload = "Hello world\n" await asyncio.gather( *[afp.write(payload, offset=i * len(payload)) for i in range(10)] ) await afp.fsync() assert await afp.read(len(payload) * 10) == payload * 10 asyncio.run(main()) ``` The Low-level API in fact is just little bit sugared `caio` API. ```python import asyncio from aiofile import AIOFile async def main(): async with AIOFile("/tmp/hello.txt", 'w+') as afp: await afp.write("Hello ") await afp.write("world", offset=7) await afp.fsync() print(await afp.read()) asyncio.run(main()) ``` #### `Reader` and `Writer` When you want to read or write file linearly following example might be helpful. ```python import asyncio from aiofile import AIOFile, Reader, Writer async def main(): async with AIOFile("/tmp/hello.txt", 'w+') as afp: writer = Writer(afp) reader = Reader(afp, chunk_size=8) await writer("Hello") await writer(" ") await writer("World") await afp.fsync() async for chunk in reader: print(chunk) asyncio.run(main()) ``` #### `LineReader` - read file line by line LineReader is a helper that is very effective when you want to read a file linearly and line by line. It contains a buffer and will read the fragments of the file chunk by chunk into the buffer, where it will try to find lines. The default chunk size is 4KB. ```python import asyncio from aiofile import AIOFile, LineReader, Writer async def main(): async with AIOFile("/tmp/hello.txt", 'w+') as afp: writer = Writer(afp) await writer("Hello") await writer(" ") await writer("World") await writer("\n") await writer("\n") await writer("From async world") await afp.fsync() async for line in LineReader(afp): print(line) asyncio.run(main()) ``` When you want to read file by lines please avoid to use `async_open` use `LineReader` instead. ## More examples Useful examples with `aiofile` ### Async CSV Dict Reader ```python import asyncio import io from csv import DictReader from aiofile import AIOFile, LineReader class AsyncDictReader: def __init__(self, afp, **kwargs): self.buffer = io.BytesIO() self.file_reader = LineReader( afp, line_sep=kwargs.pop('line_sep', '\n'), chunk_size=kwargs.pop('chunk_size', 4096), offset=kwargs.pop('offset', 0), ) self.reader = DictReader( io.TextIOWrapper( self.buffer, encoding=kwargs.pop('encoding', 'utf-8'), errors=kwargs.pop('errors', 'replace'), ), **kwargs, ) self.line_num = 0 def __aiter__(self): return self async def __anext__(self): if self.line_num == 0: header = await self.file_reader.readline() self.buffer.write(header) line = await self.file_reader.readline() if not line: raise StopAsyncIteration self.buffer.write(line) self.buffer.seek(0) try: result = next(self.reader) except StopIteration as e: raise StopAsyncIteration from e self.buffer.seek(0) self.buffer.truncate(0) self.line_num = self.reader.line_num return result async def main(): async with AIOFile('sample.csv', 'rb') as afp: async for item in AsyncDictReader(afp): print(item) asyncio.run(main()) ``` ## Troubleshooting The caio `linux` implementation works normal for modern linux kernel versions and file systems. So you may have problems specific for your environment. It's not a bug and might be resolved some ways: 1. Upgrade the kernel 2. Use compatible file systems 3. Use threads based or pure python implementation. The caio since version 0.7.0 contains some ways to do this. 1. In runtime use the environment variable `CAIO_IMPL` with possible values: * `linux` - use native linux kernels aio mechanism * `thread` - use thread based implementation written in C * `python` - use pure python implementation 2. File `default_implementation` located near `__init__.py` in caio installation path. It's useful for distros package maintainers. This file might contains comments (lines starts with `#` symbol) and the first line should be one of `linux` `thread` or `python`. 3. You might manually manage contexts: ```python import asyncio from aiofile import async_open from caio import linux_aio_asyncio, thread_aio_asyncio async def main(): linux_ctx = linux_aio_asyncio.AsyncioContext() threads_ctx = thread_aio_asyncio.AsyncioContext() async with async_open("/tmp/test.txt", "w", context=linux_ctx) as afp: await afp.write("Hello") async with async_open("/tmp/test.txt", "r", context=threads_ctx) as afp: print(await afp.read()) asyncio.run(main()) ```