sferriol-python 0.9.4__tar.gz → 0.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/PKG-INFO +1 -1
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/__init__.py +1 -0
- sferriol_python-0.9.4/sferriol/python/asyncio.py → sferriol_python-0.10.1/sferriol/python/asyncio_/__init__.py +7 -5
- sferriol_python-0.10.1/sferriol/python/asyncio_/stream.py +146 -0
- sferriol_python-0.10.1/sferriol/python/test/test__asyncio.py +78 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/PKG-INFO +1 -1
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/SOURCES.txt +2 -1
- sferriol_python-0.9.4/sferriol/python/test/test__asyncio.py +0 -44
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/Licence_CeCILL-B_V1-en.txt +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/README.md +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/pyproject.toml +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/setup.cfg +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/dictionary/__init__.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/env.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/json.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/net.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/object.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/os_.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/pkg.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/__init__.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/pkg/__init__.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/pkg/toto.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/pkg/tutu.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__dictionary.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__env.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__json.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__module.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__net.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__object.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__os.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__pkg.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__python.py +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/dependency_links.txt +0 -0
- {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import asyncio
|
|
1
|
+
import asyncio
|
|
2
2
|
import signal
|
|
3
3
|
|
|
4
|
+
from sferriol.python.asyncio_.stream import *
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
async def run_shell_cmd(cmd: str) -> (int, str, str):
|
|
6
8
|
"""Execute shell command
|
|
@@ -8,8 +10,8 @@ async def run_shell_cmd(cmd: str) -> (int, str, str):
|
|
|
8
10
|
Returns:
|
|
9
11
|
Tuple (returned code, stdout, stderr)
|
|
10
12
|
"""
|
|
11
|
-
proc = await
|
|
12
|
-
cmd, stdout=
|
|
13
|
+
proc = await asyncio.create_subprocess_shell(
|
|
14
|
+
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
13
15
|
stdout, stderr = await proc.communicate()
|
|
14
16
|
return proc.returncode, stdout.decode().strip(), stderr.decode().strip()
|
|
15
17
|
|
|
@@ -18,7 +20,7 @@ async def wait_sigint_signal() -> None:
|
|
|
18
20
|
"""
|
|
19
21
|
Wait until the SIGINT signal is received. SIGINT is the Interrupt from keyboard (CTRL + C).
|
|
20
22
|
"""
|
|
21
|
-
evt =
|
|
22
|
-
loop =
|
|
23
|
+
evt = asyncio.Event()
|
|
24
|
+
loop = asyncio.get_running_loop()
|
|
23
25
|
loop.add_signal_handler(signal.SIGINT, lambda: evt.set())
|
|
24
26
|
await evt.wait()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections import deque
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Deque
|
|
5
|
+
|
|
6
|
+
__all__ = ["Buffered_Stream"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Stream_Closed(Exception):
|
|
10
|
+
"""Stream is closed."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Stream_Overrun(Exception):
|
|
14
|
+
"""Reader fell too far behind, and some data has been overwritten."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class _Item:
|
|
19
|
+
sequence: int
|
|
20
|
+
value: Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Buffered_Stream:
|
|
24
|
+
"""Asynchronous stream for publishing and consuming a sequence of items.
|
|
25
|
+
Supports multiple independent subscribers with configurable sliding windows."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, *, buffer_size: int) -> None:
|
|
28
|
+
if buffer_size <= 0:
|
|
29
|
+
raise ValueError("buffer_size must be > 0")
|
|
30
|
+
|
|
31
|
+
self.buffer_size = buffer_size
|
|
32
|
+
|
|
33
|
+
self._buffer: Deque[_Item] = deque(maxlen=buffer_size)
|
|
34
|
+
self._next_sequence = 0
|
|
35
|
+
|
|
36
|
+
self._condition = asyncio.Condition()
|
|
37
|
+
self._closed = False
|
|
38
|
+
|
|
39
|
+
async def publish(self, value: Any) -> None:
|
|
40
|
+
"""Publish a new item."""
|
|
41
|
+
async with self._condition:
|
|
42
|
+
if self._closed:
|
|
43
|
+
raise StreamClosed
|
|
44
|
+
|
|
45
|
+
self._buffer.append(
|
|
46
|
+
_Item(
|
|
47
|
+
sequence=self._next_sequence,
|
|
48
|
+
value=value,
|
|
49
|
+
))
|
|
50
|
+
self._next_sequence += 1
|
|
51
|
+
|
|
52
|
+
self._condition.notify_all()
|
|
53
|
+
|
|
54
|
+
async def close(self) -> None:
|
|
55
|
+
async with self._condition:
|
|
56
|
+
self._closed = True
|
|
57
|
+
self._condition.notify_all()
|
|
58
|
+
|
|
59
|
+
def subscribe(self, *, window_size: int, step: int):
|
|
60
|
+
"""Subscribe to the stream with a sliding window of `window_size` items advancing by `step` items.
|
|
61
|
+
Returns:
|
|
62
|
+
Buffered_Stream_Reader
|
|
63
|
+
"""
|
|
64
|
+
if window_size <= 0:
|
|
65
|
+
raise ValueError("window_size must be > 0")
|
|
66
|
+
|
|
67
|
+
if step <= 0:
|
|
68
|
+
raise ValueError("step must be > 0")
|
|
69
|
+
|
|
70
|
+
if window_size > self.buffer_size:
|
|
71
|
+
raise ValueError("window_size cannot be greater than buffer_size")
|
|
72
|
+
|
|
73
|
+
return Buffered_Stream_Reader(
|
|
74
|
+
stream=self,
|
|
75
|
+
window_size=window_size,
|
|
76
|
+
step=step,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def _get_window(self, *, start_sequence: int,
|
|
80
|
+
window_size: int) -> list[Any]:
|
|
81
|
+
|
|
82
|
+
end_sequence = start_sequence + window_size
|
|
83
|
+
|
|
84
|
+
async with self._condition:
|
|
85
|
+
|
|
86
|
+
def ready() -> bool:
|
|
87
|
+
if self._closed:
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
if not self._buffer:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
latest = self._buffer[-1].sequence
|
|
94
|
+
|
|
95
|
+
return latest >= end_sequence - 1
|
|
96
|
+
|
|
97
|
+
await self._condition.wait_for(ready)
|
|
98
|
+
|
|
99
|
+
if not self._buffer:
|
|
100
|
+
raise StreamClosed
|
|
101
|
+
|
|
102
|
+
oldest = self._buffer[0].sequence
|
|
103
|
+
latest = self._buffer[-1].sequence
|
|
104
|
+
|
|
105
|
+
if start_sequence < oldest:
|
|
106
|
+
raise StreamOverrun(
|
|
107
|
+
f"reader needs sequence {start_sequence}, "
|
|
108
|
+
f"but oldest available sequence is {oldest}")
|
|
109
|
+
|
|
110
|
+
if end_sequence - 1 > latest:
|
|
111
|
+
raise StreamClosed
|
|
112
|
+
|
|
113
|
+
items = list(self._buffer)
|
|
114
|
+
|
|
115
|
+
return [
|
|
116
|
+
item.value for item in items
|
|
117
|
+
if start_sequence <= item.sequence < end_sequence
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Buffered_Stream_Reader:
|
|
122
|
+
|
|
123
|
+
def __init__(self, *, stream: Buffered_Stream, window_size: int,
|
|
124
|
+
step: int) -> None:
|
|
125
|
+
self._stream = stream
|
|
126
|
+
self._next_sequence = stream._next_sequence
|
|
127
|
+
self.window_size = window_size
|
|
128
|
+
self.step = step
|
|
129
|
+
|
|
130
|
+
async def get(self) -> list[Any]:
|
|
131
|
+
"""Attend la prochaine fenêtre et la retourne."""
|
|
132
|
+
start = self._next_sequence
|
|
133
|
+
|
|
134
|
+
window = await self._stream._get_window(
|
|
135
|
+
start_sequence=start,
|
|
136
|
+
window_size=self.window_size,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if len(window) != self.window_size:
|
|
140
|
+
raise RuntimeError(
|
|
141
|
+
f"internal error: expected {self.window_size} items, "
|
|
142
|
+
f"got {len(window)}")
|
|
143
|
+
|
|
144
|
+
self._next_sequence += self.step
|
|
145
|
+
|
|
146
|
+
return window
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2024 sferriol <s.ferriol@ip2i.in2p3.fr>
|
|
4
|
+
import asyncio
|
|
5
|
+
import multiprocessing
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
from sferriol.python.asyncio_ import *
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Test__Buffered_Stream(unittest.IsolatedAsyncioTestCase):
|
|
16
|
+
|
|
17
|
+
async def _producer(self, stream, n_values):
|
|
18
|
+
for i in range(n_values):
|
|
19
|
+
await stream.publish(i)
|
|
20
|
+
await asyncio.sleep(0)
|
|
21
|
+
await stream.close()
|
|
22
|
+
|
|
23
|
+
async def _consumer(self, name, stream, window_size, step):
|
|
24
|
+
self.consumer_windows[name] = list()
|
|
25
|
+
reader = stream.subscribe(
|
|
26
|
+
window_size=window_size,
|
|
27
|
+
step=step,
|
|
28
|
+
)
|
|
29
|
+
stop = False
|
|
30
|
+
while not stop:
|
|
31
|
+
try:
|
|
32
|
+
window = await reader.get()
|
|
33
|
+
self.consumer_windows[name].append(window)
|
|
34
|
+
except:
|
|
35
|
+
stop = True
|
|
36
|
+
|
|
37
|
+
async def test(self):
|
|
38
|
+
np_stream = Buffered_Stream(buffer_size=70)
|
|
39
|
+
self.consumer_windows = dict()
|
|
40
|
+
await asyncio.gather(
|
|
41
|
+
self._consumer("overlap", np_stream, window_size=5, step=2),
|
|
42
|
+
self._consumer("no_overlap", np_stream, window_size=5, step=5),
|
|
43
|
+
self._producer(np_stream, n_values=20),
|
|
44
|
+
)
|
|
45
|
+
self.assertEqual(len(self.consumer_windows["overlap"]), 8)
|
|
46
|
+
self.assertEqual(len(self.consumer_windows["no_overlap"]), 4)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Test__Run_In_Shell(unittest.IsolatedAsyncioTestCase):
|
|
50
|
+
|
|
51
|
+
async def test__stdout(self):
|
|
52
|
+
ret, stdout, stderr = await run_shell_cmd("echo toto")
|
|
53
|
+
self.assertEqual(ret, 0)
|
|
54
|
+
self.assertEqual(stdout, "toto")
|
|
55
|
+
self.assertEqual(stderr, "")
|
|
56
|
+
|
|
57
|
+
async def test__stderr(self):
|
|
58
|
+
ret, stdout, stderr = await run_shell_cmd("man toto")
|
|
59
|
+
self.assertNotEqual(ret, 0)
|
|
60
|
+
self.assertEqual(stdout, "")
|
|
61
|
+
self.assertNotEqual(stderr, "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _worker():
|
|
65
|
+
import asyncio
|
|
66
|
+
asyncio.run(wait_sigint_signal())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Test__Wait_Sigint_Signal(unittest.IsolatedAsyncioTestCase):
|
|
70
|
+
|
|
71
|
+
async def test(self):
|
|
72
|
+
p = multiprocessing.Process(target=_worker)
|
|
73
|
+
p.start()
|
|
74
|
+
await asyncio.sleep(1)
|
|
75
|
+
os.kill(p.pid, signal.SIGINT)
|
|
76
|
+
await asyncio.to_thread(p.join, 1)
|
|
77
|
+
self.assertFalse(p.is_alive())
|
|
78
|
+
self.assertEqual(p.exitcode, 0)
|
|
@@ -2,13 +2,14 @@ Licence_CeCILL-B_V1-en.txt
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
sferriol/python/__init__.py
|
|
5
|
-
sferriol/python/asyncio.py
|
|
6
5
|
sferriol/python/env.py
|
|
7
6
|
sferriol/python/json.py
|
|
8
7
|
sferriol/python/net.py
|
|
9
8
|
sferriol/python/object.py
|
|
10
9
|
sferriol/python/os_.py
|
|
11
10
|
sferriol/python/pkg.py
|
|
11
|
+
sferriol/python/asyncio_/__init__.py
|
|
12
|
+
sferriol/python/asyncio_/stream.py
|
|
12
13
|
sferriol/python/dictionary/__init__.py
|
|
13
14
|
sferriol/python/test/__init__.py
|
|
14
15
|
sferriol/python/test/test__asyncio.py
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# Copyright 2024 sferriol <s.ferriol@ip2i.in2p3.fr>
|
|
4
|
-
import asyncio
|
|
5
|
-
import multiprocessing
|
|
6
|
-
import os
|
|
7
|
-
import signal
|
|
8
|
-
import tempfile
|
|
9
|
-
import time
|
|
10
|
-
import unittest
|
|
11
|
-
|
|
12
|
-
from sferriol.python.asyncio import *
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Test__Run_In_Shell(unittest.IsolatedAsyncioTestCase):
|
|
16
|
-
|
|
17
|
-
async def test__stdout(self):
|
|
18
|
-
ret, stdout, stderr = await run_shell_cmd("echo toto")
|
|
19
|
-
self.assertEqual(ret, 0)
|
|
20
|
-
self.assertEqual(stdout, "toto")
|
|
21
|
-
self.assertEqual(stderr, "")
|
|
22
|
-
|
|
23
|
-
async def test__stderr(self):
|
|
24
|
-
ret, stdout, stderr = await run_shell_cmd("man toto")
|
|
25
|
-
self.assertNotEqual(ret, 0)
|
|
26
|
-
self.assertEqual(stdout, "")
|
|
27
|
-
self.assertNotEqual(stderr, "")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _worker():
|
|
31
|
-
import asyncio
|
|
32
|
-
asyncio.run(wait_sigint_signal())
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Test__Wait_Sigint_Signal(unittest.IsolatedAsyncioTestCase):
|
|
36
|
-
|
|
37
|
-
async def test(self):
|
|
38
|
-
p = multiprocessing.Process(target=_worker)
|
|
39
|
-
p.start()
|
|
40
|
-
await asyncio.sleep(1)
|
|
41
|
-
os.kill(p.pid, signal.SIGINT)
|
|
42
|
-
await asyncio.to_thread(p.join, 1)
|
|
43
|
-
self.assertFalse(p.is_alive())
|
|
44
|
-
self.assertEqual(p.exitcode, 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|