Source code for fetchez.streams.base
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
fetchez.streams.base
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Base fetchez Reader class to create 'streams'
:copyright: (c) 2016 - 2026 Regents of the University of Colorado
:license: MIT, see LICENSE for more details.
"""
import queue
import threading
from fetchez.core import run_fetchez
from fetchez.utils import parse_hook_string
from fetchez.hooks import FetchHook
[docs]
class QueueSinkHook(FetchHook):
"""An internal hook that yields stream chunks to a Queue."""
name = "queue_sink"
meta_stage = "stream"
[docs]
def __init__(self, q):
super().__init__()
self.q = q
[docs]
def run(self, entries):
for _mod, entry in entries:
stream = entry.get("stream")
if stream:
# Intercept the generator
def interceptor(s):
for chunk in s:
self.q.put(chunk)
yield chunk
entry["stream"] = interceptor(stream)
return entries
[docs]
class BaseStream:
[docs]
def __init__(self, modules, region=None, ignore_failures=False):
self.modules = modules
self.region = region
self.global_hooks = []
self.ignore_failures = ignore_failures
[docs]
def pipe(self, hook_or_string, **kwargs):
"""Chain a processing hook onto the pipeline."""
from fetchez.registry import HookRegistry, PresetRegistry
if isinstance(hook_or_string, str):
PresetRegistry.load_all()
if PresetRegistry.get_preset(hook_or_string):
override_def = [
{
"preset": hook_or_string,
"args": [{"name": k, "args": v} for k, v in kwargs.items()],
}
]
hooks = PresetRegistry.hook_list_from_preset(override_def)
for h in hooks:
self.global_hooks.append(h)
return self
hook_config = parse_hook_string(hook_or_string)
HookRegistry.load_all()
hook_class = HookRegistry.get_class(hook_config.get("name"))
if not hook_class:
raise ValueError(f"Hook '{hook_or_string}' not found.")
kwargs = {**hook_config.get("args", {}), **kwargs}
self.global_hooks.append(hook_class(**kwargs))
elif isinstance(hook_or_string, dict):
HookRegistry.load_all()
PresetRegistry.load_all()
hook_name = hook_or_string.get("name") or hook_or_string.get("preset")
if hook_name:
hook_class = HookRegistry.get_class(hook_name)
self.global_hooks.append(hook_class(**hook_or_string.get("args", {})))
else:
raise ValueError(f"Invalid hook definition: {hook_or_string}.")
else:
hook = (
hook_or_string
if hasattr(hook_or_string, "run")
else hook_or_string(**kwargs)
)
self.global_hooks.append(hook)
return self
def __iter__(self):
"""Yield chunks using a background pipeline thread."""
[mod.run() for mod in self.modules]
chunk_queue = queue.Queue(maxsize=100)
sink = QueueSinkHook(chunk_queue)
run_hooks = self.global_hooks + [sink]
DONE = object()
def background_worker():
try:
run_fetchez(
self.modules,
threads=2,
global_hooks=run_hooks,
ignore_failures=self.ignore_failures,
)
finally:
chunk_queue.put(DONE)
# Start the engine in the background
t = threading.Thread(target=background_worker, daemon=True)
t.start()
while True:
chunk = chunk_queue.get()
if chunk is DONE:
break
yield chunk
chunk_queue.task_done()