PyChannel 0.1.0__py3-none-any.whl
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.
- PyChannel/__init__.py +7 -0
- PyChannel/buffer.py +68 -0
- PyChannel/channel.py +39 -0
- PyChannel/global_logs.py +10 -0
- PyChannel/task.py +69 -0
- pychannel-0.1.0.dist-info/METADATA +233 -0
- pychannel-0.1.0.dist-info/RECORD +9 -0
- pychannel-0.1.0.dist-info/WHEEL +5 -0
- pychannel-0.1.0.dist-info/top_level.txt +1 -0
PyChannel/__init__.py
ADDED
PyChannel/buffer.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from threading import RLock
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import PyChannel.global_logs as global_logs
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Buffer:
|
|
12
|
+
def __init__(self, capacity: int, timeout: int, name: str = None) -> None:
|
|
13
|
+
self.cap = capacity
|
|
14
|
+
self.time = timeout
|
|
15
|
+
self.queue = []
|
|
16
|
+
self.name = name if name is not None else "Buffer"
|
|
17
|
+
self.time_init = None
|
|
18
|
+
self.lock = RLock()
|
|
19
|
+
|
|
20
|
+
#Envia o dado pra queue do buffer
|
|
21
|
+
def sender(self, data: Any) -> None:
|
|
22
|
+
with self.lock:
|
|
23
|
+
if len(self.queue) == 0:
|
|
24
|
+
self.time_init = time.monotonic()
|
|
25
|
+
|
|
26
|
+
if len(self.queue) < self.cap:
|
|
27
|
+
logger.info(f"Adding data to {self.name}...")
|
|
28
|
+
self.queue.append(data)
|
|
29
|
+
logger.info("Added!!")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
logger.warning(f"{self.name} exceeded the limit!!!")
|
|
33
|
+
|
|
34
|
+
#Retorna a queue
|
|
35
|
+
def get(self) -> list:
|
|
36
|
+
with self.lock:
|
|
37
|
+
return self.queue
|
|
38
|
+
|
|
39
|
+
#Retorna o status do buffer, sele ele tiver excedidio o tempo, ou atingido o limite, ai retorna True
|
|
40
|
+
def status(self) -> bool:
|
|
41
|
+
with self.lock:
|
|
42
|
+
if self.time_init is None:
|
|
43
|
+
logger.warning(
|
|
44
|
+
f"It is not yet possible to check the status of {self.name}, "
|
|
45
|
+
"because the queue has not received data to start the timer!!"
|
|
46
|
+
)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
if time.monotonic() - self.time_init >= self.time:
|
|
50
|
+
logger.info(f"{self.name} exceeded the execution time limit!!")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
if len(self.queue) >= self.cap:
|
|
54
|
+
logger.info(f"{self.name} exceeded the storage limit!!")
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
#Reseta a queue do buffer
|
|
61
|
+
def reset(self) -> None:
|
|
62
|
+
logger.info(f"Resetting {self.name}...")
|
|
63
|
+
|
|
64
|
+
with self.lock:
|
|
65
|
+
self.queue.clear()
|
|
66
|
+
self.time_init = None
|
|
67
|
+
|
|
68
|
+
logger.info("Reset!!")
|
PyChannel/channel.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#Pega a configuração global de logs
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import PyChannel.global_logs as global_logs
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from multiprocessing import Queue
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
#Cria o canal para pegar os resultados das tasks
|
|
13
|
+
class Channel:
|
|
14
|
+
def __init__(self, name: str = None, limit: int = None) -> None:
|
|
15
|
+
self.name = name if name is not None else "Channel"
|
|
16
|
+
logging.info(f"{self.name} created!!")
|
|
17
|
+
self.queue = Queue() if limit is None else Queue(maxsize=limit)
|
|
18
|
+
|
|
19
|
+
#Envia resultado pra queue
|
|
20
|
+
def send(self, result: Any) -> None:
|
|
21
|
+
logger.info(f"Sending result to {self.name if self.name is not None else 'Channel'}...")
|
|
22
|
+
self.queue.put(result)
|
|
23
|
+
logger.info("Sent!!!")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
#Pega o resultado da queue, e se loop for True, ele vai retornar o resultado da queue só quando não for false
|
|
27
|
+
def get(self, loop:bool = None) -> Any:
|
|
28
|
+
logger.info(f"Waiting for result from {self.name if self.name is not None else 'Channel'}...")
|
|
29
|
+
if loop:
|
|
30
|
+
logger.info(f"Waiting for the true result...")
|
|
31
|
+
while True:
|
|
32
|
+
|
|
33
|
+
result = self.queue.get()
|
|
34
|
+
if result:
|
|
35
|
+
break
|
|
36
|
+
else:
|
|
37
|
+
result = self.queue.get()
|
|
38
|
+
logger.info("Result found!!!")
|
|
39
|
+
return result
|
PyChannel/global_logs.py
ADDED
PyChannel/task.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures import Future
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import PyChannel.global_logs as global_logs
|
|
7
|
+
from loky import get_reusable_executor
|
|
8
|
+
|
|
9
|
+
from PyChannel.buffer import Buffer
|
|
10
|
+
from PyChannel.channel import Channel
|
|
11
|
+
|
|
12
|
+
#Pega a configuração de logs
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
#Cria tasks
|
|
16
|
+
class Task:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
max_workers: int,
|
|
20
|
+
name: str = None,
|
|
21
|
+
channel: Channel = None,
|
|
22
|
+
buffer: Buffer = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.name = name if name is not None else "task"
|
|
25
|
+
self.ch = channel
|
|
26
|
+
self.buff = buffer
|
|
27
|
+
self.process = get_reusable_executor(max_workers=max_workers)
|
|
28
|
+
|
|
29
|
+
def _finished(self, future: Future[Any]) -> None:
|
|
30
|
+
try:
|
|
31
|
+
result = future.result()
|
|
32
|
+
except Exception as error:
|
|
33
|
+
if self.ch is not None:
|
|
34
|
+
self.ch.send(result=error)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if self.buff is None:
|
|
38
|
+
if self.ch is not None:
|
|
39
|
+
self.ch.send(result=result)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
output: Any = False
|
|
43
|
+
|
|
44
|
+
with self.buff.lock:
|
|
45
|
+
self.buff.sender(data=result)
|
|
46
|
+
|
|
47
|
+
if self.buff.status():
|
|
48
|
+
output = self.buff.get().copy()
|
|
49
|
+
self.buff.reset()
|
|
50
|
+
|
|
51
|
+
if self.ch is not None:
|
|
52
|
+
self.ch.send(result=output)
|
|
53
|
+
|
|
54
|
+
def run(self):
|
|
55
|
+
logger.info(f"Starting {self.name}...")
|
|
56
|
+
|
|
57
|
+
def decorator(func):
|
|
58
|
+
@wraps(func)
|
|
59
|
+
def wrapper(*args, **kwargs):
|
|
60
|
+
future = self.process.submit(func, *args, **kwargs)
|
|
61
|
+
|
|
62
|
+
if self.ch is not None or self.buff is not None:
|
|
63
|
+
future.add_done_callback(self._finished)
|
|
64
|
+
|
|
65
|
+
return future
|
|
66
|
+
|
|
67
|
+
return wrapper
|
|
68
|
+
|
|
69
|
+
return decorator
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyChannel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python concurrency library inspired by Go's goroutines and channels
|
|
5
|
+
Author: Brayan
|
|
6
|
+
Keywords: concurrency,channels,goroutines,parallelism,multiprocessing
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: <3.13,>=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: loky<4,>=3.5.6
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
21
|
+
Requires-Dist: twine>=6.1; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# PyChannel
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
**PyChannel** é uma biblioteca de concorrência inspirada nas goroutines e nos channels da linguagem Go. Sua API é composta por três módulos principais: `Task`, `Channel` e `Buffer`.
|
|
31
|
+
|
|
32
|
+
> A PyChannel utiliza processos gerenciados pelo `loky`. Ela é inspirada nos conceitos do Go, mas não implementa goroutines reais.
|
|
33
|
+
|
|
34
|
+
## Fluxo de execução
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
flowchart LR
|
|
38
|
+
A["Função decorada"] --> B["Task"]
|
|
39
|
+
B --> C["Worker do loky"]
|
|
40
|
+
C --> D["Future"]
|
|
41
|
+
D --> E{"Buffer configurado?"}
|
|
42
|
+
E -- "Não" --> F["Channel recebe o resultado"]
|
|
43
|
+
E -- "Sim, acumulando" --> G["Channel recebe False"]
|
|
44
|
+
E -- "Sim, lote pronto" --> H["Channel recebe a lista"]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A `Task` envia a função para um worker. O resultado retorna por um `Future` e, quando configurado, também é encaminhado ao `Channel`. Se a `Task` possuir um `Buffer`, os resultados são acumulados antes da liberação do lote.
|
|
48
|
+
|
|
49
|
+
## `Task`
|
|
50
|
+
|
|
51
|
+
A classe `Task` transforma uma função comum em uma tarefa executada por processos.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
Task(
|
|
55
|
+
max_workers,
|
|
56
|
+
name=None,
|
|
57
|
+
channel=None,
|
|
58
|
+
buffer=None,
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Parâmetro | Função |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `max_workers` | Define a quantidade máxima de processos disponíveis no pool. |
|
|
65
|
+
| `name` | Define o nome da tarefa exibido nos logs. |
|
|
66
|
+
| `channel` | Recebe os resultados e as exceções das execuções. |
|
|
67
|
+
| `buffer` | Ativa o agrupamento automático dos resultados. |
|
|
68
|
+
|
|
69
|
+
### Decorando uma função
|
|
70
|
+
|
|
71
|
+
O método `run()` retorna o decorator responsável por enviar cada chamada ao executor:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from PyChannel import Channel, Task
|
|
75
|
+
|
|
76
|
+
channel = Channel(name="Resultados")
|
|
77
|
+
task = Task(max_workers=4, name="Cálculos", channel=channel)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@task.run()
|
|
81
|
+
def quadrado(numero):
|
|
82
|
+
return numero * numero
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
future = quadrado(10)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A chamada não devolve o resultado diretamente. Ela devolve um `Future`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
resultado = future.result() # 100
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Como existe um `Channel`, o mesmo resultado também pode ser consumido por ele:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
resultado = channel.get() # 100
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Comportamento da Task
|
|
101
|
+
|
|
102
|
+
| Configuração | Comportamento |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| Sem `Channel` e sem `Buffer` | O resultado fica disponível somente no `Future`. |
|
|
105
|
+
| Com `Channel` | Cada resultado é enviado ao Channel. |
|
|
106
|
+
| Com `Channel` e `Buffer` | O Channel recebe `False` durante o acúmulo e uma lista quando o lote fica pronto. |
|
|
107
|
+
| Com `Buffer`, sem `Channel` | O Buffer é atualizado e o resultado individual continua disponível no `Future`. |
|
|
108
|
+
|
|
109
|
+
Se a função gerar uma exceção, ela permanece armazenada no `Future`. Quando existe um Channel, o objeto da exceção também é enviado para ele.
|
|
110
|
+
|
|
111
|
+
## `Channel`
|
|
112
|
+
|
|
113
|
+
O `Channel` transporta valores entre produtores e consumidores por meio de uma `multiprocessing.Queue`.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
Channel(name=None, limit=None)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Parâmetro | Função |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `name` | Define o nome utilizado nos logs. |
|
|
122
|
+
| `limit` | Define a quantidade máxima de valores pendentes. `None` cria uma fila sem limite explícito. |
|
|
123
|
+
|
|
124
|
+
### Enviando e recebendo valores
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from PyChannel import Channel
|
|
128
|
+
|
|
129
|
+
channel = Channel(name="Eventos")
|
|
130
|
+
|
|
131
|
+
channel.send("processado")
|
|
132
|
+
resultado = channel.get()
|
|
133
|
+
|
|
134
|
+
print(resultado) # processado
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`send()` bloqueia se um Channel limitado estiver cheio. `get()` bloqueia enquanto não houver nenhum valor disponível.
|
|
138
|
+
|
|
139
|
+
### Ignorando resultados falsos
|
|
140
|
+
|
|
141
|
+
O método `get(loop=True)` continua consumindo a fila até encontrar um valor verdadeiro:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
resultado = channel.get(loop=True)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Esse modo é útil com o Buffer automático, pois ignora os valores `False` enviados durante o acúmulo e retorna diretamente o lote:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
somar()
|
|
151
|
+
somar()
|
|
152
|
+
|
|
153
|
+
lote = channel.get(loop=True)
|
|
154
|
+
print(lote) # [2, 2]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
> `loop=True` ignora qualquer valor considerado falso pelo Python, incluindo `False`, `None`, `0`, `""` e coleções vazias.
|
|
158
|
+
|
|
159
|
+
## `Buffer`
|
|
160
|
+
|
|
161
|
+
O `Buffer` acumula valores temporariamente até atingir sua capacidade ou seu limite de tempo.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
Buffer(capacity, timeout, name=None)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Parâmetro | Função |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `capacity` | Quantidade máxima de valores acumulados. |
|
|
170
|
+
| `timeout` | Tempo máximo, em segundos, contado a partir do primeiro valor. |
|
|
171
|
+
| `name` | Define o nome utilizado nos logs. |
|
|
172
|
+
|
|
173
|
+
O acesso interno é protegido por um `RLock`, permitindo chamadas concorrentes aos seus métodos.
|
|
174
|
+
|
|
175
|
+
### Uso manual
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from PyChannel import Buffer
|
|
179
|
+
|
|
180
|
+
buffer = Buffer(capacity=3, timeout=10, name="Lote")
|
|
181
|
+
|
|
182
|
+
buffer.sender("A")
|
|
183
|
+
buffer.sender("B")
|
|
184
|
+
|
|
185
|
+
print(buffer.status()) # False
|
|
186
|
+
|
|
187
|
+
buffer.sender("C")
|
|
188
|
+
|
|
189
|
+
if buffer.status():
|
|
190
|
+
lote = buffer.get().copy()
|
|
191
|
+
buffer.reset()
|
|
192
|
+
print(lote) # ["A", "B", "C"]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Métodos
|
|
196
|
+
|
|
197
|
+
| Método | Comportamento |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `sender(data)` | Adiciona um valor se ainda houver capacidade. O contador de tempo começa no primeiro valor. |
|
|
200
|
+
| `get()` | Retorna a lista atualmente armazenada. |
|
|
201
|
+
| `status()` | Retorna `True` quando a capacidade ou o timeout é atingido. |
|
|
202
|
+
| `reset()` | Limpa a lista e reinicia o controle de tempo. |
|
|
203
|
+
|
|
204
|
+
O timeout não cria uma tarefa em segundo plano. Ele é avaliado quando `status()` é chamado ou quando a `Task` processa um novo resultado.
|
|
205
|
+
|
|
206
|
+
## Integração dos módulos
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from PyChannel import Buffer, Channel, Task
|
|
210
|
+
|
|
211
|
+
buffer = Buffer(capacity=2, timeout=10, name="Resultados")
|
|
212
|
+
channel = Channel(name="Saída")
|
|
213
|
+
task = Task(
|
|
214
|
+
max_workers=2,
|
|
215
|
+
name="Soma",
|
|
216
|
+
channel=channel,
|
|
217
|
+
buffer=buffer,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@task.run()
|
|
222
|
+
def somar(a, b):
|
|
223
|
+
return a + b
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
somar(1, 1)
|
|
227
|
+
somar(2, 2)
|
|
228
|
+
|
|
229
|
+
lote = channel.get(loop=True)
|
|
230
|
+
print(lote) # A ordem pode ser [2, 4] ou [4, 2]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Os workers executam de forma independente. Por isso, os resultados chegam ao Buffer e ao Channel na ordem de conclusão, não necessariamente na ordem de envio.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
PyChannel/__init__.py,sha256=zdL_R3Zzt3pS92qvOTiEfmgGYKyEzajg39W03-uUl4o,165
|
|
2
|
+
PyChannel/buffer.py,sha256=daUlFtYl3rXwQSULF_QabHSMZ7HXAHaYXcieS5G86V8,2105
|
|
3
|
+
PyChannel/channel.py,sha256=fE6eWnVkYAa33j1yjtOWVAtN8Rsh9AEBA5F-_A-l_KY,1381
|
|
4
|
+
PyChannel/global_logs.py,sha256=cHXuh54g-Poog35uZemFNC83Ive8vTSUqX3rwPl_qgo,210
|
|
5
|
+
PyChannel/task.py,sha256=DtYtbnBOQGWoV7q6h4nuDzqQbBSRRODi6tivIuqnfSI,1874
|
|
6
|
+
pychannel-0.1.0.dist-info/METADATA,sha256=RtbzeAEQWJMIIM28KqB4dCxblOX9_i6Ms8fBkYl24zs,7225
|
|
7
|
+
pychannel-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
pychannel-0.1.0.dist-info/top_level.txt,sha256=P7oj5z4KKaGJmihfNMeboaBWP1aQj6ppTfdvFpPQOzI,10
|
|
9
|
+
pychannel-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyChannel
|