├── .github └── workflows │ └── test.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── example ├── bench │ ├── asyncio_bench.py │ ├── python_bench.py │ └── shakti_bench.py ├── client.py ├── echo-server-client.py ├── file.py ├── hi.py ├── open.py └── os.py ├── pyproject.toml ├── setup.py ├── src └── shakti │ ├── __init__.py │ ├── core │ ├── __init__.pxd │ ├── base.pxd │ └── base.pyx │ ├── event │ ├── __init__.pxd │ ├── entry.pxd │ ├── entry.pyx │ ├── run.pxd │ ├── run.pyx │ ├── sleep.pyx │ └── task.pyx │ ├── io │ ├── __init__.pxd │ ├── common.pxd │ ├── common.pyx │ ├── file.pxd │ ├── file.pyx │ ├── socket.pxd │ ├── socket.pyx │ ├── statx.pxd │ └── statx.pyx │ ├── lib │ ├── __init__.pxd │ ├── error.pxd │ ├── error.pyx │ ├── path.pxd │ ├── path.pyx │ ├── time.pxd │ └── time.pyx │ └── os │ ├── __init__.pxd │ ├── mk.pyx │ ├── random.pyx │ ├── remove.pyx │ └── rename.pyx └── test ├── conftest.py ├── core └── asyncbase_test.py ├── event ├── run_test.py ├── sleep_test.py ├── sqe_test.py └── task_test.py ├── io ├── common_test.py ├── file_test.py ├── socket_test.py └── statx_test.py ├── lib ├── error_test.py └── path_test.py └── os ├── mk_test.py ├── random_test.py ├── remove_test.py ├── rename_test.py └── time_test.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: [push, pull_request] # yamllint disable-line rule:truthy 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] 14 | 15 | steps: 16 | # Setup Python 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | # Runners 23 | - name: Install dependencies 24 | run: | 25 | python3 -m pip install --upgrade pip flake8 26 | python3 -m pip install --upgrade .[test] 27 | # TODO: need to add `cython-lint` 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Test with PyTest 35 | run: | 36 | python3 -m pytest test 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSTEALTH/Shakti/a117dc862a673a0ea24a41d981eeb02ae21ba1ca/LICENSE.txt -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft example 2 | graft test 3 | 4 | exclude src/shakti/*/*.c 5 | global-exclude *.py[cod] # note: must run last to exclude properly 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |test-status| 2 | 3 | Shakti (Work in progress ... ) 4 | ============================== 5 | 6 | Shakti will be providing developers with fast & powerful yet easy to use Python Async Interface, without the complexity of using `Liburing`_ and ``io_uring`` directly. 7 | 8 | * Mostly all events are planned to go through ``io_uring`` backend, this is a design choice. 9 | 10 | 11 | *****NOTE***** 12 | -------------- 13 | 14 | Work in progress... This project is in early ``planning`` state, so... its ok to play around with it but not for any type of serious development, yet! 15 | 16 | 17 | Requires 18 | -------- 19 | 20 | - Linux 6.11+ 21 | - Python 3.9+ 22 | 23 | 24 | Install directly from GitHub 25 | ---------------------------- 26 | 27 | .. code-block:: python 28 | 29 | # To install | upgrade. Includes ``liburing``. 30 | python3 -m pip install --upgrade git+https://github.com/YoSTEALTH/Shakti 31 | 32 | # To uninstall 33 | python3 -m pip uninstall shakti 34 | 35 | 36 | Docs 37 | ---- 38 | 39 | To find out all the class, functions and definitions: 40 | 41 | .. code-block:: python 42 | 43 | import shakti 44 | 45 | print(dir(shakti)) # to see all the importable names (this will not load all the modules) 46 | help(shakti) # to see all the help docs (this will load all the modules.) 47 | help(shakti.Statx) # to see specific function/class docs. 48 | 49 | 50 | Example 51 | ------- 52 | 53 | .. code-block:: python 54 | 55 | from shakti import Timeit, run, sleep 56 | 57 | 58 | async def main(): 59 | print('hi', end='') 60 | for i in range(4): 61 | if i: 62 | print('.', end='') 63 | await sleep(1) 64 | print('bye!') 65 | 66 | 67 | if __name__ == '__main__': 68 | with Timeit(): 69 | run(main()) 70 | 71 | File 72 | ____ 73 | 74 | .. code-block:: python 75 | 76 | from shakti import O_CREAT, O_RDWR, O_APPEND, run, open, read, write, close 77 | 78 | 79 | async def main(): 80 | fd = await open('/tmp/shakti-test.txt', O_CREAT | O_RDWR | O_APPEND) 81 | print('fd:', fd) 82 | 83 | wrote = await write(fd, b'hi...bye!') 84 | print('wrote:', wrote) 85 | 86 | content = await read(fd, 1024) 87 | print('read:', content) 88 | 89 | await close(fd) 90 | print('closed.') 91 | 92 | 93 | if __name__ == '__main__': 94 | run(main()) 95 | 96 | 97 | .. code-block:: python 98 | 99 | from shakti import File, run 100 | 101 | 102 | async def main(): 103 | # create, read & write. 104 | async with File('/tmp/test.txt', '!x+') as file: 105 | wrote = await file.write('hi... bye!') 106 | print('wrote:', wrote) 107 | 108 | content = await file.read(5, 0) # seek is set to `0` 109 | print('read:', content) 110 | 111 | # Other 112 | print('fd:', file.fileno) 113 | print('path:', file.path) 114 | print('active:', bool(file)) 115 | 116 | 117 | if __name__ == '__main__': 118 | run(main()) 119 | 120 | # Refer to `help(File)` to see full features of `File` class. 121 | 122 | 123 | OS 124 | __ 125 | 126 | .. code-block:: python 127 | 128 | from shakti import Statx, run, mkdir, rename, remove, exists 129 | 130 | 131 | async def main(): 132 | mkdir_path = '/tmp/shakti-mkdir' 133 | rename_path = '/tmp/shakti-rename' 134 | 135 | # create directory 136 | print('create directory:', mkdir_path) 137 | await mkdir(mkdir_path) 138 | 139 | # check directory stats 140 | async with Statx(mkdir_path) as stat: 141 | print('is directory:', stat.isdir) 142 | print('modified time:', stat.stx_mtime) 143 | 144 | # rename / move 145 | print('rename directory:', mkdir_path, '-to->', rename_path) 146 | await rename(mkdir_path, rename_path) 147 | 148 | # check exists 149 | print(f'{mkdir_path!r} exists:', await exists(mkdir_path)) 150 | print(f'{rename_path!r} exists:', await exists(rename_path)) 151 | 152 | # remove 153 | await remove(rename_path, is_dir=True) 154 | print(f'removed {rename_path!r} exists:', await exists(rename_path)) 155 | print('done.') 156 | 157 | 158 | if __name__ == '__main__': 159 | run(main()) 160 | 161 | Socket 162 | ______ 163 | 164 | .. code-block:: python 165 | 166 | from shakti import SOL_SOCKET, SO_REUSEADDR, run, socket, bind, listen, accept, \ 167 | connect, recv, send, shutdown, close, sleep, setsockopt, task 168 | 169 | 170 | async def echo_server(host, port): 171 | print('Starting Server') 172 | server_fd = await socket() 173 | try: 174 | await setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, True) 175 | await bind(server_fd, host, port) 176 | await listen(server_fd, 1) 177 | while client_fd := await accept(server_fd): 178 | await task(client_handler(client_fd)) 179 | break # only handles 1 client and exit 180 | finally: 181 | await close(server_fd) 182 | print('Closed Server') 183 | 184 | 185 | async def client_handler(client_fd): 186 | try: 187 | print('server recv:', await recv(client_fd, 1024)) 188 | print('server sent:', await send(client_fd, b'hi from server')) 189 | await shutdown(client_fd) 190 | finally: 191 | await close(client_fd) 192 | 193 | 194 | async def echo_client(host, port): 195 | await sleep(.001) # wait for `echo_server` to start up. 196 | client_fd = await socket() 197 | await connect(client_fd, host, port) 198 | print('client sent:', await send(client_fd, b'hi from client')) 199 | print('client recv:', await recv(client_fd, 1024)) 200 | await close(client_fd) 201 | 202 | 203 | if __name__ == '__main__': 204 | host = '127.0.0.1' 205 | port = 12345 206 | run(echo_server(host, port), echo_client(host, port)) 207 | 208 | 209 | .. code-block:: python 210 | 211 | from shakti import run, socket, connect, recv, send, close 212 | 213 | 214 | async def client(host, port, path, header): 215 | print('client:', f'{host}:{port}{path}') 216 | received = bytearray() 217 | client_fd = await socket() 218 | await connect(client_fd, host, port) 219 | print('client sent:', await send(client_fd, header)) 220 | while data := await recv(client_fd, 1024): 221 | received.extend(data) 222 | print('client recv:', len(received), received) 223 | await close(client_fd) 224 | print('closed') 225 | 226 | 227 | if __name__ == '__main__': 228 | host = 'example.com' 229 | port = 80 230 | path = '/' 231 | header = f'GET {path} HTTP/1.0\r\nHost: {host}\r\nUser-Agent: Testing\r\n\r\n'.encode() 232 | # header = f'GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: Testing\r\nConnection:close\r\n\r\n'.encode() 233 | run(client(host, port, path, header)) 234 | 235 | 236 | .. _Liburing: https://github.com/YoSTEALTH/Liburing 237 | 238 | .. |test-status| image:: https://github.com/YoSTEALTH/Shakti/actions/workflows/test.yml/badge.svg?branch=master&event=push 239 | :target: https://github.com/YoSTEALTH/Shakti/actions/workflows/test.yml 240 | :alt: Test status 241 | -------------------------------------------------------------------------------- /example/bench/asyncio_bench.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR, IPPROTO_TCP, TCP_NODELAY, \ 3 | socket 4 | 5 | LISTEN = 1024 6 | BUFFSIZE = 1024 7 | RESPONSE = b'HTTP/1.1 200 OK\r\n' 8 | RESPONSE += b'Content-Type: text/html\r\n' 9 | RESPONSE += b'Content-Length: 131\r\n' 10 | RESPONSE += b'Connection:close\r\n\r\n' 11 | RESPONSE += b'Hello' 12 | RESPONSE += b'' 13 | RESPONSE += b'Hello world!' 14 | 15 | 16 | async def echo_server(loop, address): 17 | print('Asyncio Start') 18 | sock = socket(AF_INET, SOCK_STREAM) 19 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 20 | sock.bind(address) 21 | sock.listen(LISTEN) 22 | sock.setblocking(False) 23 | with sock: 24 | while True: 25 | client, addr = await loop.sock_accept(sock) 26 | loop.create_task(echo_client(loop, client)) 27 | print('Asyncio Closed') 28 | 29 | 30 | async def echo_client(loop, client): 31 | with client: 32 | while await loop.sock_recv(client, BUFFSIZE): 33 | await loop.sock_sendall(client, RESPONSE) 34 | 35 | 36 | if __name__ == '__main__': 37 | loop = asyncio.new_event_loop() 38 | asyncio.set_event_loop(loop) 39 | loop.set_debug(False) 40 | loop.create_task(echo_server(loop, ('127.0.0.1', 12345))) 41 | loop.run_forever() 42 | # e.g: `siege -b -c100 -r100 --delay=1 http://127.0.0.1:12345/` 43 | -------------------------------------------------------------------------------- /example/bench/python_bench.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | LISTEN = 1024 5 | BUFFSIZE = 1024 6 | RESPONSE = b'HTTP/1.1 200 OK\r\n' 7 | RESPONSE += b'Content-Type: text/html\r\n' 8 | RESPONSE += b'Content-Length: 131\r\n' 9 | RESPONSE += b'Connection:close\r\n\r\n' 10 | RESPONSE += b'Hello' 11 | RESPONSE += b'' 12 | RESPONSE += b'Hello world!' 13 | 14 | 15 | def echo_server(host, port): 16 | print('Starting Python Server') 17 | sock = socket.socket() 18 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 19 | sock.bind((host, port)) 20 | sock.listen(LISTEN) 21 | while True: 22 | client, addr = sock.accept() 23 | client_handler(client) # note: runs synchronously 24 | sock.close() 25 | print('Closed Python Server') 26 | 27 | 28 | def client_handler(client): 29 | with client: 30 | while client.recv(BUFFSIZE): 31 | client.sendall(RESPONSE) 32 | 33 | 34 | if __name__ == '__main__': 35 | echo_server('127.0.0.1', 12345) 36 | # e.g: `siege -b -c100 -r100 --delay=1 http://127.0.0.1:12345/` 37 | -------------------------------------------------------------------------------- /example/bench/shakti_bench.py: -------------------------------------------------------------------------------- 1 | from shakti import SOL_SOCKET, SO_REUSEADDR, \ 2 | run, task, socket, setsockopt, bind, listen, accept, close, recv, sendall 3 | 4 | 5 | LISTEN = 1024 6 | BUFFSIZE = 1024 7 | RESPONSE = b'HTTP/1.1 200 OK\r\n' 8 | RESPONSE += b'Content-Type: text/html\r\n' 9 | RESPONSE += b'Content-Length: 131\r\n' 10 | RESPONSE += b'Connection:close\r\n\r\n' 11 | RESPONSE += b'Hello' 12 | RESPONSE += b'' 13 | RESPONSE += b'Hello world!' 14 | 15 | 16 | async def echo_server(host, port): 17 | print('Starting Shakti Server') 18 | server_fd = await socket() 19 | try: 20 | await setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, True) 21 | await bind(server_fd, host, port) 22 | await listen(server_fd, LISTEN) 23 | while client_fd := await accept(server_fd): 24 | await task(client_handler(client_fd)) 25 | finally: 26 | await close(server_fd) 27 | print('Closed Shakti Server') 28 | 29 | 30 | async def client_handler(client_fd): 31 | while await recv(client_fd, BUFFSIZE): 32 | await sendall(client_fd, RESPONSE) 33 | await close(client_fd) 34 | 35 | 36 | if __name__ == '__main__': 37 | run(echo_server('127.0.0.1', 12345)) 38 | # e.g: `siege -b -c100 -r100 --delay=1 http://127.0.0.1:12345/` 39 | -------------------------------------------------------------------------------- /example/client.py: -------------------------------------------------------------------------------- 1 | from shakti import run, socket, connect, recv, send, close 2 | 3 | 4 | async def client(host, port, path, header): 5 | print('client:', f'{host}:{port}{path}') 6 | received = bytearray() 7 | client_fd = await socket() 8 | await connect(client_fd, host, port) 9 | print('client sent:', await send(client_fd, header)) 10 | while data := await recv(client_fd, 1024): 11 | received.extend(data) 12 | print('client recv:', len(received), received) 13 | await close(client_fd) 14 | print('closed') 15 | 16 | 17 | if __name__ == '__main__': 18 | host = 'example.com' 19 | port = 80 20 | path = '/' 21 | header = f'GET {path} HTTP/1.0\r\nHost: {host}\r\nUser-Agent: Testing\r\n\r\n'.encode() 22 | # header = f'GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: Testing\r\nConnection:close\r\n\r\n'.encode() 23 | run(client(host, port, path, header)) 24 | -------------------------------------------------------------------------------- /example/echo-server-client.py: -------------------------------------------------------------------------------- 1 | from shakti import SOL_SOCKET, SO_REUSEADDR, run, socket, bind, listen, accept, \ 2 | connect, recv, send, shutdown, close, sleep, setsockopt, task 3 | 4 | 5 | async def echo_server(host, port): 6 | print('Starting Server') 7 | server_fd = await socket() 8 | try: 9 | await setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, True) 10 | await bind(server_fd, host, port) 11 | await listen(server_fd, 1) 12 | while client_fd := await accept(server_fd): 13 | await task(client_handler(client_fd)) 14 | break # only handles 1 client and exit 15 | finally: 16 | await close(server_fd) 17 | print('Closed Server') 18 | 19 | 20 | async def client_handler(client_fd): 21 | try: 22 | print('server recv:', await recv(client_fd, 1024)) 23 | print('server sent:', await send(client_fd, b'hi from server')) 24 | await shutdown(client_fd) 25 | finally: 26 | await close(client_fd) 27 | 28 | 29 | async def echo_client(host, port): 30 | await sleep(.001) # wait for `echo_server` to start up. 31 | client_fd = await socket() 32 | await connect(client_fd, host, port) 33 | print('client sent:', await send(client_fd, b'hi from client')) 34 | print('client recv:', await recv(client_fd, 1024)) 35 | await close(client_fd) 36 | 37 | 38 | if __name__ == '__main__': 39 | host = '127.0.0.1' 40 | port = 12345 41 | run(echo_server(host, port), echo_client(host, port)) 42 | -------------------------------------------------------------------------------- /example/file.py: -------------------------------------------------------------------------------- 1 | from shakti import File, run 2 | 3 | 4 | async def main(): 5 | # create, read & write. 6 | async with File('/tmp/test.txt', '!x+') as file: 7 | wrote = await file.write('hi... bye!') 8 | print('wrote:', wrote) 9 | 10 | content = await file.read(5, 0) # seek is set to `0` 11 | print('read:', content) 12 | 13 | # Other 14 | print('fd:', file.fileno) 15 | print('path:', file.path) 16 | print('active:', bool(file)) 17 | 18 | 19 | if __name__ == '__main__': 20 | run(main()) 21 | 22 | # Refer to `help(File)` to see full features of `File` class. 23 | # help(File) 24 | -------------------------------------------------------------------------------- /example/hi.py: -------------------------------------------------------------------------------- 1 | from shakti import Timeit, run, sleep 2 | 3 | 4 | async def main(): 5 | print('hi', end='') 6 | for i in range(4): 7 | if i: 8 | print('.', end='') 9 | await sleep(1) 10 | print('bye!') 11 | 12 | 13 | if __name__ == '__main__': 14 | with Timeit(): 15 | run(main()) 16 | -------------------------------------------------------------------------------- /example/open.py: -------------------------------------------------------------------------------- 1 | from shakti import O_CREAT, O_RDWR, O_APPEND, run, open, read, write, close 2 | 3 | 4 | async def main(): 5 | fd = await open('/tmp/shakti-test.txt', O_CREAT | O_RDWR | O_APPEND) 6 | print('fd:', fd) 7 | 8 | wrote = await write(fd, b'hi...bye!') 9 | print('wrote:', wrote) 10 | 11 | content = await read(fd, 1024) 12 | print('read:', content) 13 | 14 | await close(fd) 15 | print('closed.') 16 | 17 | 18 | if __name__ == '__main__': 19 | run(main()) 20 | -------------------------------------------------------------------------------- /example/os.py: -------------------------------------------------------------------------------- 1 | from shakti import Statx, run, mkdir, rename, remove, exists 2 | 3 | 4 | async def main(): 5 | mkdir_path = '/tmp/shakti-mkdir' 6 | rename_path = '/tmp/shakti-rename' 7 | 8 | # create directory 9 | print('create directory:', mkdir_path) 10 | await mkdir(mkdir_path) 11 | 12 | # check directory stats 13 | async with Statx(mkdir_path) as stat: 14 | print('is directory:', stat.isdir) 15 | print('modified time:', stat.stx_mtime) 16 | 17 | # rename / move 18 | print('rename directory:', mkdir_path, '-to->', rename_path) 19 | await rename(mkdir_path, rename_path) 20 | 21 | # check exists 22 | print(f'{mkdir_path!r} exists:', await exists(mkdir_path)) 23 | print(f'{rename_path!r} exists:', await exists(rename_path)) 24 | 25 | # remove 26 | await remove(rename_path, is_dir=True) 27 | print(f'removed {rename_path!r} exists:', await exists(rename_path)) 28 | print('done.') 29 | 30 | 31 | if __name__ == '__main__': 32 | run(main()) 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = ["setuptools>=60", "wheel", "cython>=3", 4 | "liburing @ git+https://github.com/YoSTEALTH/Liburing.git"] 5 | 6 | [project] 7 | name = "shakti" 8 | dynamic = ["version"] 9 | authors = [{name="Ritesh"}] 10 | readme = {file="README.rst", content-type="text/x-rst"} 11 | license = {file="LICENSE.txt", content-type="text"} 12 | requires-python = ">=3.8" 13 | dependencies = ["dynamic-import", "liburing @ git+https://github.com/YoSTEALTH/Liburing.git"] 14 | description = "..." 15 | classifiers = ["Topic :: Software Development", 16 | "License :: Other/Proprietary License", 17 | "Intended Audience :: Developers", 18 | "Operating System :: POSIX :: Linux", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Development Status :: 1 - Planning"] 25 | # 1 - Planning 26 | # 2 - Pre-Alpha 27 | # 3 - Alpha 28 | # 4 - Beta 29 | # 5 - Production/Stable 30 | # 6 - Mature 31 | # 7 - Inactive 32 | 33 | [project.urls] 34 | Homepage = "https://github.com/YoSTEALTH/Shakti" 35 | Issues = "https://github.com/YoSTEALTH/Shakti/issues" 36 | 37 | [project.optional-dependencies] 38 | test = ["pytest"] 39 | 40 | [tool.setuptools.packages.find] 41 | where = ["src"] 42 | 43 | [tool.setuptools.dynamic] 44 | version = {attr="shakti.__version__"} 45 | 46 | [tool.setuptools.package-data] 47 | "*" = ["*.pyx", "*.pxd"] 48 | 49 | # for debugging locally START >>> 50 | # [tool.pytest.ini_options] 51 | # pythonpath = ["src"] 52 | 53 | # [tool.coverage.run] 54 | # plugins = ["Cython.Coverage"] 55 | 56 | # [tool.cython-lint] 57 | # max-line-length = 100 58 | # ignore = ['E221'] 59 | # for debugging locally END <<< 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import cpu_count 2 | from os.path import join, dirname 3 | from setuptools import setup 4 | from importlib.util import find_spec 5 | from setuptools.command.build_ext import build_ext 6 | from Cython.Build import cythonize 7 | from Cython.Compiler import Options 8 | from Cython.Distutils import Extension 9 | 10 | 11 | class BuildExt(build_ext): 12 | def initialize_options(self): 13 | super().initialize_options() 14 | self.parallel = threads # manually set 15 | 16 | def finalize_options(self): 17 | super().finalize_options() 18 | try: 19 | include_path = join(dirname(find_spec('liburing').origin), 'include') 20 | self.include_dirs.append(include_path) 21 | except AttributeError: 22 | raise ImportError('can not find installed `liburing`') from None 23 | 24 | 25 | if __name__ == '__main__': 26 | threads = cpu_count() 27 | # compiler options 28 | Options.annotate = False 29 | Options.fast_fail = True 30 | Options.docstrings = True 31 | Options.warning_errors = False 32 | 33 | extension = [Extension(name='shakti.*', # where the `.so` will be saved. 34 | sources=['src/shakti/*/*.pyx'], 35 | language='c', 36 | extra_compile_args=['-O3', '-g0'])] 37 | 38 | setup(cmdclass={'build_ext': BuildExt}, 39 | ext_modules=cythonize(extension, 40 | nthreads=threads, 41 | compiler_directives={'language_level': 3, 42 | 'embedsignature': True, # show `__doc__` 43 | 'boundscheck': False, 44 | 'wraparound': False})) 45 | -------------------------------------------------------------------------------- /src/shakti/__init__.py: -------------------------------------------------------------------------------- 1 | from dynamic_import import importer 2 | 3 | 4 | __version__ = '2024.8.1' 5 | 6 | 7 | importer() # helps this project manage all import needs. 8 | -------------------------------------------------------------------------------- /src/shakti/core/__init__.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSTEALTH/Shakti/a117dc862a673a0ea24a41d981eeb02ae21ba1ca/src/shakti/core/__init__.pxd -------------------------------------------------------------------------------- /src/shakti/core/base.pxd: -------------------------------------------------------------------------------- 1 | from ..lib.error cimport UnsupportedOperation 2 | 3 | 4 | cdef class AsyncBase: 5 | cdef: 6 | bint __awaited__ 7 | unicode msg 8 | -------------------------------------------------------------------------------- /src/shakti/core/base.pyx: -------------------------------------------------------------------------------- 1 | cdef class AsyncBase: 2 | ''' Allows multiple ways of using async class 3 | 4 | Example 5 | >>> class File(AsyncBase): 6 | ... 7 | ... def __init__(self, path): 8 | ... self.path = path 9 | ... 10 | ... async def __ainit__(self): 11 | ... await self.open() 12 | ... 13 | ... async def open(self): 14 | ... await async_open(self.path) 15 | 16 | # usage-1 17 | >>> file = await File(...) 18 | >>> await file.close() 19 | 20 | # usage-2 - `__ainit__` method is not called 21 | >>> file = File(...) 22 | >>> await file.open() 23 | >>> await file.close() 24 | 25 | # usage-3 26 | >>> async with File(...) as file: 27 | ... ... 28 | 29 | # usage-4 30 | >>> async with Client() as (client, addr): 31 | ... ... 32 | ''' 33 | async def __ainit__(self): 34 | ''' `__ainit__` method must be created 35 | 36 | Note 37 | `__ainit__` method is called while using as: 38 | ``await AsyncBase()`` 39 | # or 40 | ``async with AsyncBase():`` 41 | ''' 42 | self.msg = f'`{self.__class__.__name__}()` - ' 43 | self.msg += 'user must implement `async def __ainit__(self)` method' 44 | raise NotImplementedError(self.msg) 45 | 46 | # midsync 47 | def __await__(self): 48 | return self.__aenter__().__await__() 49 | # note: `__await__` is called while `await AsyncBase()` 50 | 51 | async def __aenter__(self): 52 | if self.__awaited__: 53 | return self 54 | 55 | cdef object r = await self.__ainit__() 56 | self.__awaited__ = True 57 | return self if r is None else r 58 | 59 | async def __aexit__(self, *errors): 60 | if any(errors): 61 | return False 62 | 63 | def __enter__(self): 64 | self.msg = f'`with {self.__class__.__name__}() ...` ' 65 | self.msg += 'used, should be `async with {name}() ...`' 66 | raise SyntaxError(self.msg) 67 | 68 | def __exit__(self): 69 | self.msg = f'`{self.__class__.__name__}()` - ' 70 | self.msg += 'use of `__exit__` is not supported, use `__aexit__`' 71 | raise RuntimeError(self.msg) 72 | # note: this exception does not get triggered normally but added it just 73 | # in case user does some funny business. 74 | -------------------------------------------------------------------------------- /src/shakti/event/__init__.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSTEALTH/Shakti/a117dc862a673a0ea24a41d981eeb02ae21ba1ca/src/shakti/event/__init__.pxd -------------------------------------------------------------------------------- /src/shakti/event/entry.pxd: -------------------------------------------------------------------------------- 1 | from cpython.object cimport PyObject 2 | from cpython.ref cimport Py_XINCREF 3 | from liburing.lib.type cimport __u8, __u16, __s32, __u64 4 | from liburing.queue cimport IOSQE_ASYNC, IOSQE_IO_HARDLINK, IOSQE_IO_LINK, \ 5 | io_uring_sqe_set_flags, io_uring_sqe_set_data64, io_uring_sqe, \ 6 | io_uring_prep_nop 7 | from liburing.helper cimport io_uring_put_sqe 8 | from liburing.error cimport trap_error, index_error 9 | 10 | 11 | cpdef enum JOBS: 12 | NOJOB = 0 13 | CORO = 1U << 0 # 1 14 | RING = 1U << 1 # 2 15 | ENTRY = 1U << 2 # 3 16 | ENTRIES = 1U << 3 # 4 17 | 18 | 19 | cdef class SQE(io_uring_sqe): 20 | cdef: 21 | __u8 job 22 | bint sub_coro, error 23 | object coro 24 | tuple _coro 25 | unsigned int flags, link_flag 26 | readonly __s32 result 27 | 28 | 29 | # cpdef enum __entry_define__: 30 | # IOSQE_ASYNC = __IOSQE_ASYNC 31 | # IORING_TIMEOUT_BOOTTIME = __IORING_TIMEOUT_BOOTTIME 32 | # IORING_TIMEOUT_REALTIME = __IORING_TIMEOUT_REALTIME 33 | -------------------------------------------------------------------------------- /src/shakti/event/entry.pyx: -------------------------------------------------------------------------------- 1 | from types import CoroutineType 2 | 3 | 4 | cdef class SQE(io_uring_sqe): 5 | 6 | # note: `num` is used by `io_uring_sqe` 7 | def __init__(self, __u16 num=1, bint error=True, 8 | *, unsigned int flags=0, unsigned int link_flag=IOSQE_IO_HARDLINK): 9 | ''' Shakti Queue Entry 10 | 11 | Type 12 | num: int - number of entries to create 13 | error: bool - automatically raise error 14 | flags: int 15 | link_flag: int 16 | return: None 17 | 18 | Example 19 | # single 20 | >>> sqe = SQE() 21 | >>> io_uring_prep_openat(sqe, b'/dev/zero') 22 | >>> await sqe 23 | >>> sqe.result # fd 24 | 4 25 | 26 | # multiple 27 | >>> sqe = SQE(2) 28 | >>> io_uring_prep_openat(sqe[0], b'/dev/zero') 29 | >>> io_uring_prep_openat(sqe[1], b'/dev/zero') 30 | >>> await sqe 31 | >>> sqe[0].result # fd 32 | 4 33 | >>> sqe[1].result # fd 34 | 5 35 | 36 | # context manager 37 | >>> async with SQE() as sqe: 38 | ... io_uring_prep_openat(sqe, b'/dev/zero') 39 | >>> sqe.result # fd 40 | 4 41 | 42 | # do not catch & raise error for `sqe.result` 43 | >>> SQE(123, False) # or 44 | >>> SQE(123, error=False) 45 | 46 | Note 47 | - `SQE.user_data` is automatically set by `SQE()`. 48 | - Multiple sqe's e.g: `SQE(2)` are linked using `IOSQE_IO_HARDLINK` 49 | - context manger runs await in `__aexit__` thus need to check result 50 | outside of `aysnc with` block 51 | ''' 52 | self.error = error 53 | self.flags = flags 54 | if link_flag and not (link_flag & (IOSQE_IO_LINK | IOSQE_IO_HARDLINK)): 55 | raise ValueError('SQE(link_flag) must be `IOSQE_IO_HARDLINK` or `IOSQE_IO_LINK`') 56 | self.link_flag = link_flag 57 | 58 | def __getitem__(self, unsigned int index): 59 | cdef SQE sqe 60 | if self.ptr is not NULL: 61 | if index == 0: 62 | return self 63 | elif self.len and index < self.len: 64 | if (sqe := self.ref[index-1]) is not None: 65 | return sqe # return from reference cache 66 | # create new reference class 67 | sqe = SQE(0) # `0` is set to indicated `ptr` memory is not managed 68 | sqe.ptr = &self.ptr[index] 69 | self.ref[index-1] = sqe # cache sqe as this class attribute 70 | return sqe 71 | index_error(self, index, 'out of `sqe`') 72 | 73 | def __await__(self): 74 | cdef: 75 | SQE sqe 76 | __u16 i 77 | object r 78 | 79 | if self.len: 80 | if self.len == 1: # single 81 | self.job = ENTRY 82 | self.coro = None 83 | self.result = 0 84 | io_uring_sqe_set_flags(self, self.flags) 85 | io_uring_sqe_set_data64(self, <__u64>self) 86 | elif 1025 > self.len > 1: # multiple 87 | for i in range(self.len): 88 | if not i: # first 89 | self.job = ENTRIES 90 | self.coro = None 91 | self.result = 0 92 | io_uring_sqe_set_flags(self, self.flags | self.link_flag) 93 | io_uring_sqe_set_data64(self, <__u64>self) 94 | else: 95 | sqe = self[i] 96 | sqe.coro = None 97 | sqe.result = 0 98 | io_uring_sqe_set_data64(sqe, <__u64>sqe) 99 | if i < self.len-1: # middle 100 | sqe.job = NOJOB 101 | io_uring_sqe_set_flags(sqe, self.flags | self.link_flag) 102 | else: # last 103 | sqe.job = ENTRIES 104 | io_uring_sqe_set_flags(sqe, self.flags) 105 | else: 106 | raise NotImplementedError('num > 1024') 107 | r = yield self 108 | if self.len and self.error: 109 | if self.len == 1: 110 | trap_error(self.result) 111 | else: 112 | for i in range(self.len): 113 | trap_error(self[i].result) 114 | # else: don't catch error 115 | return r 116 | 117 | # midsync 118 | def __aexit__(self, *errors): 119 | if any(errors): 120 | return False 121 | return self # `await self` 122 | 123 | async def __aenter__(self): 124 | return self 125 | -------------------------------------------------------------------------------- /src/shakti/event/run.pxd: -------------------------------------------------------------------------------- 1 | from cpython.object cimport PyObject 2 | from cpython.ref cimport Py_XINCREF, Py_XDECREF 3 | from liburing.lib.type cimport __s32, __u64, uintptr_t 4 | from liburing.queue cimport io_uring, io_uring_queue_init, io_uring_queue_exit, \ 5 | io_uring_prep_nop, io_uring_submit, io_uring_cqe, io_uring_sq_ready, \ 6 | io_uring_cq_advance, io_uring_wait_cqe, io_uring_for_each_cqe, \ 7 | io_uring_sqe_set_flags, io_uring_sqe_set_data64, \ 8 | io_uring_sq_space_left 9 | from liburing.helper cimport io_uring_put_sqe 10 | from .entry cimport NOJOB, CORO, RING, ENTRY, ENTRIES, SQE 11 | 12 | 13 | cdef void __check_coroutine(tuple coroutine, unsigned int coro_len, unicode msg) 14 | cdef void __prep_coroutine(io_uring ring, 15 | tuple coroutine, 16 | unsigned int coro_len, 17 | bint sub_coro=?) -------------------------------------------------------------------------------- /src/shakti/event/run.pyx: -------------------------------------------------------------------------------- 1 | from types import CoroutineType 2 | from .entry import JOBS 3 | 4 | 5 | def run(*coroutine: tuple, unsigned int entries=1024, unsigned int flags=0) -> list: 6 | ''' 7 | Type 8 | coroutine: CoroutineType 9 | entries: int 10 | flags: int 11 | return: None 12 | 13 | Example 14 | >>> from shakti import Timeit, run, sleep 15 | ... 16 | ... 17 | >>> async def main(): 18 | ... print('hi', end='') 19 | ... for i in range(4): 20 | ... if i: 21 | ... print('.', end='') 22 | ... await sleep(1) 23 | ... print('bye!') 24 | ... 25 | ... 26 | >>> if __name__ == '__main__': 27 | ... with Timeit(): 28 | ... run(main()) 29 | ''' 30 | cdef: 31 | unicode msg 32 | io_uring ring = io_uring() 33 | unsigned int coro_len = __checkup(entries, coroutine) 34 | 35 | io_uring_queue_init(entries, ring, flags) 36 | try: 37 | __prep_coroutine(ring, coroutine, coro_len) 38 | return __event_loop(ring, entries) 39 | finally: 40 | io_uring_queue_exit(ring) 41 | 42 | 43 | cdef unsigned int __checkup(unsigned int entries, tuple coroutine): 44 | cdef: 45 | unicode msg 46 | unsigned int i, max_entries = 32768, coro_len = len(coroutine) 47 | 48 | if entries < coro_len: 49 | __close_all_coroutine(coroutine, coro_len) 50 | msg = f'`run()` - `entries` is set too low! entries: {entries!r}' 51 | raise ValueError(msg) 52 | elif coro_len > max_entries: 53 | __close_all_coroutine(coroutine, coro_len) 54 | msg = f'`run()` - `entries` is set too high! max entries: {max_entries!r}' 55 | raise ValueError(msg) 56 | 57 | # pre-check 58 | msg = '`run()` only accepts `CoroutineType`, like `async` function.' 59 | __check_coroutine(coroutine, coro_len, msg) 60 | return coro_len 61 | 62 | 63 | cdef inline void __check_coroutine(tuple coroutine, unsigned int coro_len, unicode msg): 64 | for i in range(coro_len): 65 | if not isinstance(coroutine[i], CoroutineType): 66 | __close_all_coroutine(coroutine, coro_len) 67 | raise TypeError(msg) 68 | 69 | 70 | cdef inline void __close_all_coroutine(tuple coroutine, unsigned int coro_len) noexcept: 71 | cdef unsigned int i 72 | for i in range(coro_len): 73 | try: 74 | coroutine[i].close() 75 | except: 76 | pass # ignore error while trying to close 77 | 78 | 79 | cdef inline void __prep_coroutine(io_uring ring, 80 | tuple coroutine, 81 | unsigned int coro_len, 82 | bint sub_coro=False): 83 | cdef: 84 | SQE sqe 85 | unicode msg 86 | PyObject * ptr 87 | unsigned int i 88 | 89 | for i in range(coro_len): 90 | sqe = SQE() 91 | Py_XINCREF(ptr := sqe) 92 | sqe.job = CORO 93 | sqe.coro = coroutine[i] 94 | sqe.sub_coro = sub_coro 95 | io_uring_prep_nop(sqe) 96 | io_uring_sqe_set_data64(sqe, <__u64>ptr) 97 | io_uring_put_sqe(ring, sqe) 98 | 99 | 100 | cdef list __event_loop(io_uring ring, unsigned int entries): 101 | cdef: 102 | SQE sqe, _sqe 103 | bint sub_coro 104 | list r = [] 105 | __s32 res 106 | __u64 user_data 107 | object coro, value 108 | unicode msg 109 | PyObject* ptr 110 | io_uring_cqe cqe = io_uring_cqe() 111 | unsigned int index=0, counter=0, cq_ready=0 112 | 113 | # event manager 114 | while counter := ((io_uring_submit(ring) if io_uring_sq_ready(ring) else 0) + counter-cq_ready): 115 | if io_uring_wait_cqe(ring, cqe) != 0: 116 | continue 117 | cq_ready = 0 118 | for index in range(io_uring_for_each_cqe(ring, cqe)): 119 | res, user_data = cqe.get_index(index) 120 | if not user_data: 121 | continue 122 | cq_ready += 1 123 | if (ptr := user_data) is NULL: 124 | raise RuntimeError('`engine()` - received `NULL` from `user_data`') 125 | sqe = ptr 126 | sqe.result = res 127 | if sqe.job & CORO: 128 | Py_XDECREF(ptr) 129 | value = None # start coroutine 130 | else: 131 | value = False # bogus value 132 | if not (coro := sqe.coro): 133 | continue 134 | sub_coro = sqe.sub_coro 135 | while True: 136 | try: 137 | sqe = coro.send(value) 138 | except StopIteration as e: 139 | if not sqe.sub_coro: 140 | r.append(e.value) 141 | else: 142 | if sqe.job & ENTRY: 143 | sqe.coro = coro 144 | sqe.sub_coro = sub_coro 145 | elif sqe.job & ENTRIES: 146 | sqe.job = NOJOB # change first job 147 | _sqe = sqe[sqe.len-1] # last entry 148 | _sqe.coro = coro 149 | _sqe.sub_coro = sub_coro 150 | elif sqe.job & RING: 151 | value = ring 152 | continue 153 | else: 154 | msg = f'`run()` received unrecognized `job` {JOBS(sqe.job).name}' 155 | raise NotImplementedError(msg) 156 | 157 | if not io_uring_put_sqe(ring, sqe): 158 | counter += io_uring_submit(ring) 159 | if not io_uring_put_sqe(ring, sqe): # try again 160 | msg = '`run()` - length of `sqe > entries`' 161 | raise RuntimeError(msg) 162 | break 163 | if cq_ready: 164 | io_uring_cq_advance(ring, cq_ready) # free seen entries 165 | return r 166 | -------------------------------------------------------------------------------- /src/shakti/event/sleep.pyx: -------------------------------------------------------------------------------- 1 | from libc.errno cimport ETIME 2 | from liburing.error cimport trap_error 3 | from liburing.time cimport timespec, io_uring_prep_timeout 4 | from .entry cimport SQE 5 | 6 | 7 | async def sleep(double second, unsigned int flags=0): 8 | ''' 9 | Type 10 | second: int | float # double 11 | flags: int # unsigned 12 | return: None 13 | 14 | Flags 15 | IORING_TIMEOUT_BOOTTIME 16 | IORING_TIMEOUT_REALTIME 17 | 18 | Example 19 | >>> await sleep(1) # 1 second 20 | >>> await sleep(0.001) # 1 millisecond 21 | ''' 22 | if second < 0: 23 | raise ValueError('`sleep(second)` can not be `< 0`') 24 | 25 | cdef: 26 | SQE sqe = SQE(1, False) 27 | timespec ts = timespec(second) # prepare timeout 28 | io_uring_prep_timeout(sqe, ts, 0, flags) # note: `count=1` means no timer! 29 | await sqe 30 | # note: `ETIME` is returned as result for successfully timing-out. 31 | if sqe.result != -ETIME: 32 | trap_error(sqe.result) 33 | -------------------------------------------------------------------------------- /src/shakti/event/task.pyx: -------------------------------------------------------------------------------- 1 | from liburing.queue cimport io_uring, io_uring_prep_nop 2 | from .entry cimport RING, SQE 3 | from .run cimport __check_coroutine, __prep_coroutine 4 | from types import CoroutineType 5 | 6 | 7 | async def task(*coroutines: CoroutineType): 8 | ''' Task Coroutine coroutines 9 | 10 | Type 11 | coro: CoroutineType 12 | return: None 13 | 14 | Example 15 | >>> async def hi(pos, value): 16 | ... await sleep(value) 17 | ... print(f'No.{pos}: {value}', flush=True) 18 | 19 | # usage-1 20 | >>> await task(hi(1, 1)) 21 | No.1: 1 22 | 23 | # usage-2 24 | >>> await task(hi(1, 1), hi(2, 1), hi(3, 1)) 25 | No.2: 1 26 | No.3: 1 27 | No.1: 1 28 | 29 | # usage-3 30 | >>> while True: 31 | ... addr = await accept(server_fd) 32 | ... await task(handler(addr)) 33 | 34 | Note 35 | - Completion of `task(async_function, ...)` results will not be ordered. 36 | - Coroutine supplied into `task` executes concurrently on their own. 37 | ''' 38 | cdef: 39 | object coro 40 | unicode msg 41 | SQE sqe = SQE(0, error=False) 42 | unsigned int i=0, coro_len=len(coroutines) 43 | 44 | if coro_len < 1: 45 | msg = '`task(coroutines)` not provided!' 46 | raise ValueError(msg) 47 | 48 | msg = '`run()` only accepts `CoroutineType`, ' \ 49 | 'like `async def function():`. Refer to `help(run)`' 50 | __check_coroutine(coroutines, coro_len, msg) 51 | 52 | sqe.job = RING 53 | cdef io_uring ring = await sqe 54 | __prep_coroutine(ring, coroutines, coro_len, True) 55 | -------------------------------------------------------------------------------- /src/shakti/io/__init__.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSTEALTH/Shakti/a117dc862a673a0ea24a41d981eeb02ae21ba1ca/src/shakti/io/__init__.pxd -------------------------------------------------------------------------------- /src/shakti/io/common.pxd: -------------------------------------------------------------------------------- 1 | from liburing.common cimport io_uring_prep_close, io_uring_prep_close_direct 2 | from ..event.entry cimport SQE 3 | from ..core.base cimport AsyncBase 4 | from ..lib.error cimport UnsupportedOperation 5 | 6 | 7 | cdef class IOBase(AsyncBase): 8 | cdef: 9 | bint _reading, _writing 10 | readonly int fileno 11 | 12 | cdef inline void closed(self) 13 | cdef inline void reading(self) 14 | cdef inline void writing(self) 15 | -------------------------------------------------------------------------------- /src/shakti/io/common.pyx: -------------------------------------------------------------------------------- 1 | cdef class IOBase(AsyncBase): 2 | 3 | cdef inline void closed(self): 4 | if self.fileno < 0: 5 | self.msg = f'`{self.__class__.__name__}()` I/O operation on closed' 6 | raise UnsupportedOperation(self.msg) 7 | 8 | cdef inline void reading(self): 9 | if not self._reading: 10 | self.msg = f'`{self.__class__.__name__}()` is not opened in reading "r" mode.' 11 | raise UnsupportedOperation(self.msg) 12 | 13 | cdef inline void writing(self): 14 | if not self._writing: 15 | self.msg = f'`{self.__class__.__name__}()` is not opened in writing "w" mode.' 16 | raise UnsupportedOperation(self.msg) 17 | 18 | 19 | async def close(unsigned int fd, bint direct=False): 20 | ''' 21 | Example 22 | >>> await close(fd) 23 | 24 | >>> await close(index, True) # direct descriptor 25 | 26 | Note 27 | - Set `direct=True` to close direct descriptor & `fd` can be used to supply file index. 28 | ''' 29 | cdef SQE sqe = SQE() 30 | if direct: 31 | io_uring_prep_close_direct(sqe, fd) 32 | else: 33 | io_uring_prep_close(sqe, fd) 34 | await sqe 35 | -------------------------------------------------------------------------------- /src/shakti/io/file.pxd: -------------------------------------------------------------------------------- 1 | from liburing.lib.type cimport __s32, __u32, __u64 2 | from liburing.lib.file cimport * 3 | from liburing.common cimport AT_FDCWD, io_uring_prep_close, io_uring_prep_close_direct 4 | from liburing.statx cimport STATX_SIZE, statx, io_uring_prep_statx 5 | from liburing.file cimport open_how, io_uring_prep_openat, io_uring_prep_openat2, \ 6 | io_uring_prep_read, io_uring_prep_write 7 | from ..event.entry cimport SQE 8 | from ..lib.error cimport UnsupportedOperation 9 | from .common cimport IOBase 10 | 11 | 12 | cdef class File(IOBase): 13 | cdef: 14 | str _encoding 15 | int _dir_fd, _seek 16 | bint _bytes, _append, _creating, _direct 17 | __u64 _resolve, _flags, _mode 18 | readonly bytes path 19 | 20 | 21 | cpdef enum __file_define__: 22 | # note: copied from `liburing.file.pxd` 23 | RESOLVE_NO_XDEV = __RESOLVE_NO_XDEV 24 | RESOLVE_NO_MAGICLINKS = __RESOLVE_NO_MAGICLINKS 25 | RESOLVE_NO_SYMLINKS = __RESOLVE_NO_SYMLINKS 26 | RESOLVE_BENEATH = __RESOLVE_BENEATH 27 | RESOLVE_IN_ROOT = __RESOLVE_IN_ROOT 28 | RESOLVE_CACHED = __RESOLVE_CACHED 29 | 30 | SYNC_FILE_RANGE_WAIT_BEFORE = __SYNC_FILE_RANGE_WAIT_BEFORE 31 | SYNC_FILE_RANGE_WRITE = __SYNC_FILE_RANGE_WRITE 32 | SYNC_FILE_RANGE_WAIT_AFTER = __SYNC_FILE_RANGE_WAIT_AFTER 33 | 34 | O_ACCMODE = __O_ACCMODE 35 | O_RDONLY = __O_RDONLY 36 | O_WRONLY = __O_WRONLY 37 | O_RDWR = __O_RDWR 38 | 39 | O_APPEND = __O_APPEND 40 | O_ASYNC = __O_ASYNC 41 | O_CLOEXEC = __O_CLOEXEC 42 | O_CREAT = __O_CREAT 43 | 44 | O_DIRECT = __O_DIRECT 45 | O_DIRECTORY = __O_DIRECTORY 46 | O_DSYNC = __O_DSYNC 47 | O_EXCL = __O_EXCL 48 | O_LARGEFILE = __O_LARGEFILE 49 | O_NOATIME = __O_NOATIME 50 | O_NOCTTY = __O_NOCTTY 51 | O_NOFOLLOW = __O_NOFOLLOW 52 | O_NONBLOCK = __O_NONBLOCK 53 | O_PATH = __O_PATH 54 | 55 | O_SYNC = __O_SYNC 56 | O_TMPFILE = __O_TMPFILE 57 | O_TRUNC = __O_TRUNC 58 | -------------------------------------------------------------------------------- /src/shakti/io/file.pyx: -------------------------------------------------------------------------------- 1 | FILE_ACCEESS = {'x', 'a', 'r', 'w', 'b', '+', '!', 'T'} 2 | 3 | 4 | cdef class File(IOBase): 5 | 6 | def __init__(self, 7 | str path not None, 8 | str access not None='r', 9 | __u64 mode=0o660, 10 | *, 11 | __u64 resolve=0, 12 | dir_fd=AT_FDCWD, 13 | **kwargs): 14 | ''' Asynchronous "io_uring" File I/O - easy to use, highly optimized. 15 | 16 | Type 17 | path: str 18 | access: str 19 | mode: int 20 | resolve: int 21 | dir_fd: int 22 | kwargs: Dict[str, Union[str, int]] # extended features 23 | return: None 24 | 25 | Kwargs 26 | flags: int # `O_*` flags 27 | encoding: str 28 | 29 | Resolve 30 | RESOLVE_NO_XDEV 31 | RESOLVE_NO_MAGICLINKS 32 | RESOLVE_NO_SYMLINKS 33 | RESOLVE_BENEATH 34 | RESOLVE_IN_ROOT 35 | RESOLVE_CACHED 36 | 37 | Example 38 | # exclusive creation of file 39 | >>> await (await File('file.txt', 'x')).close() 40 | 41 | # ignore already created file if exists 42 | >>> await (await File('file.txt', '!x')).close() 43 | 44 | # set permissions 45 | >>> await (await File('file.txt', 'x', 0o777)).close() 46 | 47 | # writing - also truncates 48 | >>> async with File('file.txt', 'w') as file: 49 | ... await file.write('hello ') 50 | 51 | # append 52 | >>> async with File('file.txt', 'a') as file: 53 | ... await file.write('world!') 54 | 55 | # reading (default) 56 | >>> async with File('file.txt') as file: 57 | ... await file.read() 58 | 'hello world!' 59 | 60 | # temporary file - deleted after use 61 | >>> async with File('.', 'T') as file: 62 | ... await file.write(b'hi') 63 | 64 | # Other 65 | >>> async with File('path/file.exe') as file: 66 | ... file.fileno 67 | 123 68 | ... bool(file) 69 | True 70 | ... file.path 71 | b'path/file.exe' 72 | 73 | Note 74 | - Allows reading and writing in str & bytes like types. 75 | - `access` parameter supports: 76 | "r" Open the file for reading; the file must already exist. (default) 77 | "r+" Open the file for both reading and writing; the file must already exist. 78 | "w" Open the file for writing only. Truncates file. 79 | "w+" Open the file for reading and writing only. Truncates file. 80 | "x" Open for exclusive creation of new file and failing if file already exists 81 | "!x" Open for exclusive creation of new file and ignored if file already exists 82 | "a" Open for writing, appending to the end of the file 83 | 'b' Open in bytes mode, default is text mode 84 | "T" Temporary regular file. Will be deleted on close. If not present "w" will 85 | be auto set as temporary file can not be opened without it. Should use 86 | `linkat` to save as proper file 87 | "!T" Prevents a temporary file from being linked into the filesystem 88 | - Mode can be combined e.g. `access='rw'` for reading and writing. 89 | - Internally maintains offset + read/write last seek position. 90 | - `AT_FDCWD` uses current working directory, if `path` is relative path. 91 | ''' 92 | cdef set[str] found 93 | if found := set(access) - FILE_ACCEESS: 94 | self.msg = f'`{self.__class__.__name__}(access)` - ' 95 | self.msg += f'{"".join(found)!r} is not supported, only {FILE_ACCEESS!r}' 96 | raise ValueError(self.msg) 97 | 98 | self._encoding = kwargs.get('encoding', 'utf8') 99 | self._resolve = resolve 100 | self._dir_fd = dir_fd 101 | self.fileno = -1 102 | self._flags = kwargs.get('flags', 0) 103 | self._mode = mode 104 | self._seek = 0 105 | self.path = path.encode() 106 | 107 | # True/False 108 | self._bytes = 'b' in access 109 | self._append = 'a' in access 110 | self._creating = 'x' in access 111 | if '+' in access: 112 | self._writing = self._reading = True 113 | else: 114 | self._reading = 'r' in access # or 'b' in access 115 | self._writing = 'w' in access 116 | 117 | if self._append and self._writing: 118 | self.msg = f'`{self.__class__.__name__}()` - must have exactly one of write/append mode' 119 | raise ValueError(self.msg) 120 | 121 | # exclusive create & write 122 | if 'x' in access: 123 | if '!' not in access: 124 | self._flags |= O_EXCL 125 | self._flags |= O_CREAT 126 | 127 | # temp file 128 | if 'T' in access: 129 | if self._creating: 130 | self.msg = f'`{self.__class__.__name__}()` - ' 131 | self.msg += 'can not mix create new file and create temporary file!' 132 | raise ValueError(self.msg) 133 | if '!' not in access: 134 | self._flags |= O_EXCL 135 | if not self._writing: 136 | self._writing = True 137 | self._creating = True 138 | self._flags |= O_TMPFILE 139 | 140 | # truncate to zero 141 | if 'w' in access: 142 | self._flags |= O_TRUNC 143 | 144 | # read & write 145 | if self._reading and self._writing: 146 | self._flags |= O_RDWR 147 | elif self._writing: 148 | self._flags |= O_WRONLY 149 | elif not self._creating: 150 | self._flags |= O_RDONLY 151 | 152 | # append 153 | if self._append: 154 | if self._writing: 155 | self.msg = f'`{self.__class__.__name__}()` - can not mix write and append!' 156 | raise ValueError(self.msg) 157 | self._writing = True 158 | self._flags |= O_APPEND 159 | 160 | if not (O_CREAT | O_TMPFILE) & self._flags: 161 | self._mode = 0 162 | 163 | if self._flags & O_NONBLOCK: 164 | self.msg = f'`{self.__class__.__name__}()` - ' 165 | self.msg += 'does not support `*_NONBLOCK` as its already asynchronous!' 166 | raise ValueError(self.msg) 167 | 168 | # midsync 169 | def __ainit__(self): 170 | return self.open() 171 | 172 | # midsync 173 | def __aenter__(self): 174 | return super().__aenter__() 175 | 176 | # midsync 177 | def __aexit__(self, *errors): 178 | ''' 179 | Type 180 | errors: Tuple[any] 181 | return: coroutine 182 | ''' 183 | if any(errors): 184 | self.fileno = -1 185 | return super().__aexit__(*errors) 186 | # note: if errors happens async is blocked! thus needs to use normal close. 187 | else: 188 | return self.close() 189 | 190 | def __bool__(self): 191 | return self.fileno > -1 192 | 193 | async def open(self): 194 | ''' 195 | Example 196 | # automatically open & close file 197 | >>> async with File(...): 198 | ... ... 199 | 200 | # or manually open & close file 201 | >>> file = File(...) 202 | >>> await file.open() 203 | >>> await file.close() 204 | 205 | # or automatically open file but have to close manually 206 | >>> file = await File(...) 207 | >>> await file.close() 208 | 209 | Note 210 | - `File.open` combines features of `openat` & `openat2` as its own. 211 | ''' 212 | cdef: 213 | SQE sqe = SQE() 214 | open_how how 215 | 216 | if self.fileno > -1: 217 | self.msg = f'`{self.__class__.__name__}` is already open!' 218 | raise UnsupportedOperation(self.msg) 219 | 220 | if self._resolve: 221 | how = open_how(self._flags, self._mode, self._resolve) 222 | try: 223 | io_uring_prep_openat2(sqe, self.path, how, self._dir_fd) 224 | await sqe 225 | except BlockingIOError: 226 | if how.resolve & RESOLVE_CACHED: 227 | how.resolve &= ~RESOLVE_CACHED 228 | # note: must retry without `RESOLVE_CACHED` since file path 229 | # was not in kernel's lookup cache. 230 | io_uring_prep_openat2(sqe, self.path, how, self._dir_fd) 231 | await sqe 232 | else: 233 | io_uring_prep_openat(sqe, self.path, self._flags, self._mode, self._dir_fd) 234 | # note: `EWOULDBLOCK` would be raised if `O_NONBLOCK` `flags` was set. 235 | # Since this class does not allow `O_NONBLOCK` flag to be set 236 | # there is no point accounting for that error. 237 | await sqe 238 | self.fileno = sqe.result 239 | 240 | async def close(self): 241 | self.closed() 242 | 243 | cdef SQE sqe = SQE() 244 | 245 | if self._direct: 246 | io_uring_prep_close_direct(sqe, self.fileno) 247 | else: 248 | io_uring_prep_close(sqe, self.fileno) 249 | await sqe 250 | self.fileno = -1 251 | 252 | async def read(self, object length=None, object offset=None)-> str | bytes: 253 | ''' 254 | Type 255 | length: Optional[int] 256 | offset: Optional[int] 257 | return: str | bytes 258 | 259 | Example 260 | # file content: b'hello world' 261 | 262 | >>> await file.read(5) 263 | b'hello' 264 | 265 | >>> await file.read(6) 266 | b' world' 267 | 268 | >>> await file.read(5, 3) 269 | b'lo wo' 270 | 271 | >>> await file.read() 272 | b'rld' 273 | 274 | >>> await file.read(offset=0) 275 | b'hello world' 276 | 277 | >>> async with File('path/file') as file: 278 | >>> await file.read() 279 | b'hello world' 280 | 281 | Note 282 | - if `offset` is not set last read/write seek position is used 283 | ''' 284 | self.closed() 285 | self.reading() 286 | 287 | cdef: 288 | SQE sqe = SQE() 289 | statx stat 290 | __u64 _length, _offset = self._seek if offset is None else offset 291 | 292 | if length is None: 293 | stat = statx() 294 | io_uring_prep_statx(sqe, stat, self.path, 0, STATX_SIZE, self._dir_fd) 295 | await sqe 296 | _length = stat.stx_size 297 | else: 298 | _length = length 299 | 300 | cdef bytearray buffer = bytearray(_length) 301 | 302 | io_uring_prep_read(sqe, self.fileno, buffer, _length, _offset) 303 | await sqe 304 | cdef unsigned int result = sqe.result 305 | 306 | self._seek = _offset + result 307 | if self._bytes: 308 | return bytes(buffer if _length == result else buffer[:result]) 309 | else: 310 | return (buffer if _length == result else buffer[:result]).decode(self._encoding) 311 | 312 | async def write(self, object data, object offset=None)-> __u32: 313 | ''' 314 | Type 315 | data: Union[str, bytes, bytearray, memoryview] 316 | offset: int 317 | return: int 318 | 319 | Example 320 | >>> async with File('path/file', 'w') as file: 321 | ... await file.write('hi... bye!') 322 | 10 323 | 324 | >>> async with File('path/file', 'wb') as file: 325 | ... await file.write(b'hi... bye!') 326 | 10 327 | 328 | Note 329 | - if `offset` is not set last read/write seek position is used 330 | ''' 331 | self.closed() 332 | self.writing() 333 | 334 | if self._append and offset is not None: 335 | self.msg = f'`{self.__class__.__name__}.write()` ' 336 | self.msg += '- `offset` can not be used while file is opened for append!' 337 | raise UnsupportedOperation(self.msg) 338 | 339 | if not data: 340 | return 0 341 | 342 | cdef: 343 | SQE sqe = SQE() 344 | __u64 _offset = self._seek if offset is None else offset 345 | 346 | if not self._bytes: 347 | data = data.encode(self._encoding) 348 | 349 | io_uring_prep_write(sqe, self.fileno, data, len(data), _offset) 350 | await sqe 351 | 352 | cdef __u32 result = sqe.result 353 | self._seek = _offset + result 354 | return result 355 | 356 | 357 | async def open(str path not None, __u64 flags=0, *, 358 | __u64 mode=0o660, __u64 resolve=0, int dir_fd=AT_FDCWD)-> int: 359 | ''' 360 | Type 361 | path: str 362 | flags: int 363 | mode: int 364 | resolve: int 365 | dir_fd: int 366 | return: int 367 | 368 | Flags 369 | O_CREAT 370 | O_RDWR 371 | O_RDONLY 372 | O_WRONLY 373 | O_TMPFILE 374 | ... 375 | 376 | Mode 377 | TODO 378 | 379 | Resolve 380 | RESOLVE_BENEATH 381 | RESOLVE_IN_ROOT 382 | RESOLVE_NO_MAGICLINKS 383 | RESOLVE_NO_SYMLINKS 384 | RESOLVE_NO_XDEV 385 | RESOLVE_CACHED 386 | 387 | Example 388 | >>> fd = await open('/path/file.ext') # read only 389 | >>> fd = await open('/path/file.ext', O_CREAT | O_WRONLY | O_APPEND) 390 | >>> fd = await open('/path/file.ext', O_RDWR, resolve=RESOLVE_CACHED) 391 | 392 | Note 393 | - `flags=0` is same as `O_RDONLY` 394 | - `mode` is only applied when using `flags` `O_CREAT` or `O_TMPFILE` 395 | ''' 396 | cdef: 397 | _path = path.encode() 398 | SQE sqe = SQE() 399 | open_how how = open_how() 400 | 401 | if (flags & (O_CREAT | O_TMPFILE)): 402 | how.ptr.mode = mode 403 | how.ptr.flags = flags 404 | how.ptr.resolve = resolve 405 | 406 | io_uring_prep_openat2(sqe, _path, how, dir_fd) 407 | await sqe 408 | return sqe.result # fd 409 | 410 | 411 | async def read(int fd, __s32 length, __u64 offset=0)-> bytes: 412 | ''' 413 | Example 414 | >>> await read(fd, length) 415 | b'hi...bye!' 416 | ''' 417 | if not length: return b'' 418 | cdef: 419 | SQE sqe = SQE() 420 | bytearray buffer = bytearray(length) 421 | io_uring_prep_read(sqe, fd, buffer, length, offset) 422 | await sqe 423 | return bytes(buffer if length == sqe.result else buffer[:sqe.result]) 424 | 425 | 426 | async def write(int fd, const unsigned char[:] buffer, __u64 offset=0)-> __u32: 427 | ''' 428 | Type 429 | fd: int 430 | buffer: bytes | bytearray | memoryview 431 | offset: int 432 | return: int 433 | 434 | Example 435 | >>> await write(fd, b'hi...bye!') 436 | 9 437 | ''' 438 | cdef __u32 length = len(buffer) 439 | 440 | if length == 0: 441 | return length 442 | 443 | cdef SQE sqe = SQE() 444 | io_uring_prep_write(sqe, fd, buffer, length, offset) 445 | await sqe 446 | 447 | return (length := sqe.result) 448 | -------------------------------------------------------------------------------- /src/shakti/io/socket.pxd: -------------------------------------------------------------------------------- 1 | from libc.errno cimport ENFILE 2 | from cpython.array cimport array 3 | from liburing.lib.socket cimport * 4 | from liburing.lib.type cimport bool as bool_t 5 | from liburing.socket cimport sockaddr, io_uring_prep_socket, io_uring_prep_socket_direct_alloc, \ 6 | io_uring_prep_shutdown, io_uring_prep_send, io_uring_prep_recv, \ 7 | io_uring_prep_accept, io_uring_prep_connect, \ 8 | io_uring_prep_setsockopt, io_uring_prep_getsockopt 9 | from liburing.socket_extra cimport io_uring_prep_bind, io_uring_prep_listen, getsockname as _getsockname, \ 10 | getpeername as _getpeername, getaddrinfo as _getaddrinfo, isIP 11 | from liburing.time cimport timespec, io_uring_prep_link_timeout 12 | from liburing.error cimport raise_error 13 | from ..event.entry cimport SQE 14 | 15 | 16 | # defines 17 | cpdef enum SocketFamily: 18 | AF_UNIX = __AF_UNIX 19 | AF_INET = __AF_INET 20 | AF_INET6 = __AF_INET6 21 | 22 | cpdef enum SocketType: 23 | SOCK_STREAM = __SOCK_STREAM 24 | SOCK_DGRAM = __SOCK_DGRAM 25 | SOCK_RAW = __SOCK_RAW 26 | SOCK_RDM = __SOCK_RDM 27 | SOCK_SEQPACKET = __SOCK_SEQPACKET 28 | SOCK_DCCP = __SOCK_DCCP 29 | SOCK_PACKET = __SOCK_PACKET 30 | SOCK_CLOEXEC = __SOCK_CLOEXEC 31 | SOCK_NONBLOCK = __SOCK_NONBLOCK 32 | 33 | cpdef enum ShutdownHow: 34 | SHUT_RD = __SHUT_RD 35 | SHUT_WR = __SHUT_WR 36 | SHUT_RDWR = __SHUT_RDWR 37 | 38 | cpdef enum SocketProto: 39 | IPPROTO_IP = __IPPROTO_IP 40 | IPPROTO_ICMP = __IPPROTO_ICMP 41 | IPPROTO_IGMP = __IPPROTO_IGMP 42 | IPPROTO_IPIP = __IPPROTO_IPIP 43 | IPPROTO_TCP = __IPPROTO_TCP 44 | IPPROTO_EGP = __IPPROTO_EGP 45 | IPPROTO_PUP = __IPPROTO_PUP 46 | IPPROTO_UDP = __IPPROTO_UDP 47 | IPPROTO_IDP = __IPPROTO_IDP 48 | IPPROTO_TP = __IPPROTO_TP 49 | IPPROTO_DCCP = __IPPROTO_DCCP 50 | IPPROTO_IPV6 = __IPPROTO_IPV6 51 | IPPROTO_RSVP = __IPPROTO_RSVP 52 | IPPROTO_GRE = __IPPROTO_GRE 53 | IPPROTO_ESP = __IPPROTO_ESP 54 | IPPROTO_AH = __IPPROTO_AH 55 | IPPROTO_MTP = __IPPROTO_MTP 56 | IPPROTO_BEETPH = __IPPROTO_BEETPH 57 | IPPROTO_ENCAP = __IPPROTO_ENCAP 58 | IPPROTO_PIM = __IPPROTO_PIM 59 | IPPROTO_COMP = __IPPROTO_COMP 60 | # note: not supported 61 | # IPPROTO_L2TP = __IPPROTO_L2TP 62 | IPPROTO_SCTP = __IPPROTO_SCTP 63 | IPPROTO_UDPLITE = __IPPROTO_UDPLITE 64 | IPPROTO_MPLS = __IPPROTO_MPLS 65 | IPPROTO_ETHERNET = __IPPROTO_ETHERNET 66 | IPPROTO_RAW = __IPPROTO_RAW 67 | IPPROTO_MPTCP = __IPPROTO_MPTCP 68 | 69 | # setsockopt & getsockopt start >>> 70 | cpdef enum __socket_define__: 71 | SOL_SOCKET = __SOL_SOCKET 72 | SO_DEBUG = __SO_DEBUG 73 | SO_REUSEADDR = __SO_REUSEADDR 74 | SO_TYPE = __SO_TYPE 75 | SO_ERROR = __SO_ERROR 76 | SO_DONTROUTE = __SO_DONTROUTE 77 | SO_BROADCAST = __SO_BROADCAST 78 | SO_SNDBUF = __SO_SNDBUF 79 | SO_RCVBUF = __SO_RCVBUF 80 | SO_SNDBUFFORCE = __SO_SNDBUFFORCE 81 | SO_RCVBUFFORCE = __SO_RCVBUFFORCE 82 | SO_KEEPALIVE = __SO_KEEPALIVE 83 | SO_OOBINLINE = __SO_OOBINLINE 84 | SO_NO_CHECK = __SO_NO_CHECK 85 | SO_PRIORITY = __SO_PRIORITY 86 | SO_LINGER = __SO_LINGER 87 | SO_BSDCOMPAT = __SO_BSDCOMPAT 88 | SO_REUSEPORT = __SO_REUSEPORT 89 | SO_PASSCRED = __SO_PASSCRED 90 | SO_PEERCRED = __SO_PEERCRED 91 | SO_RCVLOWAT = __SO_RCVLOWAT 92 | SO_SNDLOWAT = __SO_SNDLOWAT 93 | SO_BINDTODEVICE = __SO_BINDTODEVICE 94 | 95 | # Socket filtering 96 | SO_ATTACH_FILTER = __SO_ATTACH_FILTER 97 | SO_DETACH_FILTER = __SO_DETACH_FILTER 98 | SO_GET_FILTER = __SO_GET_FILTER 99 | SO_PEERNAME = __SO_PEERNAME 100 | SO_ACCEPTCONN = __SO_ACCEPTCONN 101 | SO_PEERSEC = __SO_PEERSEC 102 | SO_PASSSEC = __SO_PASSSEC 103 | SO_MARK = __SO_MARK 104 | SO_PROTOCOL = __SO_PROTOCOL 105 | SO_DOMAIN = __SO_DOMAIN 106 | SO_RXQ_OVFL = __SO_RXQ_OVFL 107 | SO_WIFI_STATUS = __SO_WIFI_STATUS 108 | SCM_WIFI_STATUS = __SCM_WIFI_STATUS 109 | SO_PEEK_OFF = __SO_PEEK_OFF 110 | 111 | # not tested 112 | SO_TIMESTAMP = __SO_TIMESTAMP 113 | SO_TIMESTAMPNS = __SO_TIMESTAMPNS 114 | SO_TIMESTAMPING = __SO_TIMESTAMPING 115 | SO_RCVTIMEO = __SO_RCVTIMEO 116 | SO_SNDTIMEO = __SO_SNDTIMEO 117 | # setsockopt & getsockopt end <<< 118 | -------------------------------------------------------------------------------- /src/shakti/io/socket.pyx: -------------------------------------------------------------------------------- 1 | async def socket(int family=__AF_INET, int type=__SOCK_STREAM, int protocol=0, unsigned int flags=0, 2 | *, bint direct=False)-> int: 3 | ''' Create Socket 4 | 5 | Example 6 | >>> sock_fd = await socket() # default: AF_INET, SOCK_STREAM 7 | ... ... 8 | 9 | Note 10 | - Setting `direct=True` will return direct descriptor index. 11 | ''' 12 | cdef SQE sqe = SQE(error=False) 13 | if direct: 14 | io_uring_prep_socket_direct_alloc(sqe, family, type, protocol, flags) 15 | else: 16 | io_uring_prep_socket(sqe, family, type, protocol, flags) 17 | await sqe 18 | if sqe.result > -1: 19 | return sqe.result # `fd` or `index` 20 | else: 21 | if direct and sqe.result == -ENFILE: 22 | raise_error(sqe.result, 'Either file table is full or register file not enabled!') 23 | raise_error(sqe.result) 24 | 25 | 26 | async def connect(int sockfd, str host, in_port_t port=80): 27 | ''' 28 | Example 29 | >>> sockfd = await socket() 30 | >>> await connect(sockfd, 'domain.ext') 31 | # or 32 | >>> await connect(sockfd, '0.0.0.0', 12345) 33 | # or 34 | >>> await connect(sockfd, '/path') 35 | ... 36 | >>> await close(sockfd) 37 | ''' 38 | cdef: # get family 39 | SQE sqe = SQE() 40 | bytes _host = host.encode() 41 | sockaddr addr 42 | socklen_t size = sizeof(__sockaddr_storage) 43 | __sockaddr sa 44 | 45 | __getsockname(sockfd, &sa, &size) 46 | 47 | if sa.sa_family == __AF_UNIX: 48 | addr = sockaddr(sa.sa_family, _host, port) 49 | io_uring_prep_connect(sqe, sockfd, addr) 50 | await sqe 51 | elif sa.sa_family in (__AF_INET, __AF_INET6): 52 | if isIP(sa.sa_family, _host): 53 | addr = sockaddr(sa.sa_family, _host, port) 54 | io_uring_prep_connect(sqe, sockfd, addr) 55 | await sqe 56 | else: 57 | for af_, sock_, proto, canon, addr in _getaddrinfo(_host, str(port).encode()): 58 | try: 59 | io_uring_prep_connect(sqe, sockfd, addr) 60 | await sqe 61 | except OSError: 62 | continue 63 | else: 64 | break 65 | else: 66 | raise NotImplementedError 67 | 68 | 69 | async def accept(int sockfd, int flags=0)-> int: 70 | ''' 71 | Example 72 | >>> client_fd = await accept(socket_fd) 73 | ''' 74 | cdef SQE sqe = SQE() 75 | io_uring_prep_accept(sqe, sockfd, None, flags) 76 | await sqe 77 | return sqe.result 78 | 79 | 80 | async def recv(int sockfd, unsigned int bufsize, int flags=0): 81 | ''' 82 | Example 83 | >>> await recv(client_fd, 13) 84 | b'received data' 85 | ''' 86 | cdef: 87 | SQE sqe = SQE() 88 | memoryview buf = memoryview(bytearray(bufsize)) 89 | io_uring_prep_recv(sqe, sockfd, buf, bufsize, flags) 90 | await sqe 91 | cdef unsigned int result = sqe.result 92 | return bytes(buf[:result] if result != bufsize else buf) 93 | 94 | 95 | async def send(int sockfd, const unsigned char[:] buf, int flags=0): 96 | ''' 97 | Example 98 | >>> await send(client_fd, b'send data') 99 | 10 100 | ''' 101 | cdef: 102 | SQE sqe = SQE() 103 | size_t length = len(buf) 104 | io_uring_prep_send(sqe, sockfd, buf, length, flags) 105 | await sqe 106 | return sqe.result 107 | 108 | 109 | async def sendall(int sockfd, const unsigned char[:] buf, int flags=0): 110 | ''' 111 | Example 112 | >>> await sendall(client_fd, b'send data') 113 | 10 114 | ''' 115 | cdef: 116 | SQE sqe = SQE() 117 | size_t length = len(buf) 118 | unsigned int total = 0 119 | 120 | while True: 121 | io_uring_prep_send(sqe, sockfd, buf[total:], length-total, flags) 122 | await sqe 123 | if (total := total + sqe.result) == length: 124 | break 125 | 126 | 127 | async def shutdown(int sockfd, int how=__SHUT_RDWR): 128 | ''' 129 | How 130 | SHUT_RD 131 | SHUT_WR 132 | SHUT_RDWR # (default) 133 | ''' 134 | cdef SQE sqe = SQE() 135 | io_uring_prep_shutdown(sqe, sockfd, how) 136 | await sqe 137 | 138 | 139 | async def bind(int sockfd, str host, in_port_t port)-> object: 140 | ''' 141 | Example 142 | >>> sockfd = await socket() 143 | 144 | >>> addr = await bind(sock_fd, '0.0.0.0', 12345) 145 | >>> await getsockname(sockfd, addr) 146 | '0.0.0.0', 12345 147 | 148 | # or 149 | 150 | >>> addr = await bind(sock_fd, '0.0.0.0', 0) # random port 151 | >>> await getsockname(sockfd, addr) 152 | '0.0.0.0', 6744 # random port 153 | 154 | >>> await close(sockfd) 155 | ''' 156 | cdef: # get family 157 | __sockaddr sa 158 | socklen_t size = sizeof(__sockaddr_storage) 159 | 160 | __getsockname(sockfd, &sa, &size) 161 | 162 | cdef: 163 | sockaddr addr = sockaddr(sa.sa_family, host.encode(), port) 164 | SQE sqe = SQE() 165 | io_uring_prep_bind(sqe, sockfd, addr) 166 | await sqe 167 | return addr 168 | 169 | 170 | async def listen(int sockfd, int backlog)-> int: 171 | cdef: 172 | SQE sqe = SQE() 173 | io_uring_prep_listen(sqe, sockfd, backlog) 174 | await sqe 175 | return sqe.result 176 | 177 | 178 | async def getsockname(int sockfd, sockaddr addr)-> tuple[str, int]: 179 | cdef: 180 | bytes ip 181 | int port 182 | ip, port = _getsockname(sockfd, addr) 183 | return ip.decode(), port 184 | 185 | 186 | async def setsockopt(int sockfd, int level, int optname, object optval): 187 | ''' 188 | Example 189 | >>> await setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, 1) 190 | 191 | Warning 192 | - This function is still flawed, needs more testing. 193 | ''' 194 | cdef: 195 | array val 196 | str t = type(optval).__name__ 197 | # note: have to use `str` to check as `bool` type does not work well! 198 | 199 | if t in ('int', 'bool'): 200 | val = array('i', [optval]) 201 | elif t == 'str': 202 | val = array('B', [optval.encode()]) 203 | elif t == 'bytes': 204 | val = array('B', [optval]) 205 | else: 206 | raise TypeError(f'`setsockopt` received `optval` type {t!r}, not supported') 207 | cdef SQE sqe = SQE() 208 | io_uring_prep_setsockopt(sqe, sockfd, level, optname, val) 209 | await sqe 210 | 211 | 212 | async def getsockopt(int sockfd, int level, int optname)-> int: 213 | ''' 214 | Example 215 | >>> await getsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR) 216 | 1 217 | 218 | Warning 219 | - This function is still flawed, needs more testing. 220 | ''' 221 | cdef: 222 | SQE sqe = SQE() 223 | array optval = array('i', [0]) 224 | io_uring_prep_getsockopt(sqe, sockfd, level, optname, optval) 225 | await sqe 226 | return optval[0] 227 | -------------------------------------------------------------------------------- /src/shakti/io/statx.pxd: -------------------------------------------------------------------------------- 1 | from libc.errno cimport ENOENT 2 | from liburing.lib.type cimport __AT_FDCWD 3 | from liburing.lib.statx cimport * 4 | from liburing.statx cimport statx, io_uring_prep_statx 5 | from ..event.entry cimport SQE 6 | 7 | 8 | cdef class Statx(statx): 9 | cdef: 10 | bint __awaited__ 11 | bytes _path 12 | unsigned int _mask 13 | int _flags 14 | int _dir_fd 15 | 16 | 17 | cpdef enum __statx_define__: 18 | # note: copied directly from `liburing.statx.pxd` 19 | # defines 20 | STATX_TYPE = __STATX_TYPE 21 | STATX_MODE = __STATX_MODE 22 | STATX_NLINK = __STATX_NLINK 23 | STATX_UID = __STATX_UID 24 | STATX_GID = __STATX_GID 25 | STATX_ATIME = __STATX_ATIME 26 | STATX_MTIME = __STATX_MTIME 27 | STATX_CTIME = __STATX_CTIME 28 | STATX_INO = __STATX_INO 29 | STATX_SIZE = __STATX_SIZE 30 | STATX_BLOCKS = __STATX_BLOCKS 31 | STATX_BASIC_STATS = __STATX_BASIC_STATS 32 | STATX_BTIME = __STATX_BTIME 33 | STATX_MNT_ID = __STATX_MNT_ID 34 | # note: not supported 35 | # STATX_DIOALIGN = __STATX_DIOALIGN 36 | 37 | STATX_ATTR_COMPRESSED = __STATX_ATTR_COMPRESSED 38 | STATX_ATTR_IMMUTABLE = __STATX_ATTR_IMMUTABLE 39 | STATX_ATTR_APPEND = __STATX_ATTR_APPEND 40 | STATX_ATTR_NODUMP = __STATX_ATTR_NODUMP 41 | STATX_ATTR_ENCRYPTED = __STATX_ATTR_ENCRYPTED 42 | STATX_ATTR_AUTOMOUNT = __STATX_ATTR_AUTOMOUNT 43 | STATX_ATTR_MOUNT_ROOT = __STATX_ATTR_MOUNT_ROOT 44 | STATX_ATTR_VERITY = __STATX_ATTR_VERITY 45 | STATX_ATTR_DAX = __STATX_ATTR_DAX 46 | 47 | S_IFMT = __S_IFMT 48 | 49 | S_IFSOCK = __S_IFSOCK 50 | S_IFLNK = __S_IFLNK 51 | S_IFREG = __S_IFREG 52 | S_IFBLK = __S_IFBLK 53 | S_IFDIR = __S_IFDIR 54 | S_IFCHR = __S_IFCHR 55 | S_IFIFO = __S_IFIFO 56 | 57 | S_ISUID = __S_ISUID 58 | S_ISGID = __S_ISGID 59 | S_ISVTX = __S_ISVTX 60 | 61 | S_IRWXU = __S_IRWXU 62 | S_IRUSR = __S_IRUSR 63 | S_IWUSR = __S_IWUSR 64 | S_IXUSR = __S_IXUSR 65 | 66 | S_IRWXG = __S_IRWXG 67 | S_IRGRP = __S_IRGRP 68 | S_IWGRP = __S_IWGRP 69 | S_IXGRP = __S_IXGRP 70 | 71 | S_IRWXO = __S_IRWXO 72 | S_IROTH = __S_IROTH 73 | S_IWOTH = __S_IWOTH 74 | S_IXOTH = __S_IXOTH 75 | 76 | # AT_STATX_SYNC_TYPE = __AT_STATX_SYNC_TYPE # skipping: not documented 77 | AT_STATX_SYNC_AS_STAT = __AT_STATX_SYNC_AS_STAT 78 | AT_STATX_FORCE_SYNC = __AT_STATX_FORCE_SYNC 79 | AT_STATX_DONT_SYNC = __AT_STATX_DONT_SYNC 80 | -------------------------------------------------------------------------------- /src/shakti/io/statx.pyx: -------------------------------------------------------------------------------- 1 | cdef class Statx(statx): 2 | ''' 3 | Type 4 | path: str 5 | flags: int 6 | mask: int 7 | dir_fd: int 8 | return: None 9 | 10 | Example 11 | >>> stat = await Statx('/path/to/some_file.ext') 12 | >>> stat.isfile 13 | True 14 | >>> stat.stx_size 15 | 123 16 | 17 | # with context manager 18 | >>> async with Statx('/path/to/some_file.ext') as stat: 19 | ... stat.isfile 20 | True 21 | ... stat.stx_size 22 | 123 23 | 24 | Flags 25 | - TODO 26 | 27 | Mask 28 | - TODO 29 | 30 | Note 31 | - Refer to `help(Statx)` to see further details. 32 | ''' 33 | def __cinit__(self, 34 | str path not None, 35 | int flags=0, 36 | unsigned int mask=0, 37 | int dir_fd=__AT_FDCWD): 38 | self._path = path.encode() 39 | self._mask = mask 40 | self._flags = flags 41 | self._dir_fd = dir_fd 42 | 43 | # midsync 44 | def __await__(self): 45 | return self.__aenter__().__await__() 46 | 47 | async def __aenter__(self): 48 | if self.__awaited__: 49 | return self 50 | self.__awaited__ = True 51 | cdef SQE sqe = SQE() 52 | io_uring_prep_statx(sqe, self, self._path, self._flags, self._mask, self._dir_fd) 53 | await sqe 54 | return self 55 | 56 | async def __aexit__(self, *errors): 57 | if any(errors): 58 | return False 59 | 60 | 61 | async def exists(str path not None)-> bool: 62 | ''' Check Path Exists 63 | 64 | Type 65 | path: str 66 | return: bool 67 | 68 | Example 69 | >>> if await exists('some/path'): 70 | ... # do stuff 71 | ''' 72 | cdef: 73 | bytes _path = path.encode() 74 | SQE sqe = SQE(1, False) 75 | io_uring_prep_statx(sqe, None, _path) 76 | await sqe 77 | return sqe.result != -ENOENT # FileNotFoundError 78 | -------------------------------------------------------------------------------- /src/shakti/lib/__init__.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSTEALTH/Shakti/a117dc862a673a0ea24a41d981eeb02ae21ba1ca/src/shakti/lib/__init__.pxd -------------------------------------------------------------------------------- /src/shakti/lib/error.pxd: -------------------------------------------------------------------------------- 1 | cdef class CancelledError(Exception): 2 | pass # __module__ = BaseException.__module__ 3 | # TODO: to be used in "io.event.run" 4 | 5 | 6 | cdef class UnsupportedOperation(Exception): 7 | pass # __module__ = OSError.__module__ 8 | 9 | 10 | cdef class ConnectionNotEstablishedError(Exception): 11 | pass # __module__ = ConnectionError.__module__ 12 | 13 | 14 | cdef class DirExistsError(Exception): 15 | pass # __module__ = FileExistsError.__module__ 16 | -------------------------------------------------------------------------------- /src/shakti/lib/error.pyx: -------------------------------------------------------------------------------- 1 | # Empty place holder to load `.pxd` 2 | -------------------------------------------------------------------------------- /src/shakti/lib/path.pxd: -------------------------------------------------------------------------------- 1 | # 2 | 3 | cpdef str join_string(str path, str other) 4 | cpdef bytes join_bytes(bytes path, bytes other) 5 | cpdef bint isabs(object path, bint error=?) except -1 6 | cdef bint isabs_string(str path) noexcept 7 | cdef bint isabs_bytes(bytes path) noexcept 8 | -------------------------------------------------------------------------------- /src/shakti/lib/path.pyx: -------------------------------------------------------------------------------- 1 | # 2 | 3 | def join(object path not None, *other): 4 | ''' Join Multiple Paths 5 | 6 | Type 7 | path: str | bytes 8 | *other: tuple[str | bytes] 9 | return: str | bytes 10 | 11 | Example 12 | >>> join('/one', 'two', 'three') 13 | '/one/two/three' 14 | 15 | >>> join(b'/one', b'two', b'three') 16 | b'/one/two/three' 17 | 18 | >>> join('/one', 'two', '/three', 'four') 19 | '/three/four' 20 | ''' 21 | # note: benchmark tested. 22 | cdef unsigned int i, length = len(other) 23 | 24 | if not length: 25 | return path 26 | 27 | cdef bint byte = type(path) is bytes 28 | 29 | for i in range(length): 30 | if byte: 31 | path = join_bytes(path, other[i]) 32 | else: 33 | path = join_string(path, other[i]) 34 | return path 35 | 36 | 37 | cpdef inline str join_string(str path, str other): 38 | ''' Join Two String Paths 39 | 40 | Example 41 | >>> join_string('/one', 'two') 42 | '/one/two' 43 | 44 | >>> join_string('/one', '/two') 45 | '/two' 46 | ''' 47 | cdef unsigned int length = len(other) 48 | 49 | if not length: 50 | return path 51 | 52 | if other[0] is '/': 53 | return other 54 | 55 | if other[length-1] is '/': 56 | return path + other 57 | 58 | return path + '/' + other 59 | 60 | 61 | cpdef inline bytes join_bytes(bytes path, bytes other): 62 | ''' Join Two Bytes Paths 63 | 64 | Example 65 | >>> join_bytes(b'/one', b'two') 66 | b'/one/two' 67 | 68 | >>> join_bytes(b'/one', b'/two') 69 | b'/two' 70 | ''' 71 | cdef unsigned int length = len(other) 72 | 73 | if not length: 74 | return path 75 | 76 | if other[0] is 47: # b'/' 77 | return other 78 | 79 | if other[length-1] is 47: 80 | return path + other 81 | 82 | return path + b'/' + other 83 | 84 | 85 | cpdef inline bint isabs(object path, bint error=True) except -1: 86 | ''' Absolute path 87 | 88 | Type 89 | path: str | bytes 90 | return: bool 91 | 92 | Example 93 | >>> isabs('/dev/shm') 94 | >>> isabs(b'/dev/shm') 95 | True 96 | 97 | >>> isabs('dev/shm') 98 | >>> isabs(b'dev/shm') 99 | False 100 | ''' 101 | cdef unicode msg 102 | if not path: 103 | if error: 104 | msg = '`isabs()` - received empty `path`' 105 | raise ValueError(msg) 106 | return False 107 | 108 | cdef type t = type(path) 109 | 110 | if t is str: 111 | return isabs_string(path) 112 | elif t is bytes: 113 | return isabs_bytes(path) 114 | elif error: 115 | msg = '`isabs()` - takes type `str` or `bytes`' 116 | raise TypeError(msg) 117 | else: 118 | return False 119 | 120 | 121 | cdef inline bint isabs_string(str path) noexcept: 122 | # note: assumes `path` is always true since this is cdef 123 | return path[0] is '/' 124 | 125 | 126 | cdef inline bint isabs_bytes(bytes path) noexcept: 127 | # note: assumes `path` is always true since this is cdef 128 | return path[0] is 47 129 | -------------------------------------------------------------------------------- /src/shakti/lib/time.pxd: -------------------------------------------------------------------------------- 1 | from posix.time cimport CLOCK_MONOTONIC, clockid_t, timespec, \ 2 | clock_gettime as __clock_gettime 3 | 4 | 5 | cdef inline double clock_gettime(clockid_t flag) noexcept nogil: 6 | ''' Clock Get Time 7 | 8 | Example 9 | >>> clock_gettime() 10 | >>> clock_gettime(CLOCK_MONOTONIC_RAW) 11 | 12 | Flags 13 | CLOCK_REALTIME 14 | CLOCK_MONOTONIC 15 | # Linux-specific clocks 16 | CLOCK_PROCESS_CPUTIME_ID 17 | CLOCK_THREAD_CPUTIME_ID 18 | CLOCK_MONOTONIC_RAW 19 | CLOCK_REALTIME_COARSE 20 | CLOCK_MONOTONIC_COARSE 21 | CLOCK_BOOTTIME 22 | CLOCK_REALTIME_ALARM 23 | CLOCK_BOOTTIME_ALARM 24 | 25 | Note 26 | - `clock_gettime` is vDSO thus won't incur a syscall. 27 | - This is meant to be internal function, as Python user can use `time.clock_gettime()` 28 | ''' 29 | cdef timespec ts 30 | __clock_gettime(flag, &ts) 31 | return ts.tv_sec + (ts.tv_nsec / 1_000_000_000) 32 | 33 | 34 | cdef class Timeit: 35 | cdef: 36 | bint print 37 | double eclipsing_time 38 | readonly double total_time 39 | -------------------------------------------------------------------------------- /src/shakti/lib/time.pyx: -------------------------------------------------------------------------------- 1 | cdef class Timeit: 2 | ''' Simple Benchmark 3 | 4 | Example 5 | >>> with Timeit(): 6 | ... # do stuffs 7 | ------------------------ 8 | Time: 0.0001280000000000 9 | 10 | # Time Multiple Tests 11 | >>> with Timeit() as t: 12 | ... t.start 13 | ... # do stuff 1 14 | ... t.stop 15 | ------------------------ 16 | Stop: 0.0005770000000000 17 | ... 18 | ... t.start 19 | ... # do stuff 2 20 | ... t.stop 21 | ------------------------ 22 | Stop: 0.0005860000000000 23 | ... 24 | ------------------------ 25 | Time: 0.0011970000000000 26 | 27 | # Disable Total Time Print 28 | >>> with Timeit(print=False) as t: 29 | ... print(t.total_time) 30 | 0.0012300000000000 31 | ''' 32 | def __cinit__(self, bint print=True): 33 | self.print = print 34 | self.total_time = clock_gettime(CLOCK_MONOTONIC) 35 | self.eclipsing_time = 0 36 | 37 | def __enter__(self): 38 | return self 39 | 40 | def __exit__(self, *errors): 41 | self.total_time = clock_gettime(CLOCK_MONOTONIC) - self.total_time 42 | if self.print: 43 | print(f'------------------------------\nTotal Time: {self.total_time:.16f}') 44 | 45 | @property 46 | def start(self)-> double: 47 | self.eclipsing_time = clock_gettime(CLOCK_MONOTONIC) - self.eclipsing_time 48 | return self.eclipsing_time 49 | 50 | @property 51 | def stop(self)-> double: 52 | cdef double result = clock_gettime(CLOCK_MONOTONIC) - self.eclipsing_time 53 | if self.print: 54 | print(f'------------------------\nTime: {result:.16f}\n') 55 | self.eclipsing_time = 0 56 | return result 57 | -------------------------------------------------------------------------------- /src/shakti/os/__init__.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoSTEALTH/Shakti/a117dc862a673a0ea24a41d981eeb02ae21ba1ca/src/shakti/os/__init__.pxd -------------------------------------------------------------------------------- /src/shakti/os/mk.pyx: -------------------------------------------------------------------------------- 1 | # 2 | 3 | from libc.errno cimport EEXIST 4 | from liburing.lib.type cimport __AT_FDCWD, mode_t 5 | from liburing.os cimport io_uring_prep_mkdirat 6 | from ..lib.error cimport DirExistsError 7 | from ..lib.path cimport join_bytes, join_string 8 | from ..event.entry cimport SQE 9 | from random import choice 10 | from math import perm 11 | 12 | 13 | # TODO: makedirs 14 | 15 | 16 | TEMPDIR = '/tmp' 17 | # note: The "/tmp" directory must be made available for programs that require temporary files. 18 | # https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s18.html 19 | ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 20 | 21 | 22 | async def mkdir(str path not None, mode_t mode=0o777, *, int dir_fd=__AT_FDCWD): 23 | ''' 24 | Example 25 | >>> await mkdir('create-directory') 26 | ''' 27 | cdef: 28 | SQE sqe = SQE() 29 | bytes _path = path.encode() 30 | io_uring_prep_mkdirat(sqe, _path, mode, dir_fd) 31 | await sqe 32 | 33 | 34 | async def mktdir(str prefix not None='', 35 | str suffix not None='', 36 | unsigned int length=16, 37 | str tempdir not None=TEMPDIR, 38 | mode_t mode=0o777, 39 | int dir_fd=__AT_FDCWD)-> str: 40 | ''' Make Temporary Directory 41 | 42 | Example 43 | >>> await mktdir() 44 | /tmp/gPBRwtGLhnoY5wSd 45 | 46 | # custom 47 | >>> await mktdir('start-','-end', 4) 48 | '/tmp/start-3mf8-end' 49 | 50 | Note 51 | - A random named directory is created in `/tmp/abc123...` 52 | - Directory will need to be moved or deleted manually. 53 | ''' 54 | if not length: 55 | return '' 56 | 57 | cdef: 58 | SQE sqe = SQE() # reuse same `sqe` memory 59 | str name 60 | bytes path 61 | unicode msg 62 | unsigned int retry = len(ALPHABET) 63 | 64 | for _ in range(perm(retry, length)): 65 | name = prefix + ''.join(choice(ALPHABET) for _ in range(length)) + suffix 66 | path = join_string(tempdir, name).encode() 67 | io_uring_prep_mkdirat(sqe, path, mode, dir_fd) 68 | try: 69 | await sqe 70 | except FileExistsError: 71 | continue 72 | else: 73 | return path.decode() 74 | msg = 'mktdir() - No usable temporary directory name found' 75 | raise DirExistsError(EEXIST, msg) 76 | -------------------------------------------------------------------------------- /src/shakti/os/random.pyx: -------------------------------------------------------------------------------- 1 | from liburing.common cimport iovec, io_uring_prep_close 2 | from liburing.file cimport io_uring_prep_openat, io_uring_prep_read 3 | from ..event.entry cimport SQE 4 | 5 | 6 | async def random_bytes(unsigned int length)-> bytes: 7 | ''' Async Random Bytes 8 | 9 | Example 10 | >>> await random_bytes(3) 11 | b'4\x98\xde' 12 | ''' 13 | if length == 0: 14 | return b'' 15 | 16 | cdef: 17 | SQE sqe = SQE(), sqes = SQE(2) 18 | bytearray buffer = bytearray(length) 19 | unsigned int result 20 | 21 | # open 22 | io_uring_prep_openat(sqe, b'/dev/urandom') 23 | await sqe 24 | result = sqe.result # fd 25 | 26 | # read & close 27 | io_uring_prep_read(sqes, result, buffer, length) 28 | io_uring_prep_close(sqes[1], result) 29 | await sqes 30 | result = sqes.result 31 | 32 | return bytes(buffer if result == length else buffer[:result]) 33 | -------------------------------------------------------------------------------- /src/shakti/os/remove.pyx: -------------------------------------------------------------------------------- 1 | from libc.errno cimport ENOENT 2 | from liburing.os cimport io_uring_prep_unlinkat 3 | from liburing.lib.type cimport __AT_FDCWD, __AT_REMOVEDIR 4 | from liburing.error cimport trap_error 5 | from ..event.entry cimport SQE 6 | 7 | 8 | async def remove(object path not None, bint is_dir=False, *, 9 | bint ignore=False, int dir_fd=__AT_FDCWD): 10 | ''' Remove File | Directory 11 | 12 | Type 13 | path: str | bytes 14 | is_dir: bool 15 | ignore: bool 16 | dir_fd: int 17 | return: None 18 | 19 | Example 20 | >>> await remove('./file-path.ext') # remove file 21 | >>> await remove('./directory/', True) # remove directory 22 | 23 | Note 24 | - `ignore=True` - ignore if file/directory does not exists. 25 | - `dir_fd` paths relative to directory descriptors. 26 | 27 | Version 28 | Linux 5.11 29 | ''' 30 | cdef: 31 | SQE sqe = SQE(1, False) 32 | int flags = __AT_REMOVEDIR if is_dir else 0 33 | 34 | if type(path) is str: 35 | path = path.encode() 36 | 37 | io_uring_prep_unlinkat(sqe, path, flags, dir_fd) 38 | await sqe 39 | if ignore: 40 | if not (sqe.result & -ENOENT): 41 | trap_error(sqe.result) 42 | else: 43 | trap_error(sqe.result) 44 | -------------------------------------------------------------------------------- /src/shakti/os/rename.pyx: -------------------------------------------------------------------------------- 1 | from liburing.lib.type cimport * 2 | from liburing.os cimport io_uring_prep_renameat 3 | from ..event.entry cimport SQE 4 | 5 | 6 | async def rename(str old_path not None, 7 | str new_path not None, 8 | int flags=__RENAME_NOREPLACE, 9 | *, 10 | int old_dir_fd=__AT_FDCWD, 11 | int new_dir_fd=__AT_FDCWD)-> None: 12 | ''' Rename File | Dirctory 13 | 14 | Example 15 | >>> await rename('old-name.txt', 'new-name.txt') 16 | 17 | Flag 18 | - RENAME_EXCHANGE 19 | - Atomically exchange `old_path` and `new_path`. 20 | - Both pathnames must exist but may be of different types 21 | - RENAME_NOREPLACE (set as default) 22 | - Don't overwrite `new_path` of the rename. 23 | - Raises an `OSError` if `new_path` already exists. 24 | - RENAME_WHITEOUT 25 | - This operation makes sense only for overlay/union filesystem implementations. 26 | 27 | Note 28 | - `rename` can also be used to move file/dir as well. 29 | 30 | Version 31 | linux 5.11 32 | ''' 33 | cdef: 34 | SQE sqe = SQE() 35 | bytes _old_path = old_path.encode() 36 | bytes _new_path = new_path.encode() 37 | io_uring_prep_renameat(sqe, _old_path, _new_path, old_dir_fd, new_dir_fd, flags) 38 | await sqe 39 | 40 | 41 | cpdef enum __rename_define__: 42 | RENAME_NOREPLACE = __RENAME_NOREPLACE 43 | RENAME_EXCHANGE = __RENAME_EXCHANGE 44 | RENAME_WHITEOUT = __RENAME_WHITEOUT 45 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import pytest 4 | import pathlib 5 | import getpass 6 | import tempfile 7 | import liburing 8 | # import shakti 9 | 10 | 11 | @pytest.fixture 12 | def tmp_dir(): 13 | ''' Temporary directory to store test data. 14 | 15 | Example 16 | >>> test_my_function(tmp_dir) 17 | ... # create directory 18 | ... # ---------------- 19 | ... my_dir = tmp_dir / 'my_dir' 20 | ... my_dir.mkdir() 21 | ... 22 | ... # create file 23 | ... # ----------- 24 | ... my_file = tmp_dir / 'my_file.ext' 25 | ... my_file.write_text('Hello World!!!') 26 | 27 | Note 28 | - unlike pytest's `tmpdir`, `tmp_path`, ... `tmp_dir` generated 29 | files & directories are not deleted after 3 runs. 30 | ''' 31 | path = f'{tempfile.gettempdir()}/pytest-of-{getpass.getuser()}-holder' 32 | if not os.path.exists(path): 33 | os.mkdir(path) 34 | return pathlib.Path(tempfile.mkdtemp(dir=path)) 35 | 36 | 37 | # linux version start >>> 38 | LINUX_VERSION = f'{liburing.LINUX_VERSION_MAJOR}.{liburing.LINUX_VERSION_MINOR}' 39 | 40 | 41 | @pytest.fixture(autouse=True) 42 | def skip_by_platform(request): 43 | ''' 44 | Example 45 | >>> @pytest.mark.skip_linux(6.7) 46 | >>> def test_function(): 47 | ... 48 | test.py::test_function SKIPPED (Linux `6.7 < 6.8`) 49 | 50 | >>> @pytest.mark.skip_linux(6.7, 'custom message') 51 | >>> def test_function(): 52 | ... 53 | test.py::test_function SKIPPED (custom message) 54 | 55 | >>> @pytest.mark.skip_linux('6.7', '') 56 | >>> def test_function(): 57 | ... 58 | test.py::test_function SKIPPED 59 | ''' 60 | if r := request.node.get_closest_marker('skip_linux'): 61 | if liburing.linux_version_check(version := r.args[0]): 62 | msg = r.args[1] if len(r.args) > 1 else f'Kernel `{LINUX_VERSION} < {version}`' 63 | pytest.skip(msg) 64 | 65 | 66 | def pytest_configure(config): 67 | config.addinivalue_line( 68 | "markers", 69 | "skip_linux(version:str|float|int, message:str): skipping linux version not supported.") 70 | # linux version end <<< 71 | -------------------------------------------------------------------------------- /test/core/asyncbase_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from shakti import run, AsyncBase 3 | 4 | 5 | def test_async(): 6 | run( 7 | await_class(), 8 | with_class() 9 | ) 10 | 11 | 12 | class AsyncTest(AsyncBase): 13 | async def __ainit__(self): 14 | self.hello = 'hello world' 15 | 16 | async def call(self): 17 | return 'you called?' 18 | 19 | 20 | async def await_class(): 21 | a = await AsyncTest() 22 | assert a.hello == 'hello world' 23 | 24 | with pytest.raises(AttributeError): 25 | a.something 26 | 27 | b = AsyncTest() 28 | with pytest.raises(AttributeError): 29 | assert b.hello # note: since `await` isn't used `__ainit__` isn't initialized. 30 | assert await b.call() == 'you called?' 31 | 32 | async with AsyncTest() as test: 33 | assert test.hello == 'hello world' 34 | assert await test.call() == 'you called?' 35 | 36 | 37 | async def with_class(): 38 | with pytest.raises(SyntaxError): 39 | with AsyncTest(): 40 | pass 41 | -------------------------------------------------------------------------------- /test/event/run_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | import shakti 4 | 5 | 6 | def test_run(): 7 | assert shakti.run() == [] 8 | assert shakti.run(echo(1), echo(2), echo(3)) == [1, 2, 3] 9 | shakti.run(*[coro() for i in range(1000)]) 10 | 11 | msg = '`run()` - `entries` is set too low! entries: 1' 12 | with pytest.raises(ValueError, match=re.escape(msg)): 13 | shakti.run(echo(1), echo(2), entries=1) 14 | 15 | msg = '`run()` - `entries` is set too high! max entries: 32768' 16 | with pytest.raises(ValueError, match=re.escape(msg)): 17 | shakti.run(*[echo(1) for i in range(32770)], entries=100_000) 18 | 19 | msg = '`run()` only accepts `CoroutineType`, like `async` function.' 20 | with pytest.raises(TypeError, match=re.escape(msg)): 21 | shakti.run(not_coro()) 22 | 23 | 24 | async def echo(arg): 25 | return arg 26 | 27 | 28 | async def coro(): 29 | return 'lost of coro' 30 | 31 | 32 | def not_coro(): 33 | return 'boo' 34 | -------------------------------------------------------------------------------- /test/event/sleep_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | import shakti 4 | 5 | 6 | def test_sleep(): 7 | with shakti.Timeit(False) as t: 8 | t.start 9 | shakti.run( 10 | sleep_test() 11 | ) 12 | assert 1.25 < t.stop < 1.3 13 | 14 | 15 | async def sleep_test(): 16 | await shakti.sleep(1) # int 17 | await shakti.sleep(.25) # float 18 | 19 | msg = re.escape('`sleep(second)` can not be `< 0`') 20 | with pytest.raises(ValueError, match=msg): 21 | await shakti.sleep(-1) 22 | 23 | # TODO: need to test flags. 24 | -------------------------------------------------------------------------------- /test/event/sqe_test.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import liburing 3 | import pytest 4 | import shakti 5 | 6 | 7 | def test_SQE(): 8 | shakti.run( 9 | single_sqe(), 10 | multiple_sqes(1000), 11 | with_statement() 12 | ) 13 | 14 | 15 | async def single_sqe(): 16 | sqe = shakti.SQE() 17 | liburing.io_uring_prep_openat(sqe, b'/dev/zero') 18 | await sqe 19 | await shakti.close(fd := sqe.result) # fd 20 | assert fd > 0 21 | 22 | with pytest.raises(ValueError): 23 | shakti.SQE(1025) 24 | 25 | 26 | async def multiple_sqes(loop): 27 | sqes = shakti.SQE(loop, True) 28 | for i in range(loop): 29 | liburing.io_uring_prep_openat(sqes[i], b'/dev/zero') 30 | await sqes 31 | for i in range(loop): 32 | await shakti.close(fd := sqes[i].result) 33 | assert fd > 0 34 | 35 | 36 | async def with_statement(): 37 | # single 38 | async with shakti.SQE(error=False) as sqe: 39 | liburing.io_uring_prep_openat(sqe, b'/dev/zero') 40 | await shakti.close(fd := sqe.result) # fd 41 | assert fd > 0 42 | 43 | # multiple 44 | async with shakti.SQE(2, error=False) as sqe: 45 | liburing.io_uring_prep_openat(sqe, b'/bad-link') 46 | liburing.io_uring_prep_openat(sqe[1], b'/dev/zero') 47 | assert sqe.result == -errno.ENOENT 48 | await shakti.close(fd := sqe[1].result) # fd 49 | assert fd > 0 50 | -------------------------------------------------------------------------------- /test/event/task_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shakti 3 | 4 | 5 | def test_task(): 6 | shakti.run(task_error(), task_check(), task_multi()) 7 | 8 | 9 | async def task_error(): 10 | with pytest.raises(TypeError): 11 | await shakti.task(bad) 12 | 13 | with pytest.raises(ValueError): 14 | await shakti.task() 15 | 16 | 17 | async def task_check(): 18 | r = [] 19 | await shakti.task(echo(r, 1)) 20 | await shakti.sleep(.001) 21 | assert r == [0] 22 | await shakti.sleep(.003) 23 | assert r == [0, 1] 24 | 25 | 26 | async def task_multi(): 27 | r = [] 28 | await shakti.task(echo(r, 1), echo(r, 2), echo(r, 3)) 29 | await shakti.sleep(.05) 30 | assert len(r) == 2 + 4 + 6 31 | 32 | 33 | # resource start >>> 34 | def bad(): 35 | pass 36 | 37 | 38 | async def echo(r, value): 39 | for i in range(value): 40 | r.append(i) 41 | await shakti.sleep(.002) 42 | r.append(i+1) 43 | # resource end <<< 44 | -------------------------------------------------------------------------------- /test/io/common_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shakti 3 | 4 | 5 | def test_common(tmp_dir): 6 | shakti.run(close_error()) 7 | 8 | 9 | async def close_error(): 10 | with pytest.raises(OSError, match='Bad file descriptor'): 11 | await shakti.close(12345) 12 | 13 | with pytest.raises(OSError, match='No such device or address'): 14 | await shakti.close(12345, True) 15 | -------------------------------------------------------------------------------- /test/io/file_test.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pytest 3 | import shakti 4 | 5 | 6 | def test_file(tmp_dir): 7 | shakti.run( 8 | open_close(tmp_dir), 9 | read(), 10 | write(tmp_dir), 11 | 12 | # # File START >>> 13 | file_open_close(tmp_dir), 14 | temp_file(tmp_dir), 15 | resolve(tmp_dir), 16 | write_read(tmp_dir), 17 | # File END <<< 18 | ) 19 | 20 | 21 | async def open_close(tmp_dir): 22 | file_path = str(tmp_dir) 23 | fd = await shakti.open(file_path, shakti.O_TMPFILE | shakti.O_RDWR) 24 | assert fd > 3 25 | assert await shakti.exists(file_path) 26 | await shakti.close(fd) 27 | 28 | file_path = str(tmp_dir / 'open.txt') 29 | fd = await shakti.open(file_path, shakti.O_CREAT) 30 | assert fd > 3 31 | await shakti.close(fd) 32 | 33 | async with shakti.Statx(file_path) as stat: 34 | assert stat.isfile 35 | 36 | 37 | async def read(): 38 | fd = await shakti.open('/dev/random') 39 | assert len(await shakti.read(fd, 0)) == 0 40 | assert len(await shakti.read(fd, 10)) == 10 41 | await shakti.close(fd) 42 | 43 | 44 | async def write(tmp_dir): 45 | file_path = str(tmp_dir / 'write.text') 46 | fd = await shakti.open(file_path, 47 | shakti.O_CREAT | shakti.O_WRONLY | shakti.O_APPEND, mode=0o440) 48 | assert await shakti.write(fd, b'hi') == 2 49 | assert await shakti.write(fd, bytearray(b'...')) == 3 50 | assert await shakti.write(fd, memoryview(b'bye!')) == 4 51 | assert (await shakti.Statx(file_path)).stx_size == 9 52 | await shakti.close(fd) 53 | 54 | 55 | # File START >>> 56 | async def file_open_close(tmp_dir): 57 | # normal file 58 | async with shakti.File(__file__) as file: 59 | assert file.fileno > 1 60 | assert file.path == __file__.encode() 61 | assert bool(file) is True 62 | assert bool(file) is False 63 | 64 | # temp file 65 | file = await shakti.File('.', 'T') 66 | assert file.fileno > 1 67 | assert file.path == b'.' 68 | assert bool(file) is True 69 | await file.close() 70 | assert bool(file) is False 71 | 72 | # create new file 73 | path = os.path.join(tmp_dir, 'exists.txt') 74 | await (await shakti.File(path, 'x')).close() 75 | with pytest.raises(FileExistsError): 76 | await (await shakti.File(path, 'x')).close() 77 | # ignore if file already exists 78 | await (await shakti.File(path, '!x')).close() 79 | 80 | 81 | async def temp_file(tmp_dir): 82 | # openat 83 | async with shakti.File('.', 'Trw', encoding='latin-1') as file: 84 | assert await file.write('hello world') == 11 85 | assert await file.read(11, 0) == 'hello world' 86 | 87 | # openat2 88 | async with shakti.File('.', 'Trw', resolve=shakti.RESOLVE_CACHED) as file: 89 | assert await file.write('hello world') == 11 90 | assert await file.read(11, 0) == 'hello world' 91 | 92 | async with shakti.File('/tmp', 'T') as file: 93 | assert await file.write('hello world') == 11 94 | 95 | # create dummy blocker file 96 | path = os.path.join(tmp_dir, 'block.txt') 97 | await (await shakti.File(path, 'x')).close() 98 | 99 | with pytest.raises(NotADirectoryError): 100 | async with shakti.File(path, 'T') as file: 101 | assert await file.write('hello world') == 11 102 | 103 | 104 | async def resolve(tmp_dir): 105 | file_path = os.path.join(tmp_dir, 'resolve_test.txt') 106 | # This will catch BlockingIOError that removes `RESOLVE_CACHED` from `how` 107 | # note: not really a way to test this other then to manual add `print` statement to see output. 108 | async with shakti.File(file_path, 'x', resolve=shakti.RESOLVE_CACHED): 109 | pass 110 | async with shakti.File(file_path, 'rwb', resolve=shakti.RESOLVE_CACHED) as file: 111 | assert await file.write(b'hello world') == 11 112 | assert await file.read(None, 0) == b'hello world' 113 | 114 | 115 | async def write_read(tmp_dir): 116 | path = os.path.join(tmp_dir, 'test-write-open.txt') 117 | async with shakti.File(path, 'xb+') as file: 118 | # write 119 | assert await file.write(bytearray(b'hello')) == 5 120 | assert await file.write(b' ') == 1 121 | assert await file.write(memoryview(b'world')) == 5 122 | # read 123 | assert await file.read(5, 0) == b'hello' 124 | assert await file.read(6) == b' world' 125 | assert await file.read(5, 3) == b'lo wo' 126 | assert await file.read() == b'rld' 127 | assert await file.read(None, 0) == b'hello world' 128 | assert await file.read() == b'' 129 | # type check 130 | assert isinstance(await file.read(11, 0), bytes) 131 | 132 | async with shakti.File(path) as file: 133 | assert await file.read() == 'hello world' 134 | 135 | # stats eheck 136 | assert (await shakti.Statx(path)).stx_size == 11 137 | # File END <<< 138 | -------------------------------------------------------------------------------- /test/io/socket_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shakti 3 | 4 | 5 | @pytest.mark.skip_linux(6.11) 6 | def test_socket(): 7 | random_port = [] 8 | shakti.run( 9 | socket(), 10 | bind(), 11 | listen(), 12 | echo_client(random_port), 13 | echo_server(random_port), 14 | ) 15 | 16 | 17 | @pytest.mark.skip_linux(6.7) 18 | def test_sockname(): 19 | shakti.run(set_get_sockname()) 20 | 21 | 22 | async def socket(): 23 | # file descriptor 24 | assert (sock_fd1 := await shakti.socket()) > 0 # shakti.AF_INET, shakti.SOCK_STREAM 25 | assert (sock_fd2 := await shakti.socket(shakti.AF_UNIX, shakti.SOCK_DGRAM)) > sock_fd1 26 | await shakti.close(sock_fd1) 27 | await shakti.close(sock_fd2) 28 | 29 | # direct descriptor without register 30 | with pytest.raises(OSError, match='Either file table is full or register file not enabled!'): 31 | assert await shakti.socket(direct=True) 32 | 33 | # TODO: direct descriptor with register 34 | # assert (sock_fd := await shakti.socket(direct=True)) == 0 35 | # await shakti.close(sock_fd, True) 36 | 37 | 38 | async def bind(): 39 | # IPv4 40 | sockfd = await shakti.socket() 41 | addr = await shakti.bind(sockfd, '127.0.0.1', 0) 42 | ip, port = await shakti.getsockname(sockfd, addr) 43 | assert ip == '127.0.0.1' 44 | assert port > 1000 45 | await shakti.close(sockfd) 46 | 47 | # TODO: 48 | # IPv6 49 | # sockfd = await shakti.socket(shakti.AF_INET6) 50 | # addr = await shakti.bind(sockfd, '::1', 12345) 51 | # ip, port = await shakti.getsockname(sockfd, addr) 52 | # assert ip == '::1' 53 | # assert port > 1000 54 | # await shakti.close(sockfd) 55 | 56 | 57 | async def listen(): 58 | sockfd = await shakti.socket() 59 | await shakti.bind(sockfd, '127.0.0.1', 0) 60 | assert await shakti.listen(sockfd, 1) == 0 61 | await shakti.close(sockfd) 62 | 63 | 64 | async def echo_server(random_port): 65 | assert (server_fd := await shakti.socket()) > 0 66 | try: 67 | addr = await shakti.bind(server_fd, '127.0.0.1', 0) # random port 68 | assert await shakti.listen(server_fd, 1) == 0 69 | ip, port = await shakti.getsockname(server_fd, addr) 70 | assert ip == '127.0.0.1' 71 | assert port > 1000 72 | random_port.append(port) 73 | while client_fd := await shakti.accept(server_fd): 74 | # await task(client_handler(client_fd)) 75 | await client_handler(client_fd) 76 | break 77 | finally: 78 | await shakti.close(server_fd) 79 | 80 | 81 | async def client_handler(client_fd): 82 | assert await shakti.recv(client_fd, 1024) == b'hi from `echo_client`' 83 | assert await shakti.send(client_fd, b'hi from `echo_server`') == 21 84 | await shakti.shutdown(client_fd) 85 | await shakti.close(client_fd) 86 | 87 | 88 | async def echo_client(random_port): 89 | await shakti.sleep(.001) # wait for `echo_server` to start up. 90 | assert (client_fd := await shakti.socket()) 91 | try: 92 | await shakti.connect(client_fd, '127.0.0.1', random_port[0]) 93 | assert await shakti.send(client_fd, b'hi from `echo_client`') == 21 94 | assert await shakti.recv(client_fd, 1024) == b'hi from `echo_server`' 95 | finally: 96 | await shakti.close(client_fd) 97 | 98 | 99 | async def set_get_sockname(): 100 | assert (socket_fd := await shakti.socket()) > 0 101 | try: 102 | await shakti.setsockopt(socket_fd, shakti.SOL_SOCKET, shakti.SO_REUSEADDR, 1) 103 | assert await shakti.getsockopt(socket_fd, shakti.SOL_SOCKET, shakti.SO_REUSEADDR) == 1 104 | 105 | await shakti.setsockopt(socket_fd, shakti.SOL_SOCKET, shakti.SO_REUSEADDR, 0) 106 | assert await shakti.getsockopt(socket_fd, shakti.SOL_SOCKET, shakti.SO_REUSEADDR) == 0 107 | finally: 108 | await shakti.close(socket_fd) 109 | -------------------------------------------------------------------------------- /test/io/statx_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import socket 3 | import shakti 4 | 5 | 6 | def test_statx(tmp_dir): 7 | shakti.run( 8 | statx_class(tmp_dir), 9 | bad_file(), 10 | type_check(), 11 | exists_check(tmp_dir) 12 | ) 13 | 14 | 15 | async def statx_class(tmp_dir): 16 | async with shakti.Statx('/dev/zero') as statx: 17 | assert statx.stx_size == 0 18 | assert statx.isfile is False 19 | 20 | statx = await shakti.Statx('/dev/zero') 21 | assert statx.stx_size == 0 22 | assert statx.isfile is False 23 | 24 | # create socket file. 25 | server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 26 | server.bind(sock_path := str(tmp_dir / 'test.sock')) 27 | 28 | async with shakti.Statx(sock_path) as statx: 29 | assert statx.stx_size == 0 30 | assert statx.isfile is False 31 | assert statx.issock is True 32 | 33 | 34 | async def bad_file(): 35 | with pytest.raises(FileNotFoundError): 36 | await shakti.Statx('bad_file.txt') 37 | 38 | 39 | async def type_check(): 40 | with pytest.raises(TypeError): 41 | await shakti.Statx(None) 42 | 43 | 44 | async def exists_check(tmp_dir): 45 | file_path = tmp_dir / 'file.txt' 46 | file_path.write_text('hi') 47 | file_path = str(file_path) 48 | 49 | dir_path = tmp_dir / 'directory' 50 | dir_path.mkdir() 51 | dir_path = str(dir_path) 52 | 53 | assert not await shakti.exists('no_file.txt') 54 | assert await shakti.exists(file_path) 55 | assert await shakti.exists(dir_path) 56 | with pytest.raises(TypeError): 57 | await shakti.exists(None) 58 | -------------------------------------------------------------------------------- /test/lib/error_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shakti 3 | 4 | 5 | def test_error(tmp_dir): 6 | shakti.run(error()) 7 | 8 | 9 | async def error(): 10 | with pytest.raises(shakti.CancelledError): 11 | raise shakti.CancelledError() 12 | 13 | with pytest.raises(shakti.UnsupportedOperation): 14 | raise shakti.UnsupportedOperation() 15 | 16 | with pytest.raises(shakti.ConnectionNotEstablishedError): 17 | raise shakti.ConnectionNotEstablishedError() 18 | -------------------------------------------------------------------------------- /test/lib/path_test.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from shakti import isabs, join 3 | 4 | 5 | def test_isabs(): 6 | # copied from cpython `Lib/test/test_posixpath.py` 7 | with raises(ValueError): 8 | assert isabs(None) is False 9 | with raises(ValueError): 10 | assert isabs('') is False 11 | assert isabs('/') is True 12 | assert isabs('/foo') is True 13 | assert isabs('/foo/bar') is True 14 | assert isabs('foo/bar') is False 15 | 16 | with raises(ValueError): 17 | assert isabs(b'') is False 18 | assert isabs(b'/') is True 19 | assert isabs(b'/foo') is True 20 | assert isabs(b'/foo/bar') is True 21 | assert isabs(b'foo/bar') is False 22 | 23 | with raises(TypeError): 24 | assert isabs(['bad type']) is False 25 | 26 | 27 | def test_join(): 28 | assert join('/foo', '', '') == '/foo' 29 | assert join(b'/foo', b'', b'') == b'/foo' 30 | 31 | # copied from cpython `Lib/test/test_posixpath.py` 32 | assert join('/foo', 'bar', '/bar', 'baz') == '/bar/baz' 33 | assert join('/foo', 'bar', 'baz') == '/foo/bar/baz' 34 | assert join('/foo/', 'bar/', 'baz/') == '/foo/bar/baz/' 35 | 36 | assert join(b'/foo', b'bar', b'/bar', b'baz') == b'/bar/baz' 37 | assert join(b'/foo', b'bar', b'baz') == b'/foo/bar/baz' 38 | assert join(b'/foo/', b'bar/', b'baz/') == b'/foo/bar/baz/' 39 | 40 | -------------------------------------------------------------------------------- /test/os/mk_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | import shakti 4 | 5 | 6 | def test(tmp_dir): 7 | shakti.run( 8 | mkdir(tmp_dir), 9 | 10 | # mktdir START >>> 11 | make_temp_dir(tmp_dir), 12 | make_remove_tmp(), 13 | make_error(tmp_dir) 14 | # mktdir END <<< 15 | ) 16 | 17 | 18 | async def mkdir(tmp_dir): 19 | dir_path = str(tmp_dir / 'dir') 20 | await shakti.mkdir(dir_path) 21 | 22 | async with shakti.Statx(dir_path) as stat: 23 | assert stat.isdir 24 | assert not stat.isfile 25 | assert (stat.stx_mode & 0o777) == 0o777 - os.umask(0) # 755 26 | 27 | 28 | # mktdir START >>> 29 | async def make_temp_dir(tmp_dir): 30 | tmpdir = str(tmp_dir) 31 | one = await shakti.mktdir(mode=0o440, tempdir=tmpdir) 32 | async with shakti.Statx(one) as stat: 33 | assert stat.isdir 34 | assert not stat.isfile 35 | assert (stat.stx_mode & 0o777) == 0o440 36 | 37 | # prep 38 | prefix = 'start-' 39 | suffix = '-end' 40 | tmp_len = len(tmpdir)+1 41 | 42 | one = await shakti.mktdir(prefix, suffix, length=19, tempdir=tmpdir) 43 | assert await shakti.exists(one) 44 | assert len(one) == tmp_len + len(prefix) + len(suffix) + 19 45 | assert one[tmp_len:].startswith(prefix) 46 | assert one.endswith(suffix) 47 | 48 | 49 | async def make_remove_tmp(): 50 | # make and remove from "/tmp" 51 | tmp_path = await shakti.mktdir() 52 | assert len(tmp_path) == len('/tmp/') + 16 # default 53 | assert tmp_path.startswith('/tmp/') 54 | await shakti.remove(tmp_path, True) 55 | 56 | 57 | async def make_error(tmp_dir): 58 | tmpdir = str(tmp_dir) 59 | # with pytest.raises(ValueError): 60 | assert await shakti.mktdir('', '', length=0, tempdir=tmpdir) == '' 61 | 62 | # None check 63 | with pytest.raises(TypeError): 64 | await shakti.mktdir(None, '', length=0, tempdir=tmpdir) 65 | with pytest.raises(TypeError): 66 | await shakti.mktdir('', None, length=0, tempdir=tmpdir) 67 | with pytest.raises(TypeError): 68 | await shakti.mktdir('', '', length=0, tempdir=None) 69 | 70 | # runs out of combination to create 71 | i = 0 72 | tmp_path = await shakti.mktdir(tempdir=tmpdir) 73 | created = [] 74 | with pytest.raises(shakti.DirExistsError): 75 | while True: 76 | i += 1 77 | created.append(await shakti.mktdir(length=1, tempdir=tmp_path)) 78 | 79 | # remove files, doubles as created check. 80 | for i in created: 81 | await shakti.remove(shakti.join_string(tmp_path, i), True) 82 | await shakti.remove(tmp_path, True) 83 | # mktdir END <<< 84 | -------------------------------------------------------------------------------- /test/os/random_test.py: -------------------------------------------------------------------------------- 1 | import shakti 2 | 3 | 4 | def test_random(): 5 | shakti.run(main()) 6 | 7 | 8 | async def main(): 9 | data = await shakti.random_bytes(0) 10 | assert len(data) == 0 11 | assert data == b'' 12 | 13 | data = await shakti.random_bytes(12) 14 | assert len(data) == 12 15 | assert data != b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 16 | -------------------------------------------------------------------------------- /test/os/remove_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shakti 3 | 4 | 5 | def test_remove(tmp_dir): 6 | shakti.run( 7 | remove_file_dir(tmp_dir) 8 | ) 9 | 10 | 11 | async def remove_file_dir(tmp_dir): 12 | file_path = tmp_dir / 'file.txt' 13 | file_path.write_text('test') 14 | file_path = str(file_path) 15 | 16 | file_path_2 = tmp_dir / 'file2.txt' 17 | file_path_2.write_text('test') 18 | file_path_2 = str(file_path_2) 19 | 20 | dir_path = tmp_dir / 'directory' 21 | dir_path.mkdir() 22 | dir_path = str(dir_path) 23 | 24 | await shakti.remove(file_path) # remove file 25 | await shakti.remove(file_path, ignore=True) # ignore if file does not exist 26 | with pytest.raises(FileNotFoundError): 27 | await shakti.remove(file_path) 28 | 29 | await shakti.remove(file_path_2, ignore=True) # ignore if file does not exist 30 | await shakti.remove(dir_path, True) # remove directory 31 | 32 | assert not await shakti.exists(file_path) # file should not exist 33 | assert not await shakti.exists(file_path_2) # file should not exist 34 | assert not await shakti.exists(dir_path) # dir should not exist 35 | -------------------------------------------------------------------------------- /test/os/rename_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shakti 3 | 4 | 5 | def test_rename(tmp_dir): 6 | shakti.run( 7 | rename_file(tmp_dir), 8 | rename_dir(tmp_dir), 9 | rename_error(tmp_dir), 10 | rename_exchange_flag(tmp_dir), 11 | rename_move(tmp_dir), 12 | ) 13 | 14 | 15 | async def rename_file(tmp_dir): 16 | old_file_path = tmp_dir / 'old_file.txt' 17 | old_file_path.write_text('old') 18 | old_file_path = str(old_file_path) 19 | 20 | new_file_path = str(tmp_dir / 'new_file.txt') 21 | 22 | assert await shakti.exists(old_file_path) 23 | assert not await shakti.exists(new_file_path) 24 | 25 | await shakti.rename(old_file_path, new_file_path) 26 | 27 | assert not await shakti.exists(old_file_path) # old file should not exist 28 | assert await shakti.exists(new_file_path) # renamed file should exist 29 | 30 | 31 | async def rename_dir(tmp_dir): 32 | old_dir_path = str(tmp_dir / 'old_dir') 33 | new_dir_path = str(tmp_dir / 'new_dir') 34 | 35 | assert not await shakti.exists(old_dir_path) 36 | assert not await shakti.exists(new_dir_path) 37 | 38 | await shakti.mkdir(old_dir_path) 39 | assert await shakti.exists(old_dir_path) 40 | 41 | await shakti.rename(old_dir_path, new_dir_path) 42 | assert not await shakti.exists(old_dir_path) 43 | assert await shakti.exists(new_dir_path) 44 | 45 | 46 | async def rename_error(tmp_dir): 47 | old_dir_path = str(tmp_dir / 'old_error') 48 | new_dir_path = str(tmp_dir / 'new_error') 49 | 50 | await shakti.mkdir(old_dir_path) 51 | await shakti.mkdir(new_dir_path) 52 | 53 | with pytest.raises(FileExistsError): 54 | await shakti.rename(old_dir_path, new_dir_path) 55 | 56 | 57 | async def rename_exchange_flag(tmpdir): 58 | old_file_path = tmpdir / 'old_file_flag' 59 | old_file_path.write_text('old file') 60 | old_file_path = str(old_file_path) 61 | 62 | new_dir_path = str(tmpdir / 'new_dir_flag') 63 | await shakti.mkdir(new_dir_path) 64 | 65 | # rename exchange file and dir 66 | await shakti.rename(old_file_path, new_dir_path, shakti.RENAME_EXCHANGE) 67 | assert (await shakti.Statx(new_dir_path)).isfile # should be file path now 68 | assert (await shakti.Statx(old_file_path)).isdir # should be dir path now 69 | 70 | 71 | async def rename_move(tmpdir): 72 | one = str(tmpdir / 'one') 73 | two = str(tmpdir / 'one' / 'two') 74 | mov = str(tmpdir / 'two') 75 | 76 | file_before = str(tmpdir / 'one' / 'two' / 'file.txt') 77 | file_after = str(tmpdir / 'two' / 'file.txt') 78 | 79 | await shakti.mkdir(one) 80 | await shakti.mkdir(two) 81 | 82 | with open(file_before, 'x+') as file: 83 | file.write('file before') 84 | 85 | # add a file into "./one/two" dir 86 | assert await shakti.exists(one) 87 | assert await shakti.exists(two) 88 | assert await shakti.exists(file_before) 89 | 90 | # using `rename` to move directory from './one/two' to './two' 91 | await shakti.rename(two, mov) 92 | assert await shakti.exists(one) 93 | assert not await shakti.exists(two) 94 | assert await shakti.exists(mov) 95 | assert await shakti.exists(file_after) 96 | -------------------------------------------------------------------------------- /test/os/time_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import shakti 3 | 4 | 5 | def test_Timeit(): 6 | with shakti.Timeit(print=False) as t: 7 | # first 8 | t.start 9 | time.sleep(0.25) 10 | assert 0.25 < t.stop < 0.3 11 | 12 | # second 13 | t.start 14 | time.sleep(0.25) 15 | assert 0.25 < t.stop < 0.3 16 | 17 | # second 18 | t.start 19 | time.sleep(0.25) 20 | assert 0.25 < t.stop < 0.3 21 | 22 | # total time 23 | assert 0.75 < t.total_time < 0.8 24 | --------------------------------------------------------------------------------