├── .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 |
--------------------------------------------------------------------------------