fred-oss 0.7.0__tar.gz → 0.9.0__tar.gz

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.
Files changed (41) hide show
  1. {fred_oss-0.7.0/src/main/fred_oss.egg-info → fred_oss-0.9.0}/PKG-INFO +2 -1
  2. {fred_oss-0.7.0 → fred_oss-0.9.0}/requirements.txt +2 -0
  3. fred_oss-0.9.0/src/main/fred/version +1 -0
  4. fred_oss-0.9.0/src/main/fred/worker/runner/__init__.py +11 -0
  5. fred_oss-0.9.0/src/main/fred/worker/runner/client.py +101 -0
  6. fred_oss-0.9.0/src/main/fred/worker/runner/handler.py +136 -0
  7. {fred_oss-0.7.0 → fred_oss-0.9.0/src/main/fred_oss.egg-info}/PKG-INFO +2 -1
  8. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred_oss.egg-info/SOURCES.txt +3 -0
  9. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred_oss.egg-info/requires.txt +1 -0
  10. fred_oss-0.7.0/src/main/fred/version +0 -1
  11. {fred_oss-0.7.0 → fred_oss-0.9.0}/MANIFEST.in +0 -0
  12. {fred_oss-0.7.0 → fred_oss-0.9.0}/NOTICE.txt +0 -0
  13. {fred_oss-0.7.0 → fred_oss-0.9.0}/README.md +0 -0
  14. {fred_oss-0.7.0 → fred_oss-0.9.0}/setup.cfg +0 -0
  15. {fred_oss-0.7.0 → fred_oss-0.9.0}/setup.py +0 -0
  16. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/cli/__init__.py +0 -0
  17. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/cli/__main__.py +0 -0
  18. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/cli/interface.py +0 -0
  19. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/cli/main.py +0 -0
  20. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/__init__.py +0 -0
  21. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/cli_ext.py +0 -0
  22. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/runtime.py +0 -0
  23. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/runtimes/__init__.py +0 -0
  24. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/runtimes/scanner.py +0 -0
  25. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/runtimes/sync.py +0 -0
  26. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/wrappers/__init__.py +0 -0
  27. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py +0 -0
  28. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/runpod/__init__.py +0 -0
  29. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/runpod/cli_ext.py +0 -0
  30. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/integrations/runpod/helper.py +0 -0
  31. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/maturity.py +0 -0
  32. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/settings.py +0 -0
  33. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/utils/__init__.py +0 -0
  34. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/utils/dateops.py +0 -0
  35. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/utils/runtime.py +0 -0
  36. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/version.py +0 -0
  37. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/worker/__init__.py +0 -0
  38. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred/worker/interface.py +0 -0
  39. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred_oss.egg-info/dependency_links.txt +0 -0
  40. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred_oss.egg-info/entry_points.txt +0 -0
  41. {fred_oss-0.7.0 → fred_oss-0.9.0}/src/main/fred_oss.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-oss
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: FREDOSS
5
5
  Home-page: https://fred.fahera.mx
6
6
  Author: Fahera Research, Education, and Development
@@ -10,6 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: NOTICE.txt
11
11
  Requires-Dist: fire==0.7.1
12
12
  Requires-Dist: psutil==7.0.0
13
+ Requires-Dist: redis==6.4.0
13
14
  Dynamic: author
14
15
  Dynamic: author-email
15
16
  Dynamic: description
@@ -2,4 +2,6 @@
2
2
  fire==0.7.1
3
3
  # Minimal requirements
4
4
  psutil==7.0.0
5
+ # Fred Workers Infra
6
+ redis==6.4.0
5
7
  # Databricks Runtime: 16.4 LTS (Python 3.12.3)
@@ -0,0 +1 @@
1
+ 0.9.0
@@ -0,0 +1,11 @@
1
+ from fred.maturity import Maturity, MaturityLevel
2
+
3
+
4
+ module_maturity = Maturity(
5
+ level=MaturityLevel.ALPHA,
6
+ reference=__name__,
7
+ message=(
8
+ "Fred-Worker Redis Runners implementation is in early development "
9
+ "and therefore currently with incomplete and unstable features."
10
+ )
11
+ )
@@ -0,0 +1,101 @@
1
+ import uuid
2
+ import json
3
+ from dataclasses import dataclass
4
+
5
+ from fred.settings import (
6
+ get_environ_variable,
7
+ logger_manager,
8
+ )
9
+
10
+ from redis import Redis
11
+
12
+ logger = logger_manager.get_logger(name=__name__)
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class RunnerClient:
17
+ instance: Redis
18
+ req_queue: str
19
+ res_queue: str
20
+
21
+ @classmethod
22
+ def auto(cls, **kwargs) -> "RunnerClient":
23
+ redis_configs = {
24
+ "host": kwargs.get("host") or get_environ_variable(name="REDIS_HOST", default="localhost"),
25
+ "port": int(kwargs.get("port") or get_environ_variable(name="REDIS_PORT", default=6379)),
26
+ "db": int(kwargs.get("db") or get_environ_variable(name="REDIS_DB", default=0)),
27
+ }
28
+ redis_instance = Redis(**redis_configs)
29
+ req_queue = kwargs.get("request_queue") or get_environ_variable(name="FRD_RUNNER_REQUEST_QUEUE", default=None) or (
30
+ logger.warning("Redis request queue not specified; defaulting to 'req:demo'.") or "req:demo"
31
+ )
32
+ res_queue = kwargs.get("response_queue") or get_environ_variable(name="FRD_RUNNER_RESPONSE_QUEUE", default=None) or (
33
+ logger.warning("Redis response queue not specified; defaulting to inferring pattern.") or f"res:{req_queue.split(':')[-1]}"
34
+ )
35
+ logger.info(f"Connecting to Redis, using request queue '{req_queue}' and response queue '{res_queue}'.")
36
+ return cls(
37
+ instance=redis_instance,
38
+ req_queue=req_queue,
39
+ res_queue=res_queue,
40
+ )
41
+
42
+ @property
43
+ def PING(self):
44
+ return self.signal("PING")
45
+
46
+ @property
47
+ def STOP(self):
48
+ return self.signal("STOP")
49
+
50
+ def signal(self, signal: str):
51
+ # TODO: Validate signals via enum
52
+ self.instance.lpush(self.req_queue, signal)
53
+
54
+ def send(self, item: dict, uuid_hash: bool = False) -> str:
55
+ item_id = item.get("item_id")
56
+ item_str = json.dumps(item)
57
+ if not item_id:
58
+ logger.warning("Item does not have 'item_id'; assigning a UUID based on the hash.")
59
+ item["item_id"] = item_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, item_str)) \
60
+ if uuid_hash else str(uuid.uuid4())
61
+ item_str = json.dumps(item)
62
+ self.instance.lpush(self.req_queue, item_str)
63
+ self.instance.set(f"item_status:{item_id}", "IN_QUEUE")
64
+ return item_id
65
+
66
+ def fetch_status(self, item_id: str) -> str | None:
67
+ status_raw = self.instance.get(f"item_status:{item_id}")
68
+ if not status_raw:
69
+ logger.info(f"No status found for item_id '{item_id}'.")
70
+ return None
71
+ return status_raw.decode("utf-8")
72
+
73
+ def fetch_result(self, item_id: str, blocking: bool = False) -> dict | None:
74
+ match self.fetch_status(item_id=item_id):
75
+ case None:
76
+ logger.info(f"No status found for item_id '{item_id}'.")
77
+ return None
78
+ case "IN_QUEUE" | "PROCESSING":
79
+ if blocking:
80
+ logger.info(f"Blocking until item '{item_id}' is completed.")
81
+ while (status := self.fetch_status(item_id=item_id)) in ("IN_QUEUE", "PROCESSING"):
82
+ continue
83
+ else:
84
+ logger.info(f"Item '{item_id}' is still in progress (current status: '{self.fetch_status(item_id=item_id)}').")
85
+ return None
86
+ case "FAILED":
87
+ logger.error(f"Item '{item_id}' processing failed.")
88
+ return None
89
+ case "COMPLETED":
90
+ result_raw = self.instance.get(f"item_output:{item_id}")
91
+ if result_raw:
92
+ try:
93
+ return json.loads(result_raw)
94
+ except json.JSONDecodeError as e:
95
+ logger.error(f"Error decoding JSON result for item_id '{item_id}': {e}")
96
+ return None
97
+ else:
98
+ logger.error(f"No result found for item_id '{item_id}'.")
99
+ return None
100
+ case status:
101
+ logger.warning(f"Item '{item_id}' has unrecognized status '{status}'. Proceeding to fetch result.")
@@ -0,0 +1,136 @@
1
+ import uuid
2
+ import json
3
+ from dataclasses import dataclass
4
+
5
+ from fred.utils.dateops import datetime_utcnow
6
+ from fred.worker.interface import HandlerInterface
7
+ from fred.settings import (
8
+ get_environ_variable,
9
+ logger_manager,
10
+ )
11
+
12
+ from redis import Redis
13
+
14
+ logger = logger_manager.get_logger(name=__name__)
15
+
16
+
17
+ @dataclass(frozen=True, slots=False)
18
+ class RunnerHandler(HandlerInterface):
19
+
20
+ def __post_init__(self):
21
+ super().__post_init__()
22
+ logger.info("Runpod Handler initialized using Fred-Worker interface.")
23
+
24
+ def handler(self, payload: dict) -> dict:
25
+ # TODO: Breakdown the handler logic into smaller methods for better readability and testing
26
+ # E.g., loop method, process item method, signal handling method, etc.
27
+ lifespan = payload.get("lifetime", 3600) # Default to 1 hour if not specified
28
+ timeout = payload.get("timeout", 30) # Default to 30 seconds if not specified
29
+ # Get Redis connection details from payload or environment variables
30
+ redis_configs = payload.pop(
31
+ "redis_configs",
32
+ {
33
+ "host": get_environ_variable(name="REDIS_HOST", default="localhost"),
34
+ "port": int(get_environ_variable(name="REDIS_PORT", default=6379)),
35
+ "db": int(get_environ_variable(name="REDIS_DB", default=0)),
36
+ }
37
+ )
38
+ # Connect to Redis
39
+ redis = Redis(**redis_configs)
40
+ req_queue = payload.pop("redis_request_queue", None) or get_environ_variable(name="FRD_RUNNER_REQUEST_QUEUE", default=None) or (
41
+ logger.warning("Redis request queue not specified; defaulting to: 'req:demo'") or "req:demo"
42
+ )
43
+ res_queue = payload.pop("redis_response_queue", None) or get_environ_variable(name="FRD_RUNNER_RESPONSE_QUEUE", default=None) or (
44
+ logger.warning("Redis response queue not specified; defaulting to: None (not using response queue)") or None
45
+ )
46
+ # Handoff to target handler (i.e., runner)
47
+ runner_configs = payload.pop("runner_configs")
48
+ runner_id = runner_configs.pop("id", str(uuid.uuid4()))
49
+ runner = HandlerInterface.find_handler(**runner_configs)
50
+ logger.info(f"Runpod Redis Handler started with runner '{runner_id}' listening to Redis queue: '{req_queue}'")
51
+ redis.set(f"runner_status:{runner_id}", "RUNNING")
52
+ redis.set(f"runner_created_at:{runner_id}", datetime_utcnow().isoformat())
53
+ # Main runner loop to process items from Redis queue
54
+ # TODO: Can we make this main-loop concurrent with threads or async?
55
+ # TODO: Consider collecting metrics (e.g., processing time per item, total items processed, errors, etc.) and stats
56
+ start_time = datetime_utcnow()
57
+ last_processed_time = datetime_utcnow()
58
+ while (elapsed_seconds := (datetime_utcnow() - start_time).total_seconds()):
59
+ if elapsed_seconds > lifespan:
60
+ logger.info("Lifespan exceeded; exiting runner loop.")
61
+ break
62
+ if (idle_seconds := (datetime_utcnow() - last_processed_time).total_seconds()) > timeout:
63
+ logger.info(f"Idle time ({idle_seconds}) exceeded timeout ({timeout}); exiting runner loop.")
64
+ break
65
+ # Fetch item from Redis queue
66
+ try:
67
+ item_raw = redis.rpop(req_queue)
68
+ except Exception as e:
69
+ logger.error(f"Error fetching item from Redis queue '{req_queue}': {e}")
70
+ continue
71
+ # If no item, iterate again
72
+ if not item_raw:
73
+ continue
74
+ try:
75
+ # Handle special signals
76
+ match (item_str := item_raw.decode("utf-8")):
77
+ case "STOP" | "SHUTDOWN" | "TERMINATE":
78
+ logger.info("Received STOP signal; exiting runner loop.")
79
+ break
80
+ case "PING":
81
+ logger.info("Received PING signal; continuing.")
82
+ last_processed_time = datetime_utcnow()
83
+ continue
84
+ case _:
85
+ pass
86
+ # Parse item payload and extract item_id
87
+ item_payload = json.loads(item_str)
88
+ item_id = item_payload.pop("item_id") or (
89
+ logger.warning("No item_id provided in payload; generating a new one using UUID5.")
90
+ or str(uuid.uuid5(uuid.NAMESPACE_OID, item_str))
91
+ )
92
+ except Exception as e:
93
+ logger.error(f"Error decoding or parsing item from Redis: {e}")
94
+ continue
95
+ logger.info(f"Processing item with ID: {item_id}")
96
+ redis.set(f"item_status:{item_id}", "IN_PROGRESS")
97
+ try:
98
+ out_payload = runner.run(
99
+ event={
100
+ "id": item_id,
101
+ "input": item_payload
102
+ }
103
+ )
104
+ out_str = json.dumps(out_payload) if isinstance(out_payload, dict) else str(out_payload)
105
+ redis.set(f"item_status:{item_id}", "COMPLETED")
106
+ # TODO: Consider adding a TTL to the result keys (e.g., 24 hours)
107
+ redis.set(f"item_output:{item_id}", out_str)
108
+ if res_queue:
109
+ redis.lpush(res_queue, out_str)
110
+ except Exception as e:
111
+ logger.error(f"Error processing item with ID {item_id}: {e}")
112
+ redis.set(f"item_status:{item_id}", "FAILED")
113
+ redis.set(f"item_output:{item_id}", str(e))
114
+ continue
115
+ logger.info(f"Processed item with ID: {item_id}")
116
+ last_processed_time = datetime_utcnow()
117
+ redis.set(f"runner_status:{runner_id}", "STOPPED")
118
+ pending_requests = redis.llen(req_queue)
119
+ if pending_requests:
120
+ logger.warning(f"Runner '{runner_id}' stopped with {pending_requests} pending items still in the queue '{req_queue}'.")
121
+ # TODO: Consider adding logic (optional/configurable) to spin up a new runner to handle pending items or notify runner-manager
122
+ else:
123
+ logger.info("Runner stopped with no pending items in the queue.")
124
+ return {
125
+ "status": "completed",
126
+ "runner_id": runner_id,
127
+ "processed_at": datetime_utcnow().isoformat(),
128
+ "total_elapsed_seconds": (datetime_utcnow() - start_time).total_seconds(),
129
+ "last_processed_at": last_processed_time.isoformat(),
130
+ "idle_seconds": (datetime_utcnow() - last_processed_time).total_seconds(),
131
+ "pending_requests": pending_requests,
132
+ "request_queue": req_queue,
133
+ "response_queue": res_queue,
134
+ "lifespan_seconds": lifespan,
135
+ "timeout_seconds": timeout,
136
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-oss
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: FREDOSS
5
5
  Home-page: https://fred.fahera.mx
6
6
  Author: Fahera Research, Education, and Development
@@ -10,6 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: NOTICE.txt
11
11
  Requires-Dist: fire==0.7.1
12
12
  Requires-Dist: psutil==7.0.0
13
+ Requires-Dist: redis==6.4.0
13
14
  Dynamic: author
14
15
  Dynamic: author-email
15
16
  Dynamic: description
@@ -27,6 +27,9 @@ src/main/fred/utils/dateops.py
27
27
  src/main/fred/utils/runtime.py
28
28
  src/main/fred/worker/__init__.py
29
29
  src/main/fred/worker/interface.py
30
+ src/main/fred/worker/runner/__init__.py
31
+ src/main/fred/worker/runner/client.py
32
+ src/main/fred/worker/runner/handler.py
30
33
  src/main/fred_oss.egg-info/PKG-INFO
31
34
  src/main/fred_oss.egg-info/SOURCES.txt
32
35
  src/main/fred_oss.egg-info/dependency_links.txt
@@ -1,2 +1,3 @@
1
1
  fire==0.7.1
2
2
  psutil==7.0.0
3
+ redis==6.4.0
@@ -1 +0,0 @@
1
- 0.7.0
File without changes
File without changes
File without changes
File without changes
File without changes