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.
@@ -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
@@ -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
@@ -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
+ )
@@ -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
+ }
File without changes
@@ -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
@@ -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
+ [![CI](https://github.com/dionipadilha/dataloom/actions/workflows/ci.yml/badge.svg)](https://github.com/dionipadilha/dataloom/actions/workflows/ci.yml) ![Python](https://img.shields.io/badge/python-3.9%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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