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.
Files changed (34) hide show
  1. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/PKG-INFO +1 -1
  2. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/__init__.py +1 -0
  3. sferriol_python-0.9.4/sferriol/python/asyncio.py → sferriol_python-0.10.1/sferriol/python/asyncio_/__init__.py +7 -5
  4. sferriol_python-0.10.1/sferriol/python/asyncio_/stream.py +146 -0
  5. sferriol_python-0.10.1/sferriol/python/test/test__asyncio.py +78 -0
  6. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/PKG-INFO +1 -1
  7. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/SOURCES.txt +2 -1
  8. sferriol_python-0.9.4/sferriol/python/test/test__asyncio.py +0 -44
  9. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/Licence_CeCILL-B_V1-en.txt +0 -0
  10. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/README.md +0 -0
  11. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/pyproject.toml +0 -0
  12. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/setup.cfg +0 -0
  13. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/dictionary/__init__.py +0 -0
  14. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/env.py +0 -0
  15. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/json.py +0 -0
  16. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/net.py +0 -0
  17. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/object.py +0 -0
  18. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/os_.py +0 -0
  19. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/pkg.py +0 -0
  20. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/__init__.py +0 -0
  21. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/pkg/__init__.py +0 -0
  22. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/pkg/toto.py +0 -0
  23. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/pkg/tutu.py +0 -0
  24. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__dictionary.py +0 -0
  25. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__env.py +0 -0
  26. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__json.py +0 -0
  27. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__module.py +0 -0
  28. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__net.py +0 -0
  29. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__object.py +0 -0
  30. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__os.py +0 -0
  31. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__pkg.py +0 -0
  32. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol/python/test/test__python.py +0 -0
  33. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/dependency_links.txt +0 -0
  34. {sferriol_python-0.9.4 → sferriol_python-0.10.1}/sferriol_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sferriol-python
3
- Version: 0.9.4
3
+ Version: 0.10.1
4
4
  Summary: Python utilities
5
5
  Author-email: Sylvain Ferriol <sylvain.ferriol@cnrs.fr>
6
6
  License:
@@ -4,6 +4,7 @@ import time
4
4
  import types
5
5
  from typing import Any, Callable
6
6
 
7
+ import sferriol.python.asyncio_ as asyncio
7
8
  import sferriol.python.os_ as os
8
9
 
9
10
 
@@ -1,6 +1,8 @@
1
- import asyncio as _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 _asyncio.create_subprocess_shell(
12
- cmd, stdout=_asyncio.subprocess.PIPE, stderr=_asyncio.subprocess.PIPE)
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 = _asyncio.Event()
22
- loop = _asyncio.get_running_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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sferriol-python
3
- Version: 0.9.4
3
+ Version: 0.10.1
4
4
  Summary: Python utilities
5
5
  Author-email: Sylvain Ferriol <sylvain.ferriol@cnrs.fr>
6
6
  License:
@@ -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)