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 ADDED
@@ -0,0 +1,7 @@
1
+ from .task import Task
2
+ from .channel import Channel
3
+ from .buffer import Buffer
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["Task", "Channel", "Buffer", "__version__"]
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
@@ -0,0 +1,10 @@
1
+ # Configures global logging
2
+ import logging
3
+
4
+ logging.basicConfig(
5
+ level=logging.INFO,
6
+ format="%(asctime)s | %(levelname)s | %(message)s",
7
+ handlers=[
8
+ logging.StreamHandler()
9
+ ]
10
+ )
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
+ ![Python](https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12-3776AB?style=for-the-badge&logo=python&logoColor=white)
26
+ ![Versão](https://img.shields.io/badge/vers%C3%A3o-0.1.0-2ea44f?style=for-the-badge)
27
+ ![Status](https://img.shields.io/badge/status-alpha-orange?style=for-the-badge)
28
+ ![Concorrência](https://img.shields.io/badge/concorr%C3%AAncia-loky-6f42c1?style=for-the-badge)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ PyChannel