dataloom-engine 0.3.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.
- dataloom_engine/__init__.py +43 -0
- dataloom_engine/config.py +52 -0
- dataloom_engine/exceptions.py +25 -0
- dataloom_engine/hooks.py +50 -0
- dataloom_engine/logs.py +28 -0
- dataloom_engine/loom.py +167 -0
- dataloom_engine/processors.py +43 -0
- dataloom_engine/py.typed +0 -0
- dataloom_engine/sinks.py +172 -0
- dataloom_engine/sources.py +56 -0
- dataloom_engine/types.py +18 -0
- dataloom_engine/weaver.py +84 -0
- dataloom_engine-0.3.0.dist-info/METADATA +196 -0
- dataloom_engine-0.3.0.dist-info/RECORD +17 -0
- dataloom_engine-0.3.0.dist-info/WHEEL +5 -0
- dataloom_engine-0.3.0.dist-info/licenses/LICENSE +21 -0
- dataloom_engine-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dataloom_engine/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
DataLoom: Um motor de orquestração multi-thread leve e eficiente.
|
|
5
|
+
|
|
6
|
+
Este módulo expõe a API pública do pacote. Observe que classes internas
|
|
7
|
+
como 'Weaver' não são expostas propositalmente, mantendo a superfície
|
|
8
|
+
de uso limpa e segura para o consumidor.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataloom_engine.config import LoomConfig
|
|
12
|
+
from dataloom_engine.exceptions import ConfigurationError, LoomError, WeaverError
|
|
13
|
+
from dataloom_engine.hooks import LoomHooks
|
|
14
|
+
from dataloom_engine.logs import LoomLogs
|
|
15
|
+
from dataloom_engine.loom import Loom
|
|
16
|
+
from dataloom_engine.processors import Processor
|
|
17
|
+
from dataloom_engine.sinks import (
|
|
18
|
+
CallbackSink,
|
|
19
|
+
CsvFileSink,
|
|
20
|
+
JsonFileSink,
|
|
21
|
+
Sink,
|
|
22
|
+
ThreadedBufferedSink,
|
|
23
|
+
)
|
|
24
|
+
from dataloom_engine.sources import Source
|
|
25
|
+
from dataloom_engine.types import LoomState
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Loom",
|
|
29
|
+
"LoomConfig",
|
|
30
|
+
"LoomState",
|
|
31
|
+
"Processor",
|
|
32
|
+
"Sink",
|
|
33
|
+
"JsonFileSink",
|
|
34
|
+
"CsvFileSink",
|
|
35
|
+
"CallbackSink",
|
|
36
|
+
"ThreadedBufferedSink",
|
|
37
|
+
"Source",
|
|
38
|
+
"LoomHooks",
|
|
39
|
+
"LoomLogs",
|
|
40
|
+
"LoomError",
|
|
41
|
+
"WeaverError",
|
|
42
|
+
"ConfigurationError",
|
|
43
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# dataloom_engine/config.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Gerenciamento de configuração do orquestrador.
|
|
5
|
+
Centraliza parâmetros operacionais como diretórios e tamanhos de lote.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Union
|
|
11
|
+
|
|
12
|
+
from dataloom_engine.exceptions import ConfigurationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LoomConfig:
|
|
17
|
+
"""
|
|
18
|
+
Objeto de configuração principal do DataLoom.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
output_dir (Path): Diretório base onde os Sinks padrão salvarão dados.
|
|
22
|
+
Strings são convertidas para Path automaticamente.
|
|
23
|
+
batch_size (int): Quantidade de itens gerados por ciclo de processamento.
|
|
24
|
+
interval_seconds (float): Intervalo de tempo entre gerações de tarefas.
|
|
25
|
+
queue_maxsize (Optional[int]): Capacidade máxima da fila de tarefas
|
|
26
|
+
(backpressure). None usa o padrão do Loom (num_weavers * 4);
|
|
27
|
+
0 significa fila ilimitada.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ConfigurationError: se algum parâmetro for inválido.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
output_dir: Union[str, Path]
|
|
34
|
+
batch_size: int = 10
|
|
35
|
+
interval_seconds: float = 1.0
|
|
36
|
+
queue_maxsize: Optional[int] = None
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
self.output_dir = Path(self.output_dir)
|
|
40
|
+
|
|
41
|
+
if self.batch_size <= 0:
|
|
42
|
+
raise ConfigurationError(
|
|
43
|
+
f"batch_size deve ser maior que zero (recebido: {self.batch_size})."
|
|
44
|
+
)
|
|
45
|
+
if self.interval_seconds < 0:
|
|
46
|
+
raise ConfigurationError(
|
|
47
|
+
f"interval_seconds não pode ser negativo (recebido: {self.interval_seconds})."
|
|
48
|
+
)
|
|
49
|
+
if self.queue_maxsize is not None and self.queue_maxsize < 0:
|
|
50
|
+
raise ConfigurationError(
|
|
51
|
+
f"queue_maxsize não pode ser negativo (recebido: {self.queue_maxsize})."
|
|
52
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# dataloom_engine/exceptions.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Exceções personalizadas para o DataLoom.
|
|
5
|
+
Permite que consumidores capturem erros específicos da biblioteca
|
|
6
|
+
sem depender de exceções genéricas do Python.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoomError(Exception):
|
|
11
|
+
"""Exceção base para todos os erros do DataLoom."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigurationError(LoomError):
|
|
17
|
+
"""Lançado quando há problemas na validação do LoomConfig."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WeaverError(LoomError):
|
|
23
|
+
"""Lançado quando um Weaver falha de forma irrecuperável."""
|
|
24
|
+
|
|
25
|
+
pass
|
dataloom_engine/hooks.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# dataloom_engine/hooks.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Pontos de extensão para observabilidade (Hooks).
|
|
5
|
+
Permite injetar lógica de monitoramento sem acoplar ao core do engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoomHooks:
|
|
12
|
+
"""
|
|
13
|
+
Classe base para hooks do ciclo de vida.
|
|
14
|
+
Todos os métodos são opcionais e no-op por padrão: sobrescreva apenas
|
|
15
|
+
os pontos de interesse. (Deliberadamente não é um ABC — não há método
|
|
16
|
+
obrigatório a implementar.)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def on_start(self) -> None:
|
|
20
|
+
"""Chamado imediatamente antes dos Weavers serem iniciados."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def on_stop(self) -> None:
|
|
24
|
+
"""Chamado após o encerramento gracioso do orquestrador."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def on_error(self, error: Exception) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Chamado quando ocorre uma exceção no loop principal do Loom ou
|
|
30
|
+
durante o processamento de um lote em um Weaver (WeaverError).
|
|
31
|
+
|
|
32
|
+
Atenção: pode ser invocado a partir de múltiplas threads Weaver
|
|
33
|
+
simultaneamente — implementações devem ser thread-safe.
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def on_batch_processed(self, result: Dict[str, Any], duration_seconds: float) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Chamado após cada lote processado e entregue ao Sink com sucesso.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
result: O dicionário produzido pelo Processor para o lote.
|
|
43
|
+
duration_seconds: Tempo gasto em process() + send(), medido
|
|
44
|
+
com relógio monotônico.
|
|
45
|
+
|
|
46
|
+
Atenção: assim como on_error, é invocado a partir das threads
|
|
47
|
+
Weaver — implementações devem ser thread-safe e rápidas, pois
|
|
48
|
+
rodam no caminho quente do pipeline.
|
|
49
|
+
"""
|
|
50
|
+
pass
|
dataloom_engine/logs.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# dataloom_engine/logs.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Utilitários de configuração de logging para o DataLoom.
|
|
5
|
+
Fornece um namespace estático para configuração rápida.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoomLogs:
|
|
12
|
+
"""
|
|
13
|
+
Namespace utilitário para gerenciamento de logs do DataLoom.
|
|
14
|
+
Não deve ser instanciado, apenas usado estaticamente.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def setup(level: int = logging.INFO) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Configura o logging básico para stdout.
|
|
21
|
+
|
|
22
|
+
Uso:
|
|
23
|
+
LoomLogs.setup(level=logging.DEBUG)
|
|
24
|
+
"""
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
level=level,
|
|
27
|
+
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
|
28
|
+
)
|
dataloom_engine/loom.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# dataloom_engine/loom.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
O módulo Loom é o coração do motor de orquestração.
|
|
5
|
+
Define a classe principal responsável por gerenciar o ciclo de vida
|
|
6
|
+
das threads (Weavers) e a distribuição de tarefas.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
from typing import TYPE_CHECKING, Optional
|
|
12
|
+
|
|
13
|
+
from dataloom_engine.config import LoomConfig
|
|
14
|
+
from dataloom_engine.hooks import LoomHooks
|
|
15
|
+
from dataloom_engine.processors import Processor
|
|
16
|
+
from dataloom_engine.sinks import Sink
|
|
17
|
+
from dataloom_engine.types import LoomState
|
|
18
|
+
from dataloom_engine.weaver import STOP_SENTINEL, Weaver
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from dataloom_engine.sources import Source
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Loom:
|
|
25
|
+
"""
|
|
26
|
+
Orquestrador principal do DataLoom.
|
|
27
|
+
|
|
28
|
+
Exemplo de uso:
|
|
29
|
+
config = LoomConfig(...)
|
|
30
|
+
loom = Loom(config, processor, sink)
|
|
31
|
+
loom.start()
|
|
32
|
+
|
|
33
|
+
Também pode ser usado como context manager, garantindo stop()
|
|
34
|
+
mesmo se o bloco levantar exceção ou for interrompido:
|
|
35
|
+
with Loom(config, processor, sink) as loom:
|
|
36
|
+
loom.start()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
config: LoomConfig,
|
|
42
|
+
processor: Processor,
|
|
43
|
+
sink: Sink,
|
|
44
|
+
source: Optional["Source"] = None,
|
|
45
|
+
hooks: Optional[LoomHooks] = None,
|
|
46
|
+
num_weavers: int = 2,
|
|
47
|
+
):
|
|
48
|
+
self.config = config
|
|
49
|
+
self.processor = processor
|
|
50
|
+
self.sink = sink
|
|
51
|
+
|
|
52
|
+
# Default dependency injection if not provided
|
|
53
|
+
if source is None:
|
|
54
|
+
# Import tardio para evitar dependência circular no import do módulo
|
|
55
|
+
from dataloom_engine.sources import RandomNumPySource
|
|
56
|
+
|
|
57
|
+
source = RandomNumPySource(config)
|
|
58
|
+
self.source: "Source" = source
|
|
59
|
+
|
|
60
|
+
# Garante nova instância de hooks se não fornecida (evita estado global)
|
|
61
|
+
self.hooks = hooks or LoomHooks()
|
|
62
|
+
self.num_weavers = num_weavers
|
|
63
|
+
|
|
64
|
+
self.state = LoomState.PENDING
|
|
65
|
+
|
|
66
|
+
# Fila limitada (backpressure): se os Weavers não acompanharem o
|
|
67
|
+
# ritmo do Source, o produtor aguarda em vez de acumular memória.
|
|
68
|
+
# queue_maxsize=0 na config desliga o limite.
|
|
69
|
+
if config.queue_maxsize is not None:
|
|
70
|
+
maxsize = config.queue_maxsize
|
|
71
|
+
else:
|
|
72
|
+
maxsize = num_weavers * 4
|
|
73
|
+
self.task_queue: queue.Queue = queue.Queue(maxsize=maxsize)
|
|
74
|
+
|
|
75
|
+
self.stop_event = threading.Event()
|
|
76
|
+
self.weavers: list[Weaver] = []
|
|
77
|
+
self._stop_lock = threading.Lock()
|
|
78
|
+
self._stopped = False
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> "Loom":
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
84
|
+
# Garante limpeza completa ao sair do bloco with, inclusive em
|
|
85
|
+
# exceções (como KeyboardInterrupt) que escapem do start().
|
|
86
|
+
# stop() é idempotente, então não há custo se já foi chamado.
|
|
87
|
+
self.stop()
|
|
88
|
+
|
|
89
|
+
def start(self) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Inicializa os Weavers e começa o loop de geração de tarefas.
|
|
92
|
+
Este método bloqueia a execução até que ocorra erro ou parada.
|
|
93
|
+
"""
|
|
94
|
+
self.state = LoomState.RUNNING
|
|
95
|
+
self.hooks.on_start()
|
|
96
|
+
|
|
97
|
+
# Acorda os tecelões
|
|
98
|
+
for _ in range(self.num_weavers):
|
|
99
|
+
weaver = Weaver(
|
|
100
|
+
self.task_queue,
|
|
101
|
+
self.processor,
|
|
102
|
+
self.sink,
|
|
103
|
+
on_error=self.hooks.on_error,
|
|
104
|
+
on_batch_processed=self.hooks.on_batch_processed,
|
|
105
|
+
)
|
|
106
|
+
weaver.start()
|
|
107
|
+
self.weavers.append(weaver)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Consome do Source
|
|
111
|
+
for batch in self.source:
|
|
112
|
+
if self.stop_event.is_set():
|
|
113
|
+
break
|
|
114
|
+
self._enqueue(batch)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self.state = LoomState.FAILED
|
|
117
|
+
self.hooks.on_error(e)
|
|
118
|
+
raise
|
|
119
|
+
finally:
|
|
120
|
+
self.stop()
|
|
121
|
+
|
|
122
|
+
def _enqueue(self, batch) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Coloca um lote na fila sem bloquear indefinidamente: o timeout
|
|
125
|
+
permite reagir a um stop() disparado por outra thread.
|
|
126
|
+
"""
|
|
127
|
+
while not self.stop_event.is_set():
|
|
128
|
+
try:
|
|
129
|
+
self.task_queue.put(batch, timeout=0.1)
|
|
130
|
+
return
|
|
131
|
+
except queue.Full:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
def stop(self) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Sinaliza a parada de todos os componentes e aguarda limpeza.
|
|
137
|
+
Seguro para ser chamado múltiplas vezes ou dentro de blocos finally.
|
|
138
|
+
|
|
139
|
+
Os itens já enfileirados são processados antes do encerramento:
|
|
140
|
+
cada Weaver consome a fila até encontrar seu sentinela de parada.
|
|
141
|
+
"""
|
|
142
|
+
with self._stop_lock:
|
|
143
|
+
if self._stopped:
|
|
144
|
+
return
|
|
145
|
+
self._stopped = True
|
|
146
|
+
|
|
147
|
+
self.stop_event.set()
|
|
148
|
+
|
|
149
|
+
# Um sentinela por Weaver: cada thread drena a fila e encerra
|
|
150
|
+
# ao consumir o seu. Isso substitui o join() na fila, que podia
|
|
151
|
+
# travar para sempre se um Weaver morresse antes de esvaziá-la.
|
|
152
|
+
for _ in self.weavers:
|
|
153
|
+
self.task_queue.put(STOP_SENTINEL)
|
|
154
|
+
for weaver in self.weavers:
|
|
155
|
+
weaver.join()
|
|
156
|
+
|
|
157
|
+
# Não sobrescreve FAILED definido pelo start() em caso de erro
|
|
158
|
+
if self.state is LoomState.RUNNING:
|
|
159
|
+
self.state = LoomState.COMPLETED
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
self.sink.close()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# Não queremos que erro no close esconda outros erros, mas logamos
|
|
165
|
+
self.hooks.on_error(e)
|
|
166
|
+
|
|
167
|
+
self.hooks.on_stop()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dataloom_engine/processors.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Contratos de processamento de dados.
|
|
5
|
+
Define como os lotes (batches) são transformados antes de serem enviados ao Sink.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Processor(ABC):
|
|
15
|
+
"""Interface base para transformação de dados."""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def process(self, batch: np.ndarray) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Processa um lote de dados.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
batch: Array contendo os dados brutos (tamanho definido em LoomConfig).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dict[str, Any]: Dicionário com os resultados processados.
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StatisticsProcessor(Processor):
|
|
32
|
+
"""
|
|
33
|
+
Implementação de referência que calcula estatísticas básicas.
|
|
34
|
+
Útil para testes e validação inicial.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def process(self, batch: np.ndarray) -> Dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"mean": float(np.mean(batch)),
|
|
40
|
+
"std": float(np.std(batch)),
|
|
41
|
+
"min": float(np.min(batch)),
|
|
42
|
+
"max": float(np.max(batch)),
|
|
43
|
+
}
|
dataloom_engine/py.typed
ADDED
|
File without changes
|
dataloom_engine/sinks.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# dataloom_engine/sinks.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Contratos de saída de dados (Sinks).
|
|
5
|
+
Define como e onde os resultados processados são depositados.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import queue
|
|
12
|
+
import threading
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable, Dict, Optional
|
|
16
|
+
|
|
17
|
+
from dataloom_engine.exceptions import LoomError
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Sink(ABC):
|
|
23
|
+
"""Interface base para destinos de dados."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def send(self, result: Dict[str, Any]) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Envia o resultado para o destino final.
|
|
29
|
+
Implementações devem garantir thread-safety se acessarem recursos compartilhados.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def close(self) -> None: # noqa: B027 -- no-op deliberado: close é opcional
|
|
34
|
+
"""
|
|
35
|
+
Método de ciclo de vida chamado quando o Loom encerra.
|
|
36
|
+
Útil para fechar conexões, flushear buffers ou parar threads de background.
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class JsonFileSink(Sink):
|
|
42
|
+
"""
|
|
43
|
+
Sink padrão que escreve resultados em um arquivo JSON local.
|
|
44
|
+
Utiliza threading.Lock para garantir integridade na escrita concorrente.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, output_dir: Path):
|
|
48
|
+
self.output_dir = output_dir
|
|
49
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
# Lock garante que apenas um Weaver escreva no arquivo por vez
|
|
51
|
+
self._lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
def send(self, result: Dict[str, Any]) -> None:
|
|
54
|
+
filename = self.output_dir / "results.json"
|
|
55
|
+
|
|
56
|
+
with self._lock:
|
|
57
|
+
with open(filename, "a") as f:
|
|
58
|
+
json.dump(result, f)
|
|
59
|
+
f.write("\n")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CsvFileSink(Sink):
|
|
63
|
+
"""
|
|
64
|
+
Sink que escreve resultados em um arquivo CSV local.
|
|
65
|
+
|
|
66
|
+
O cabeçalho é definido pelas chaves do primeiro resultado recebido.
|
|
67
|
+
Nos resultados seguintes, chaves extras são ignoradas e chaves
|
|
68
|
+
ausentes ficam vazias. Utiliza threading.Lock para garantir
|
|
69
|
+
integridade na escrita concorrente.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, output_dir: Path, filename: str = "results.csv"):
|
|
73
|
+
self.output_dir = Path(output_dir)
|
|
74
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
self._path = self.output_dir / filename
|
|
76
|
+
self._lock = threading.Lock()
|
|
77
|
+
self._fieldnames: Optional[list] = None
|
|
78
|
+
|
|
79
|
+
def send(self, result: Dict[str, Any]) -> None:
|
|
80
|
+
with self._lock:
|
|
81
|
+
fieldnames = self._fieldnames
|
|
82
|
+
write_header = fieldnames is None
|
|
83
|
+
if fieldnames is None:
|
|
84
|
+
fieldnames = list(result.keys())
|
|
85
|
+
self._fieldnames = fieldnames
|
|
86
|
+
with open(self._path, "a", newline="") as f:
|
|
87
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
|
88
|
+
if write_header:
|
|
89
|
+
writer.writeheader()
|
|
90
|
+
writer.writerow(result)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CallbackSink(Sink):
|
|
94
|
+
"""
|
|
95
|
+
Sink que delega cada resultado a um callable fornecido pelo usuário.
|
|
96
|
+
Permite integrar o pipeline a qualquer destino (fila externa, banco,
|
|
97
|
+
métrica) sem precisar criar uma subclasse de Sink.
|
|
98
|
+
|
|
99
|
+
Atenção: o callable é invocado a partir das threads Weaver — deve
|
|
100
|
+
ser thread-safe.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
callback: Callable[[Dict[str, Any]], None],
|
|
106
|
+
on_close: Optional[Callable[[], None]] = None,
|
|
107
|
+
):
|
|
108
|
+
self.callback = callback
|
|
109
|
+
self.on_close = on_close
|
|
110
|
+
|
|
111
|
+
def send(self, result: Dict[str, Any]) -> None:
|
|
112
|
+
self.callback(result)
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
if self.on_close is not None:
|
|
116
|
+
self.on_close()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ThreadedBufferedSink(Sink):
|
|
120
|
+
"""
|
|
121
|
+
Decorator que adiciona um buffer em memória e escrita assíncrona
|
|
122
|
+
para qualquer Sink existente.
|
|
123
|
+
|
|
124
|
+
O worker consome a fila até encontrar o sentinela de parada, o que
|
|
125
|
+
garante que todos os itens enviados antes do close() sejam entregues
|
|
126
|
+
ao sink alvo, sem janelas de corrida entre sinalização e drenagem.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
# Sentinela interno que instrui o worker a encerrar após drenar a fila
|
|
130
|
+
_STOP: Any = object()
|
|
131
|
+
|
|
132
|
+
def __init__(self, target_sink: Sink, buffer_size: int = 1000):
|
|
133
|
+
self.target = target_sink
|
|
134
|
+
self.queue: queue.Queue = queue.Queue(maxsize=buffer_size)
|
|
135
|
+
self._closed = False
|
|
136
|
+
self._close_lock = threading.Lock()
|
|
137
|
+
self.worker_thread = threading.Thread(target=self._worker, daemon=True)
|
|
138
|
+
self.worker_thread.start()
|
|
139
|
+
|
|
140
|
+
def send(self, result: Dict[str, Any]) -> None:
|
|
141
|
+
if self._closed:
|
|
142
|
+
raise LoomError("ThreadedBufferedSink já foi fechado; send() não é permitido.")
|
|
143
|
+
self.queue.put(result)
|
|
144
|
+
|
|
145
|
+
def _worker(self) -> None:
|
|
146
|
+
while True:
|
|
147
|
+
item = self.queue.get()
|
|
148
|
+
try:
|
|
149
|
+
if item is self._STOP:
|
|
150
|
+
return
|
|
151
|
+
self.target.send(item)
|
|
152
|
+
except Exception:
|
|
153
|
+
# O worker precisa sobreviver a falhas do sink alvo,
|
|
154
|
+
# senão a fila para de drenar e o close() trava.
|
|
155
|
+
logger.exception("Sink alvo falhou ao receber item; item descartado.")
|
|
156
|
+
finally:
|
|
157
|
+
self.queue.task_done()
|
|
158
|
+
|
|
159
|
+
def close(self) -> None:
|
|
160
|
+
# Idempotente: apenas a primeira chamada executa o fechamento
|
|
161
|
+
with self._close_lock:
|
|
162
|
+
if self._closed:
|
|
163
|
+
return
|
|
164
|
+
self._closed = True
|
|
165
|
+
|
|
166
|
+
# O sentinela entra atrás dos itens pendentes: o worker drena
|
|
167
|
+
# tudo antes de encerrar
|
|
168
|
+
self.queue.put(self._STOP)
|
|
169
|
+
self.worker_thread.join()
|
|
170
|
+
|
|
171
|
+
# Propaga o fechamento
|
|
172
|
+
self.target.close()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# dataloom_engine/sources.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Data Sources Module.
|
|
5
|
+
Defines the interface for generating or fetching data batches to be processed by the Loom.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any, Iterator
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from dataloom_engine.config import LoomConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Source(ABC):
|
|
18
|
+
"""
|
|
19
|
+
Abstract Base Class for data sources.
|
|
20
|
+
A Source must be iterable, yielding batches of data.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def __iter__(self) -> Iterator[Any]:
|
|
25
|
+
"""
|
|
26
|
+
Yields data batches.
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RandomNumPySource(Source):
|
|
32
|
+
"""
|
|
33
|
+
Standard source that generates random NumPy arrays.
|
|
34
|
+
Simulates a continuous data stream.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: LoomConfig, limit: int = -1):
|
|
38
|
+
"""
|
|
39
|
+
Args:
|
|
40
|
+
config: Configuration object containing batch_size and interval_seconds.
|
|
41
|
+
limit: Maximum number of batches to generate. -1 for infinite.
|
|
42
|
+
"""
|
|
43
|
+
self.config = config
|
|
44
|
+
self.limit = limit
|
|
45
|
+
|
|
46
|
+
def __iter__(self) -> Iterator[np.ndarray]:
|
|
47
|
+
count = 0
|
|
48
|
+
while self.limit == -1 or count < self.limit:
|
|
49
|
+
# Simulate generic production delay
|
|
50
|
+
time.sleep(self.config.interval_seconds)
|
|
51
|
+
|
|
52
|
+
# Generate batch
|
|
53
|
+
batch = np.random.rand(self.config.batch_size)
|
|
54
|
+
yield batch
|
|
55
|
+
|
|
56
|
+
count += 1
|
dataloom_engine/types.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# dataloom_engine/types.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Define os tipos fundamentais e estados do sistema DataLoom.
|
|
5
|
+
Estas definições são usadas transversalmente por vários módulos para
|
|
6
|
+
garantir consistência de estado.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LoomState(Enum):
|
|
13
|
+
"""Representa o estado atual do ciclo de vida da máquina Loom."""
|
|
14
|
+
|
|
15
|
+
PENDING = "pending"
|
|
16
|
+
RUNNING = "running"
|
|
17
|
+
COMPLETED = "completed"
|
|
18
|
+
FAILED = "failed"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# dataloom_engine/weaver.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Definição do Trabalhador (Weaver).
|
|
5
|
+
Thread dedicada que consome tarefas da fila, processa e envia ao Sink.
|
|
6
|
+
Este módulo é interno e não deve ser importado diretamente pelo usuário.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import queue
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from dataloom_engine.exceptions import WeaverError
|
|
16
|
+
from dataloom_engine.processors import Processor
|
|
17
|
+
from dataloom_engine.sinks import Sink
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Sentinela interno: instrui o Weaver a encerrar. O Loom enfileira um
|
|
22
|
+
# sentinela por Weaver após os dados, garantindo que a fila seja drenada
|
|
23
|
+
# por completo antes das threads morrerem.
|
|
24
|
+
STOP_SENTINEL: Any = object()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Weaver(threading.Thread):
|
|
28
|
+
"""
|
|
29
|
+
Agente de execução que roda em uma thread separada.
|
|
30
|
+
Orquestra o fluxo: Fila -> Processor -> Sink.
|
|
31
|
+
|
|
32
|
+
Erros de processamento não matam a thread: são convertidos em
|
|
33
|
+
WeaverError, logados e reportados via callback on_error (se fornecido).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
task_queue: queue.Queue,
|
|
39
|
+
processor: Processor,
|
|
40
|
+
sink: Sink,
|
|
41
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
42
|
+
on_batch_processed: Optional[Callable[[Dict[str, Any], float], None]] = None,
|
|
43
|
+
):
|
|
44
|
+
super().__init__(daemon=True)
|
|
45
|
+
self.task_queue = task_queue
|
|
46
|
+
self.processor = processor
|
|
47
|
+
self.sink = sink
|
|
48
|
+
self.on_error = on_error
|
|
49
|
+
self.on_batch_processed = on_batch_processed
|
|
50
|
+
|
|
51
|
+
def run(self) -> None:
|
|
52
|
+
while True:
|
|
53
|
+
batch = self.task_queue.get()
|
|
54
|
+
try:
|
|
55
|
+
if batch is STOP_SENTINEL:
|
|
56
|
+
return
|
|
57
|
+
self._process_batch(batch)
|
|
58
|
+
finally:
|
|
59
|
+
# Sinaliza que o item da fila foi tratado (sucesso ou falha)
|
|
60
|
+
self.task_queue.task_done()
|
|
61
|
+
|
|
62
|
+
def _process_batch(self, batch: Any) -> None:
|
|
63
|
+
try:
|
|
64
|
+
started = time.monotonic()
|
|
65
|
+
result = self.processor.process(batch)
|
|
66
|
+
self.sink.send(result)
|
|
67
|
+
duration = time.monotonic() - started
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
logger.exception("Weaver falhou ao processar um lote; o lote foi descartado.")
|
|
70
|
+
if self.on_error is not None:
|
|
71
|
+
error = WeaverError(f"Falha ao processar lote: {exc}")
|
|
72
|
+
error.__cause__ = exc
|
|
73
|
+
try:
|
|
74
|
+
self.on_error(error)
|
|
75
|
+
except Exception:
|
|
76
|
+
logger.exception("Callback on_error lançou uma exceção.")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Métrica só é emitida em sucesso; falhas passam pelo on_error
|
|
80
|
+
if self.on_batch_processed is not None:
|
|
81
|
+
try:
|
|
82
|
+
self.on_batch_processed(result, duration)
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("Callback on_batch_processed lançou uma exceção.")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataloom-engine
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: DataLoom: Um orquestrador de threads leve e eficiente para dados.
|
|
5
|
+
Author-email: Dioni Padilha <dionipdl@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dionipadilha/dataloom
|
|
8
|
+
Project-URL: Changelog, https://github.com/dionipadilha/dataloom/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/dionipadilha/dataloom/issues
|
|
10
|
+
Keywords: pipeline,data,threading,loom,concurrency
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.21.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# 🧵 DataLoom
|
|
34
|
+
|
|
35
|
+
> Um motor de orquestração multi-thread leve, eficiente e seguro para Python.
|
|
36
|
+
|
|
37
|
+
[](https://github.com/dionipadilha/dataloom/actions/workflows/ci.yml)  
|
|
38
|
+
|
|
39
|
+
<img width="2752" height="1536" alt="DataLoom: Um motor de orquestração multi-thread leve, eficiente e seguro para Python." src="https://github.com/user-attachments/assets/c1f3fc98-5eaf-4add-a38c-2717ae699cd5" />
|
|
40
|
+
|
|
41
|
+
O **DataLoom** é uma biblioteca projetada para processar fluxos de dados utilizando o padrão **Produtor-Consumidor** com múltiplas threads (Weavers). Ele abstrai a complexidade de filas (`Queues`), sincronização (`Locks`) e gerenciamento de ciclo de vida, permitindo que você foque apenas na lógica de transformação dos dados.
|
|
42
|
+
|
|
43
|
+
## Por que usar o DataLoom?
|
|
44
|
+
⚡ Performance: Processamento paralelo real para tarefas I/O bound.
|
|
45
|
+
🧩 Simplicidade: API intuitiva inspirada na metáfora de tecelagem.
|
|
46
|
+
🛡️ Segurança: Thread-safety garantido por design em todo o pipeline.
|
|
47
|
+
📦 Leve: Dependências mínimas, pronto para rodar em qualquer ambiente Python.
|
|
48
|
+
|
|
49
|
+
### Por que não usar só `ThreadPoolExecutor`?
|
|
50
|
+
|
|
51
|
+
Pergunta justa — a stdlib resolve o *paralelismo*, mas não o *pipeline*. O
|
|
52
|
+
`concurrent.futures` te dá um pool de threads; tudo ao redor fica por sua conta:
|
|
53
|
+
|
|
54
|
+
| Você precisa de... | Com a stdlib | Com o DataLoom |
|
|
55
|
+
| --------------------------------------------------- | ------------------------ | --------------------------------- |
|
|
56
|
+
| Fluxo contínuo produtor-consumidor | `Queue` + loops manuais | `Loom` + `Source` |
|
|
57
|
+
| Backpressure (produtor mais rápido que consumidores) | `Queue(maxsize=...)` manual | Padrão, configurável |
|
|
58
|
+
| Shutdown limpo (drenar fila, fechar recursos) | Sentinelas e joins manuais | `stop()` / `with Loom(...)` |
|
|
59
|
+
| Worker que sobrevive a erros e os reporta | try/except em cada worker | `hooks.on_error` centralizado |
|
|
60
|
+
| Métricas por item processado | Instrumentação manual | `hooks.on_batch_processed` |
|
|
61
|
+
| Escrita concorrente segura em arquivo | Lock manual | Sinks prontos (JSON, CSV, callback) |
|
|
62
|
+
|
|
63
|
+
Se o seu caso é "aplicar uma função a uma lista e coletar os resultados",
|
|
64
|
+
use `ThreadPoolExecutor.map` — é a ferramenta certa. O DataLoom é para
|
|
65
|
+
**fluxos contínuos ou longos** onde ciclo de vida, resiliência e
|
|
66
|
+
observabilidade importam. E, como toda solução baseada em threads no
|
|
67
|
+
CPython, o ganho de paralelismo vale para cargas **I/O bound**; para
|
|
68
|
+
CPU bound, prefira multiprocessing.
|
|
69
|
+
|
|
70
|
+
## ✨ Características
|
|
71
|
+
|
|
72
|
+
- **API Coesa:** Conceitos alinhados (`Loom`, `Weaver`, `Sink`).
|
|
73
|
+
- **Concorrente:** Gerencia automaticamente múltiplos _Weavers_ (threads) em paralelo.
|
|
74
|
+
- **Seguro:** Sinks padrão são thread-safe e exceções são tipadas (`LoomError`).
|
|
75
|
+
- **Resiliente:** Erros de processamento não derrubam os Weavers e são reportados via hooks.
|
|
76
|
+
- **Gerenciável:** `Loom` é um context manager — `with Loom(...) as loom:` garante `stop()` automático.
|
|
77
|
+
- **Backpressure:** Fila de tarefas limitada por padrão (`queue_maxsize`), evitando crescimento de memória sem controle.
|
|
78
|
+
- **Observável:** Hooks de ciclo de vida e métricas por lote (`on_batch_processed` com resultado e duração), além de Logs integrados.
|
|
79
|
+
|
|
80
|
+
## 📦 Instalação
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install dataloom-engine
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> ⚠️ **Atenção ao nome:** o pacote instala o módulo `dataloom_engine`
|
|
87
|
+
> (`import dataloom_engine`). Não confunda com o pacote `dataloom` do PyPI,
|
|
88
|
+
> que é um ORM de outro autor e não tem relação com este projeto.
|
|
89
|
+
|
|
90
|
+
Para desenvolver ou usar a versão mais recente do repositório:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git clone https://github.com/dionipadilha/dataloom.git
|
|
94
|
+
cd dataloom
|
|
95
|
+
pip install -e .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 🚀 Uso Rápido
|
|
99
|
+
|
|
100
|
+
Aqui está um exemplo completo de como tecer um pipeline de dados:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
import numpy as np
|
|
105
|
+
from dataloom_engine import (
|
|
106
|
+
Loom,
|
|
107
|
+
LoomConfig,
|
|
108
|
+
LoomLogs,
|
|
109
|
+
Processor,
|
|
110
|
+
JsonFileSink,
|
|
111
|
+
ThreadedBufferedSink
|
|
112
|
+
)
|
|
113
|
+
from dataloom_engine.sources import RandomNumPySource
|
|
114
|
+
|
|
115
|
+
# 1. Defina sua lógica de processamento (Stateless)
|
|
116
|
+
class MyFilterProcessor(Processor):
|
|
117
|
+
def process(self, batch: np.ndarray) -> dict:
|
|
118
|
+
# 'batch' é um array numpy com o tamanho definido na config
|
|
119
|
+
avg = float(batch.mean())
|
|
120
|
+
return {
|
|
121
|
+
"processed_items": len(batch),
|
|
122
|
+
"average_value": avg,
|
|
123
|
+
"status": "high" if avg > 0.5 else "low"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# 2. Configuração Inicial
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
# Configura logs no console
|
|
129
|
+
LoomLogs.setup()
|
|
130
|
+
|
|
131
|
+
# Define parâmetros do motor
|
|
132
|
+
config = LoomConfig(
|
|
133
|
+
output_dir=Path("./data_out"),
|
|
134
|
+
batch_size=100, # Processa 100 itens por vez
|
|
135
|
+
interval_seconds=1 # Gera um novo lote a cada 1 segundo
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 3. Fonte de Dados
|
|
139
|
+
# Agora desvinculada do motor, permitindo fontes customizadas (DB, CSV, S3)
|
|
140
|
+
source = RandomNumPySource(config)
|
|
141
|
+
|
|
142
|
+
# 4. Sink Arquivado e Otimizado
|
|
143
|
+
# ThreadedBufferedSink evita que o I/O bloqueie o processamento
|
|
144
|
+
file_sink = JsonFileSink(config.output_dir)
|
|
145
|
+
sink = ThreadedBufferedSink(file_sink)
|
|
146
|
+
|
|
147
|
+
# 5. Inicializa o Loom (O Orquestrador)
|
|
148
|
+
loom = Loom(
|
|
149
|
+
config=config,
|
|
150
|
+
processor=MyFilterProcessor(),
|
|
151
|
+
sink=sink,
|
|
152
|
+
source=source,
|
|
153
|
+
num_weavers=4 # 4 Threads trabalhando em paralelo
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
print("🧵 DataLoom iniciado! Pressione Ctrl+C para parar.")
|
|
157
|
+
try:
|
|
158
|
+
# O context manager garante stop() e limpeza dos recursos,
|
|
159
|
+
# mesmo em caso de exceção ou Ctrl+C
|
|
160
|
+
with loom:
|
|
161
|
+
loom.start()
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
print("\n🛑 Tear parado.")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 🏗️ Arquitetura
|
|
167
|
+
|
|
168
|
+
O DataLoom utiliza uma metáfora de tecelagem:
|
|
169
|
+
|
|
170
|
+
- **Loom (Tear):** A máquina principal. Gerencia a fila de tarefas e o ciclo de vida.
|
|
171
|
+
- **Weaver (Tecelão):** As threads trabalhadoras. Elas pegam a matéria-prima (batch), processam e entregam.
|
|
172
|
+
- **Processor:** A lógica de negócio. Transforma dados brutos em informação.
|
|
173
|
+
- **Sink:** O destino final. Onde o produto acabado é depositado (ex: `JsonFileSink`, `CsvFileSink`, ou qualquer destino via `CallbackSink`).
|
|
174
|
+
|
|
175
|
+
## 🛠️ Desenvolvimento e Testes
|
|
176
|
+
|
|
177
|
+
Para contribuir com o projeto ou rodar os testes unitários:
|
|
178
|
+
|
|
179
|
+
1. Instale as dependências de desenvolvimento:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install -e ".[dev]"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
2. Rode a suíte de testes (via pytest):
|
|
186
|
+
```bash
|
|
187
|
+
pytest
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 📄 Licença
|
|
191
|
+
|
|
192
|
+
Este projeto está licenciado sob a licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
|
193
|
+
|
|
194
|
+
## 🗒️ Histórico de versões
|
|
195
|
+
|
|
196
|
+
As mudanças de cada versão estão documentadas no [CHANGELOG](CHANGELOG.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
dataloom_engine/__init__.py,sha256=xiSTbVN-UNC2AgqXqmN_389W8oyFUY4pgWsPPWhkhic,1100
|
|
2
|
+
dataloom_engine/config.py,sha256=617hDIdvFqZf-_5mMFLHYYUeE60dqFDKOtgn6c94HIA,1788
|
|
3
|
+
dataloom_engine/exceptions.py,sha256=Vmwl4Us4uZrlggvVjZVNdsB__qeK4v3J5QrB2o7nvZ4,526
|
|
4
|
+
dataloom_engine/hooks.py,sha256=wf4NeDB9tljrCWQ2Ws81E447ob56k3EVzZUerqNxrz4,1665
|
|
5
|
+
dataloom_engine/logs.py,sha256=xo8LUl-8EVncmh7h3F6QaDt4byqN4trinrkbk0_QBl0,672
|
|
6
|
+
dataloom_engine/loom.py,sha256=8d0rh25ScIgxndH2dKdIq6wg418gztuELotsZ9ozpYc,5450
|
|
7
|
+
dataloom_engine/processors.py,sha256=Dh-AdkNsHS6q2dXDToqVbmfVCMuzwlw0cJhT4CBEpZw,1094
|
|
8
|
+
dataloom_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
dataloom_engine/sinks.py,sha256=n_JfwjnV63HQLZBOCzawbJBXtmKOteSzzSYzYV3rcrA,5629
|
|
10
|
+
dataloom_engine/sources.py,sha256=CPth2KGSVCK7_r1UV3LDxMY-XZhZLOsRVTbt5A2KZAs,1385
|
|
11
|
+
dataloom_engine/types.py,sha256=rsnKlTM5c_HbuWz4-_yq_l4-XGpPgpMuWn_Zp_Dn144,422
|
|
12
|
+
dataloom_engine/weaver.py,sha256=k6WdgIr7P1rEGUfqF1NgkmJKiuK6gLXqJtMHiOT06NE,2909
|
|
13
|
+
dataloom_engine-0.3.0.dist-info/licenses/LICENSE,sha256=Q0g4SFHxXcfz1o6yJa86DkY4QqnJzIXYCZJ1FiA-ivY,1072
|
|
14
|
+
dataloom_engine-0.3.0.dist-info/METADATA,sha256=LB8Pj356mF00tclFD6qrz1Yt61NyOpvXrgB5g7_XKos,8284
|
|
15
|
+
dataloom_engine-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
16
|
+
dataloom_engine-0.3.0.dist-info/top_level.txt,sha256=Zt-vsTb_3NRem8T-SVx5o8MolujisFvy8Y4Sfux7Q3E,16
|
|
17
|
+
dataloom_engine-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 [Dioni Padilha]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dataloom_engine
|