fred-oss 0.29.0__tar.gz → 0.30.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 (94) hide show
  1. {fred_oss-0.29.0/src/main/fred_oss.egg-info → fred_oss-0.30.0}/PKG-INFO +1 -1
  2. fred_oss-0.30.0/src/main/fred/version +1 -0
  3. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/interface.py +4 -4
  4. fred_oss-0.30.0/src/main/fred/worker/runner/backend.py +31 -0
  5. fred_oss-0.30.0/src/main/fred/worker/runner/client.py +140 -0
  6. fred_oss-0.30.0/src/main/fred/worker/runner/handler.py +191 -0
  7. fred_oss-0.30.0/src/main/fred/worker/runner/model/_handler.py +21 -0
  8. fred_oss-0.30.0/src/main/fred/worker/runner/model/_item.py +46 -0
  9. fred_oss-0.30.0/src/main/fred/worker/runner/model/_request.py +41 -0
  10. fred_oss-0.30.0/src/main/fred/worker/runner/model/_runner_spec.py +88 -0
  11. fred_oss-0.30.0/src/main/fred/worker/runner/model/catalog.py +16 -0
  12. fred_oss-0.30.0/src/main/fred/worker/runner/model/interface.py +10 -0
  13. fred_oss-0.30.0/src/main/fred/worker/runner/plugins/_local.py +34 -0
  14. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/plugins/catalog.py +4 -1
  15. fred_oss-0.30.0/src/main/fred/worker/runner/plugins/interface.py +131 -0
  16. fred_oss-0.30.0/src/main/fred/worker/runner/rest/routers/__init__.py +0 -0
  17. fred_oss-0.30.0/src/main/fred/worker/runner/settings.py +17 -0
  18. fred_oss-0.30.0/src/main/fred/worker/runner/signal.py +13 -0
  19. fred_oss-0.30.0/src/main/fred/worker/runner/status.py +27 -0
  20. {fred_oss-0.29.0 → fred_oss-0.30.0/src/main/fred_oss.egg-info}/PKG-INFO +1 -1
  21. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/SOURCES.txt +11 -0
  22. fred_oss-0.29.0/src/main/fred/version +0 -1
  23. fred_oss-0.29.0/src/main/fred/worker/runner/client.py +0 -101
  24. fred_oss-0.29.0/src/main/fred/worker/runner/handler.py +0 -136
  25. fred_oss-0.29.0/src/main/fred/worker/runner/plugins/_local.py +0 -26
  26. fred_oss-0.29.0/src/main/fred/worker/runner/plugins/interface.py +0 -152
  27. {fred_oss-0.29.0 → fred_oss-0.30.0}/MANIFEST.in +0 -0
  28. {fred_oss-0.29.0 → fred_oss-0.30.0}/NOTICE.txt +0 -0
  29. {fred_oss-0.29.0 → fred_oss-0.30.0}/README.md +0 -0
  30. {fred_oss-0.29.0 → fred_oss-0.30.0}/requirements.txt +0 -0
  31. {fred_oss-0.29.0 → fred_oss-0.30.0}/setup.cfg +0 -0
  32. {fred_oss-0.29.0 → fred_oss-0.30.0}/setup.py +0 -0
  33. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/__init__.py +0 -0
  34. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/__main__.py +0 -0
  35. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/interface.py +0 -0
  36. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/main.py +0 -0
  37. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/__init__.py +0 -0
  38. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/__init__.py +0 -0
  39. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/_keyval.py +0 -0
  40. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/_queue.py +0 -0
  41. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/catalog.py +0 -0
  42. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/interface.py +0 -0
  43. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/__init__.py +0 -0
  44. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/_redis.py +0 -0
  45. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/_stdlib.py +0 -0
  46. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/catalog.py +0 -0
  47. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/interface.py +0 -0
  48. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/utils.py +0 -0
  49. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/__init__.py +0 -0
  50. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/__init__.py +0 -0
  51. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/_function.py +0 -0
  52. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/catalog.py +0 -0
  53. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/interface.py +0 -0
  54. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/impl.py +0 -0
  55. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/result.py +0 -0
  56. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/settings.py +0 -0
  57. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/utils.py +0 -0
  58. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/__init__.py +0 -0
  59. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/cli_ext.py +0 -0
  60. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtime.py +0 -0
  61. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtimes/__init__.py +0 -0
  62. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtimes/scanner.py +0 -0
  63. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtimes/sync.py +0 -0
  64. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/wrappers/__init__.py +0 -0
  65. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py +0 -0
  66. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/runpod/__init__.py +0 -0
  67. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/runpod/cli_ext.py +0 -0
  68. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/runpod/helper.py +0 -0
  69. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/maturity.py +0 -0
  70. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/__init__.py +0 -0
  71. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/_either.py +0 -0
  72. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/catalog.py +0 -0
  73. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/interface.py +0 -0
  74. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/settings.py +0 -0
  75. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/utils/__init__.py +0 -0
  76. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/utils/dateops.py +0 -0
  77. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/utils/runtime.py +0 -0
  78. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/version.py +0 -0
  79. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/__init__.py +0 -0
  80. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/__init__.py +0 -0
  81. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/info.py +0 -0
  82. {fred_oss-0.29.0/src/main/fred/worker/runner/plugins → fred_oss-0.30.0/src/main/fred/worker/runner/model}/__init__.py +0 -0
  83. {fred_oss-0.29.0/src/main/fred/worker/runner/rest → fred_oss-0.30.0/src/main/fred/worker/runner/plugins}/__init__.py +0 -0
  84. {fred_oss-0.29.0/src/main/fred/worker/runner/rest/routers → fred_oss-0.30.0/src/main/fred/worker/runner/rest}/__init__.py +0 -0
  85. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/cli_ext.py +0 -0
  86. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/routers/_runner.py +0 -0
  87. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/routers/catalog.py +0 -0
  88. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/routers/interface.py +0 -0
  89. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/server.py +0 -0
  90. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/utils.py +0 -0
  91. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/dependency_links.txt +0 -0
  92. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/entry_points.txt +0 -0
  93. {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/requires.txt +0 -0
  94. {fred_oss-0.29.0 → fred_oss-0.30.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.29.0
3
+ Version: 0.30.0
4
4
  Summary: FREDOSS
5
5
  Home-page: https://fred.fahera.mx
6
6
  Author: Fahera Research, Education, and Development
@@ -0,0 +1 @@
1
+ 0.30.0
@@ -139,14 +139,14 @@ class HandlerInterface:
139
139
  return json.loads(metadata_serialized)
140
140
 
141
141
  @overload
142
- def run(self, event: dict, as_future: bool = True) -> Future[dict]:
142
+ def run(self, event: dict, as_future: bool = True, future_id: Optional[str] = None) -> Future[dict]:
143
143
  ...
144
144
 
145
145
  @overload
146
- def run(self, event: dict, as_future: bool = False) -> dict:
146
+ def run(self, event: dict, as_future: bool = False, future_id: Optional[str] = None) -> dict:
147
147
  ...
148
148
 
149
- def run(self, event: dict, as_future: bool = False) -> dict | Future[dict]:
149
+ def run(self, event: dict, as_future: bool = False, future_id: Optional[str] = None) -> dict | Future[dict]:
150
150
  """Process an incoming event and return a structured response.
151
151
  The event is expected to be a dictionary with at least an 'id' and 'input' keys.
152
152
  The 'input' key should contain the payload to be processed.
@@ -158,7 +158,7 @@ class HandlerInterface:
158
158
  If requested as a Future, returns a Future that will resolve to the response dictionary.
159
159
  """
160
160
  if as_future:
161
- return Future(function=lambda: self.run(event=event, as_future=False))
161
+ return Future(function=lambda: self.run(event=event, as_future=False), future_id=future_id)
162
162
  # Extract payload and event ID
163
163
  payload = event.get("input", {})
164
164
  job_event_identifier = event.get("id")
@@ -0,0 +1,31 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fred.settings import logger_manager
4
+ from fred.dao.comp.catalog import FredKeyVal, FredQueue
5
+ from fred.dao.service.catalog import ServiceCatalog
6
+
7
+ logger = logger_manager.get_logger(name=__name__)
8
+
9
+
10
+ @dataclass(frozen=True, slots=False)
11
+ class RunnerBackend:
12
+ keyval: FredKeyVal
13
+ queue: FredQueue
14
+
15
+ @classmethod
16
+ def auto(cls, service_name: str, **kwargs) -> 'RunnerBackend':
17
+ service = ServiceCatalog[service_name.upper()]
18
+ match service:
19
+ case ServiceCatalog.REDIS:
20
+ from fred.dao.service.utils import get_redis_configs_from_payload
21
+ service_kwargs = get_redis_configs_from_payload(kwargs)
22
+ case ServiceCatalog.STDLIB:
23
+ service_kwargs = {}
24
+ case _:
25
+ logger.error(f"Unknown service '{service_name}'... will attempt to use provided kwargs as-is.")
26
+ service_kwargs = kwargs
27
+ components = service.component_catalog(**service_kwargs)
28
+ return cls(
29
+ keyval=components.KEYVAL.value,
30
+ queue=components.QUEUE.value,
31
+ )
@@ -0,0 +1,140 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+
5
+ from fred.future import Future
6
+ from fred.future.result import FutureResult
7
+ from fred.dao.comp.catalog import FredQueue
8
+ from fred.worker.runner.status import RunnerStatus
9
+ from fred.worker.runner.signal import RunnerSignal
10
+ from fred.worker.runner.backend import RunnerBackend
11
+ from fred.worker.runner.model.catalog import RunnerModelCatalog
12
+ from fred.worker.runner.settings import (
13
+ FRD_RUNNER_BACKEND,
14
+ FRD_RUNNER_REQUEST_QUEUE,
15
+ FRD_RUNNER_RESPONSE_QUEUE,
16
+ )
17
+ from fred.settings import logger_manager
18
+
19
+ logger = logger_manager.get_logger(name=__name__)
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class RunnerClient:
24
+ _runner_backend: RunnerBackend
25
+ req_queue: FredQueue
26
+ res_queue: FredQueue
27
+
28
+
29
+ @classmethod
30
+ def auto(
31
+ cls,
32
+ queue_slug: Optional[str] = None,
33
+ service_name: Optional[str] = None,
34
+ **kwargs
35
+ ) -> "RunnerClient":
36
+ queue_slug = queue_slug or kwargs.pop("queue_slug", None) or (
37
+ logger.warning("Queue slug not specified; defaulting to: 'demo'")
38
+ or "demo"
39
+ )
40
+ queue_name_request = FRD_RUNNER_REQUEST_QUEUE or f"req:{queue_slug}"
41
+ queue_name_response = FRD_RUNNER_RESPONSE_QUEUE or f"res:{queue_slug}"
42
+ runner_backend = RunnerBackend.auto(
43
+ service_name=service_name or FRD_RUNNER_BACKEND,
44
+ **kwargs
45
+ )
46
+ return cls(
47
+ _runner_backend=runner_backend,
48
+ req_queue=runner_backend.queue(name=queue_name_request),
49
+ res_queue=runner_backend.queue(name=queue_name_response),
50
+ )
51
+
52
+ @property
53
+ def PING(self):
54
+ return self.signal(RunnerSignal.PING)
55
+
56
+ @property
57
+ def STOP(self):
58
+ return self.signal(RunnerSignal.STOP)
59
+
60
+ def signal(self, signal: str | RunnerSignal):
61
+ signal = RunnerSignal[signal.upper()] if isinstance(signal, str) else signal
62
+ return signal.send(self.req_queue)
63
+
64
+ def runner_status(self, runner_id: str) -> RunnerStatus:
65
+ runner_status=self._runner_backend.keyval(
66
+ key=RunnerStatus.get_key(runner_id=runner_id)
67
+ )
68
+ if not (value := runner_status.get()):
69
+ logger.warning(f"No status found for runner_id: '{runner_id}'")
70
+ return RunnerStatus.UNDEFINED
71
+ return RunnerStatus.parse_value(value=value)
72
+
73
+ def send(
74
+ self,
75
+ item: dict,
76
+ req_uuid_hash: bool = False,
77
+ item_uuid_hash: bool = False,
78
+ ) -> str:
79
+ item_instance = RunnerModelCatalog.ITEM.value.uuid(payload=item, uuid_hash=item_uuid_hash)
80
+ request = item_instance.as_request(
81
+ use_hash=req_uuid_hash,
82
+ request_id=item.get("request_id"),
83
+ )
84
+ request.dispatch(request_queue=self.req_queue)
85
+ return request.request_id
86
+
87
+ @staticmethod
88
+ def fetch_status(request_id: str) -> Optional[str]:
89
+ return FutureResult._get_status_key(future_id=request_id).get()
90
+
91
+ def pullsync(
92
+ self,
93
+ request_id: str,
94
+ retry_sync: int = 10,
95
+ retry_delay: float = 0.5,
96
+ **kwargs,
97
+ ) -> Future:
98
+ # TODO: Once the broadcast method is implemented, we can add a warning notice regarding
99
+ # the pullsync metho adding extra load to the backend service
100
+ future = Future(
101
+ function=self._is_ready_for_pullsync,
102
+ request_id=request_id,
103
+ retry_sync=retry_sync,
104
+ retry_delay=retry_delay,
105
+ fail=True, # Always fail inside the future to propagate the exception
106
+ )
107
+ return future.flat_map(
108
+ lambda _: Future.pullsync(future_id=request_id, **kwargs)
109
+ )
110
+
111
+ def _is_ready_for_pullsync(
112
+ self,
113
+ request_id: str,
114
+ retry_sync: int = 10,
115
+ retry_delay: float = 0.5,
116
+ fail: bool = False,
117
+ ) -> bool:
118
+ if not self.fetch_status(request_id=request_id):
119
+ logger.info(f"No status found for request_id '{request_id}'")
120
+ retry_kwargs = {
121
+ "request_id": request_id,
122
+ "retry_sync": retry_sync - 1,
123
+ "retry_delay": retry_delay,
124
+ "fail": fail,
125
+ }
126
+ if retry_sync:
127
+ time.sleep(retry_delay)
128
+ return self._is_ready_for_pullsync(**retry_kwargs)
129
+ elif fail:
130
+ logger.error(f"Failed to fetch status for request_id '{request_id}' after retries.")
131
+ raise ValueError(f"No status found for request_id '{request_id}'")
132
+ else:
133
+ return False
134
+ return True
135
+
136
+ def fetch_result(self, request_id: str, now: bool = False, timeout: Optional[float] = None) -> Optional[dict]:
137
+ future = self.pullsync(request_id=request_id)
138
+ if now:
139
+ return future.getwhatevernow()
140
+ return future.wait_and_resolve(timeout=timeout)
@@ -0,0 +1,191 @@
1
+ import uuid
2
+ import json
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from fred.future import Future
7
+ from fred.monad.catalog import EitherMonad
8
+ from fred.utils.dateops import datetime_utcnow
9
+ from fred.dao.comp.catalog import FredQueue
10
+ from fred.worker.runner.settings import FRD_RUNNER_BACKEND
11
+ from fred.worker.runner.backend import RunnerBackend
12
+ from fred.worker.runner.model.catalog import RunnerModelCatalog
13
+ from fred.worker.interface import HandlerInterface
14
+ from fred.worker.runner.status import RunnerStatus
15
+
16
+ from fred.settings import logger_manager
17
+
18
+
19
+ logger = logger_manager.get_logger(name=__name__)
20
+
21
+
22
+ @dataclass(frozen=True, slots=False)
23
+ class RunnerHandler(HandlerInterface):
24
+
25
+ def __post_init__(self):
26
+ super().__post_init__()
27
+ logger.info("Fred-Runner outer-handler initialized using Fred-Worker interface.")
28
+
29
+ @staticmethod
30
+ def _runner_process(
31
+ item: dict,
32
+ runner: HandlerInterface,
33
+ item_id: str,
34
+ request_id: str,
35
+ ) -> Future[dict]:
36
+ logger.info(f"Processing item '{item_id}' provided by request-id: {request_id}")
37
+ return runner.run(
38
+ event={
39
+ "id": item_id,
40
+ "input": item
41
+ },
42
+ as_future=True,
43
+ future_id=request_id,
44
+ )
45
+
46
+ def _runner_loop(
47
+ self,
48
+ runner: HandlerInterface,
49
+ req_queue: FredQueue,
50
+ lifespan: int,
51
+ timeout: int,
52
+ res_queue: Optional[FredQueue] = None,
53
+ ) -> dict:
54
+ start_time = datetime_utcnow()
55
+ last_processed_time = datetime_utcnow()
56
+ while (elapsed_seconds := (datetime_utcnow() - start_time).total_seconds()):
57
+ if elapsed_seconds > lifespan:
58
+ logger.info("Lifespan exceeded; exiting runner loop.")
59
+ break
60
+ if (idle_seconds := (datetime_utcnow() - last_processed_time).total_seconds()) > timeout:
61
+ logger.info(f"Idle time ({idle_seconds}) exceeded timeout ({timeout}); exiting runner loop.")
62
+ break
63
+ # Fetch item from Redis queue
64
+ try:
65
+ message = req_queue.pop()
66
+ # If no item, iterate again
67
+ if not message:
68
+ continue
69
+ except Exception as e:
70
+ logger.error(f"Error fetching item from Redis queue '{req_queue}': {e}")
71
+ continue
72
+ # Handle special signals
73
+ match message:
74
+ case "STOP" | "SHUTDOWN" | "TERMINATE":
75
+ logger.info("Received STOP signal; exiting runner loop.")
76
+ break
77
+ case "PING":
78
+ logger.info("Received PING signal; continuing.")
79
+ last_processed_time = datetime_utcnow()
80
+ continue
81
+ case _:
82
+ pass
83
+ try:
84
+ item = json.loads(message)
85
+ item_id = item.get("item_id") or (
86
+ logger.warning("No item_id provided in item-payload; generating a new one using UUID5 hash.")
87
+ or str(uuid.uuid5(uuid.NAMESPACE_OID, message))
88
+ )
89
+ # The request_id is used as the future_id for tracking purposes
90
+ request_id = item.get("request_id") or (
91
+ logger.warning(f"No request_id provided in item-payload; using the item_id '{item_id}' instead.")
92
+ or item_id
93
+ )
94
+ except Exception as e:
95
+ logger.error(f"Error decoding or parsing item from Redis: {e}")
96
+ continue
97
+ # Process item using runner and handle result
98
+ future = self._runner_process(item=item, runner=runner, item_id=item_id, request_id=request_id).map(
99
+ lambda res: res if isinstance(res, str) else json.dumps(res, default=str)
100
+ )
101
+ match future.wait():
102
+ case EitherMonad.Right(value):
103
+ if res_queue:
104
+ logger.debug(f"Processed item ID '{item_id}' on request ID '{request_id}' and pushed result to response queue.")
105
+ res_queue.add(value)
106
+ case EitherMonad.Left(error):
107
+ logger.error(f"Error processing item ID '{item_id}' on request ID '{request_id}': {error}")
108
+ continue
109
+ case _:
110
+ logger.error(f"Unexpected result processing item ID '{item_id}' on request ID '{request_id}': {future}")
111
+ continue
112
+ last_processed_time = datetime_utcnow()
113
+ return {
114
+ "started_at": start_time.isoformat(),
115
+ "stopped_at": datetime_utcnow().isoformat(),
116
+ "total_elapsed_seconds": (datetime_utcnow() - start_time).total_seconds(),
117
+ "last_processed_at": last_processed_time.isoformat(),
118
+ "idle_seconds": (datetime_utcnow() - last_processed_time).total_seconds(),
119
+ }
120
+
121
+
122
+ def handler(self, payload: dict) -> dict:
123
+ # Configure the backend service abstraction (e.g., Redis)
124
+ runner_backend = RunnerBackend.auto(
125
+ service_name=payload.pop("runner_backend", None) or FRD_RUNNER_BACKEND,
126
+ **payload.pop("runner_backend_configs", {}),
127
+ )
128
+ # Outer handler model instance
129
+ spec = RunnerModelCatalog.RUNNER_SPEC.value.from_payload(payload=payload)
130
+
131
+ # Determine request and response queues to use for this runner instance
132
+ req_queue = runner_backend.queue(name=spec.request_queue_name)
133
+ res_queue = runner_backend.queue(name=spec.response_queue_name) \
134
+ if spec.use_response_queue else None
135
+ # Get runner (inner handler) instance and ID
136
+ # The 'outer' handler delegates the processing of tasks to the 'inner' handler
137
+ # and is responsible for managing the lifecycle and updating the status of the runner
138
+ runner = spec.inner.get_handler()
139
+ runner_id = spec.runner_id
140
+ runner_status = runner_backend.keyval(
141
+ key=RunnerStatus.get_key(runner_id=runner_id)
142
+ )
143
+ logger.info(f"Starting runner with ID '{runner_id}' using request-queue '{req_queue.name}'")
144
+ runner_status.set(
145
+ value=RunnerStatus.STARTED.get_val(),
146
+ expire=None,
147
+ )
148
+ runner_loop = Future(
149
+ function=self._runner_loop,
150
+ runner=runner,
151
+ req_queue=req_queue,
152
+ lifespan=spec.lifetime,
153
+ timeout=spec.timeout,
154
+ res_queue=res_queue,
155
+ # The runner_id is used as the future_id for tracking purposes
156
+ future_id=runner_id,
157
+ )
158
+ runner_status.set(
159
+ value=RunnerStatus.RUNNING.get_val(),
160
+ expire=None,
161
+ )
162
+ results = {
163
+ "runner_id": runner_id,
164
+ "runner_started_at": datetime_utcnow().isoformat(),
165
+ "runner_pending_requests": req_queue.size(),
166
+ }
167
+ match runner_loop.wait():
168
+ case EitherMonad.Right(meta):
169
+ results["runner_status"] = "success"
170
+ results["metadata"] = meta
171
+ case EitherMonad.Left(error):
172
+ results["runner_status"] = "failure"
173
+ results["metadata"] = {
174
+ "error": str(error)
175
+ }
176
+ case _:
177
+ results["runner_status"] = "unexpected"
178
+ results["metadata"] = {
179
+ "error": "Unexpected result from runner loop"
180
+ }
181
+ results["pending_requests"] = pending_requests = req_queue.size()
182
+ runner_status.set(
183
+ value=RunnerStatus.STOPPED.get_val(str(pending_requests)),
184
+ expire=3600, # Keep the stopped status for 1 hour
185
+ )
186
+ if pending_requests:
187
+ logger.warning(f"Runner '{runner_id}' stopped with {pending_requests} pending items still in the queue: '{req_queue.name}'")
188
+ else:
189
+ logger.info(f"Runner '{runner_id}' stopped with no pending items in the queue: '{req_queue.name}'")
190
+
191
+ return results
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fred.settings import logger_manager
4
+ from fred.worker.interface import HandlerInterface
5
+ from fred.worker.runner.model.interface import ModelInterface
6
+
7
+ logger = logger_manager.get_logger(name=__name__)
8
+
9
+
10
+ @dataclass(frozen=True, slots=False)
11
+ class Handler(ModelInterface):
12
+ classname: str
13
+ classpath: str
14
+ kwargs: dict
15
+
16
+ def get_handler(self) -> HandlerInterface:
17
+ return HandlerInterface.find_handler(
18
+ handler_classname=self.classname,
19
+ import_pattern=self.classpath,
20
+ **self.kwargs,
21
+ )
@@ -0,0 +1,46 @@
1
+ import json
2
+ import uuid
3
+ from typing import Optional
4
+
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from fred.utils.dateops import datetime_utcnow
8
+ from fred.worker.runner.model._request import RunnerRequest
9
+ from fred.worker.runner.model.interface import ModelInterface
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class RunnerItem(ModelInterface):
14
+ item_id: str
15
+ item_created_at: str
16
+ item_payload: dict
17
+
18
+ @classmethod
19
+ def uuid(cls, payload: dict, uuid_hash: bool = False) -> "RunnerItem":
20
+ item_id = payload.get("item_id") or (
21
+ str(uuid.uuid5(uuid.NAMESPACE_DNS, json.dumps(payload, default=str)))
22
+ if uuid_hash else str(uuid.uuid4())
23
+ )
24
+ return cls(
25
+ item_id=item_id,
26
+ item_created_at=datetime_utcnow().isoformat(),
27
+ item_payload=payload
28
+ )
29
+
30
+ def as_payload(self) -> dict:
31
+ return {
32
+ "item_id": self.item_id,
33
+ "item_created_at": self.item_created_at,
34
+ **self.item_payload,
35
+ }
36
+
37
+ def as_request(
38
+ self,
39
+ use_hash: bool = False,
40
+ request_id: Optional[str] = None
41
+ ) -> RunnerRequest:
42
+ return RunnerRequest.uuid(
43
+ payload=self.as_payload(),
44
+ request_id=request_id,
45
+ uuid_hash=use_hash,
46
+ )
@@ -0,0 +1,41 @@
1
+ import uuid
2
+ import json
3
+ from typing import Optional
4
+
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from fred.dao.comp.catalog import FredQueue
8
+ from fred.worker.runner.model.interface import ModelInterface
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class RunnerRequest(ModelInterface):
13
+ request_id: str
14
+ payload: dict
15
+
16
+ @classmethod
17
+ def uuid(
18
+ cls,
19
+ payload: dict,
20
+ uuid_hash: bool = False,
21
+ request_id: Optional[str] = None,
22
+ ) -> "RunnerRequest":
23
+ request_id = request_id or (
24
+ str(uuid.uuid5(uuid.NAMESPACE_OID, json.dumps(payload, default=str)))
25
+ if uuid_hash else str(uuid.uuid4())
26
+ )
27
+ return cls(request_id=request_id, payload=payload)
28
+
29
+ def as_payload(self) -> dict:
30
+ return {
31
+ "request_id": self.request_id,
32
+ **self.payload,
33
+ }
34
+
35
+ def dispatch(self, request_queue: FredQueue, **kwargs):
36
+ serialization_kwargs = {
37
+ "default": str,
38
+ **kwargs
39
+ }
40
+ request = json.dumps(self.as_payload(), **serialization_kwargs)
41
+ return request_queue.add(request)
@@ -0,0 +1,88 @@
1
+ import uuid
2
+ from dataclasses import dataclass
3
+
4
+ from fred.settings import logger_manager
5
+ from fred.utils.dateops import datetime_utcnow
6
+ from fred.worker.runner.model._handler import Handler
7
+ from fred.worker.runner.model.interface import ModelInterface
8
+ from fred.worker.runner.settings import (
9
+ FRD_RUNNER_REQUEST_QUEUE,
10
+ FRD_RUNNER_RESPONSE_QUEUE,
11
+ )
12
+
13
+ logger = logger_manager.get_logger(name=__name__)
14
+
15
+
16
+
17
+ @dataclass(frozen=True, slots=False)
18
+ class RunnerSpec(ModelInterface):
19
+ runner_id: str
20
+ created_at: str
21
+ queue_slug: str
22
+ inner: Handler
23
+ use_response_queue: bool = False
24
+ lifetime: int = 3600 # Default to 1 hour if not specified
25
+ timeout: int = 30 # Default to 30 seconds if not specified
26
+
27
+ @classmethod
28
+ def auto(cls, **kwargs) -> "RunnerSpec":
29
+ kwargs["runner_id"] = kwargs.get("runner_id", str(uuid.uuid4()))
30
+ return cls.from_payload(payload=kwargs)
31
+
32
+ @classmethod
33
+ def from_payload(cls, payload: dict) -> "RunnerSpec":
34
+ inner_handler_kwargs = {
35
+ "classname": payload.pop("handler_classname", None) or (
36
+ logger.warning("No handler_classname provided; defaulting to: 'HandlerInterface'")
37
+ or "HandlerInterface"
38
+ ),
39
+ "classpath": payload.pop("handler_classpath", None) or (
40
+ logger.warning("No handler_classpath provided; defaulting to: 'fred.worker.interface'")
41
+ or "fred.worker.interface"
42
+ ),
43
+ "kwargs": payload.pop("handler_kwargs", {}),
44
+ **payload.pop("handler_configs", {})
45
+ }
46
+ runner_id = payload.pop("runner_id", None) or (
47
+ logger.warning("No runner_id provided; generating a new one using UUID4.")
48
+ or str(uuid.uuid4())
49
+ )
50
+ return cls(
51
+ runner_id=runner_id,
52
+ created_at=datetime_utcnow().isoformat(),
53
+ queue_slug=payload.pop("queue_slug", None) or (
54
+ logger.warning("Queue slug not specified; defaulting to: 'demo'")
55
+ or "demo"
56
+ ),
57
+ inner=Handler(**inner_handler_kwargs),
58
+ use_response_queue=payload.pop("use_response_queue", False),
59
+ lifetime=payload.pop("lifetime", 3600),
60
+ timeout=payload.pop("timeout", 30),
61
+ )
62
+
63
+ def as_dict(self) -> dict:
64
+ return {
65
+ "runner_id": self.runner_id,
66
+ "created_at": self.created_at,
67
+ "queue_slug": self.queue_slug,
68
+ "handler_classname": self.inner.classname,
69
+ "handler_classpath": self.inner.classpath,
70
+ "handler_kwargs": self.inner.kwargs,
71
+ "use_response_queue": self.use_response_queue,
72
+ "lifetime": self.lifetime,
73
+ "timeout": self.timeout,
74
+ }
75
+
76
+ def as_event(self) -> dict:
77
+ return {
78
+ "id": self.runner_id,
79
+ "input": self.as_dict(),
80
+ }
81
+
82
+ @property
83
+ def request_queue_name(self) -> str:
84
+ return FRD_RUNNER_REQUEST_QUEUE or f"req:{self.queue_slug}"
85
+
86
+ @property
87
+ def response_queue_name(self) -> str:
88
+ return FRD_RUNNER_RESPONSE_QUEUE or f"res:{self.queue_slug}"
@@ -0,0 +1,16 @@
1
+ import enum
2
+
3
+ from fred.worker.runner.model._runner_spec import RunnerSpec
4
+ from fred.worker.runner.model._request import RunnerRequest
5
+ from fred.worker.runner.model._item import RunnerItem
6
+ from fred.worker.runner.model._handler import Handler
7
+
8
+
9
+ class RunnerModelCatalog(enum.Enum):
10
+ RUNNER_SPEC = RunnerSpec
11
+ REQUEST = RunnerRequest
12
+ HANDLER = Handler
13
+ ITEM = RunnerItem
14
+
15
+ def __call__(self, *args, **kwargs):
16
+ return self.value(*args, **kwargs)
@@ -0,0 +1,10 @@
1
+ from fred.settings import (
2
+ logger_manager,
3
+ )
4
+
5
+
6
+ logger = logger_manager.get_logger(name=__name__)
7
+
8
+
9
+ class ModelInterface:
10
+ pass
@@ -0,0 +1,34 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fred.settings import logger_manager
4
+ from fred.worker.runner.info import RunnerInfo
5
+ from fred.worker.runner.handler import RunnerHandler
6
+ from fred.worker.runner.model._runner_spec import RunnerSpec
7
+ from fred.worker.runner.plugins.interface import PluginInterface
8
+
9
+ logger = logger_manager.get_logger(name=__name__)
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class LocalPlugin(PluginInterface):
14
+
15
+ def _execute(
16
+ self,
17
+ spec: RunnerSpec,
18
+ **kwargs
19
+ ):
20
+ """Execute the runner locally by directly invoking the handler's run method.
21
+ This method bypasses any queuing mechanism and runs the handler in the current process.
22
+ Args:
23
+ spec (RunnerSpec): The specification of the runner to execute.
24
+ **kwargs: Additional keyword arguments that may be used for execution.
25
+ """
26
+ outer_handler_classname = kwargs.pop("outer_handler_classname", "RunnerHandler")
27
+ outer_handler_classpath = kwargs.pop("outer_handler_classpath", "fred.worker.runner.handler")
28
+ outer_handler_init_kwargs = kwargs.pop("outer_handler_init_kwargs", {})
29
+ outer_handler = RunnerHandler.find_handler(
30
+ handler_classname=outer_handler_classname,
31
+ import_pattern=outer_handler_classpath,
32
+ **outer_handler_init_kwargs,
33
+ )
34
+ outer_handler.run(event=spec.as_event(), as_future=False)
@@ -10,6 +10,9 @@ logger = logger_manager.get_logger(name=__name__)
10
10
  class PluginCatalog(enum.Enum):
11
11
  """Enum for the different plugins available in FRED."""
12
12
 
13
- LOCAL = LocalPlugin.auto()
13
+ LOCAL = LocalPlugin
14
14
  RUNPOD = None # Placeholder for future RunPod plugin
15
15
  LAMBDA = None # Placeholder for future AWS Lambda plugin
16
+
17
+ def __call__(self, *args, **kwargs):
18
+ return self.value.auto(*args, **kwargs)