fred-oss 0.21.0__tar.gz → 0.22.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.
- {fred_oss-0.21.0/src/main/fred_oss.egg-info → fred_oss-0.22.0}/PKG-INFO +1 -1
- fred_oss-0.22.0/src/main/fred/future/__init__.py +12 -0
- fred_oss-0.22.0/src/main/fred/future/impl.py +204 -0
- fred_oss-0.22.0/src/main/fred/future/result.py +271 -0
- fred_oss-0.22.0/src/main/fred/future/settings.py +18 -0
- fred_oss-0.22.0/src/main/fred/version +1 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0/src/main/fred_oss.egg-info}/PKG-INFO +1 -1
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/SOURCES.txt +4 -0
- fred_oss-0.21.0/src/main/fred/version +0 -1
- {fred_oss-0.21.0 → fred_oss-0.22.0}/MANIFEST.in +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/NOTICE.txt +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/README.md +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/requirements.txt +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/setup.cfg +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/setup.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/__main__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/main.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/_keyval.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/_queue.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/catalog.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/_redis.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/_stdlib.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/catalog.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/utils.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/cli_ext.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtime.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/scanner.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/sync.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/wrappers/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/runpod/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/runpod/cli_ext.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/runpod/helper.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/maturity.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/_either.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/catalog.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/settings.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/utils/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/utils/dateops.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/utils/runtime.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/version.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/client.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/handler.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/info.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/_local.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/catalog.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/cli_ext.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/__init__.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/_runner.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/catalog.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/interface.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/server.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/utils.py +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/dependency_links.txt +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/entry_points.txt +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/requires.txt +0 -0
- {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from fred.maturity import Maturity, MaturityLevel
|
|
2
|
+
from fred.future.impl import Future
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module_maturity = Maturity(
|
|
6
|
+
level=MaturityLevel.ALPHA,
|
|
7
|
+
reference=__name__,
|
|
8
|
+
message=(
|
|
9
|
+
"Fred-Futures implementation is in early development "
|
|
10
|
+
"and therefore currently with incomplete and unstable features."
|
|
11
|
+
)
|
|
12
|
+
)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from threading import Thread
|
|
2
|
+
from typing import (
|
|
3
|
+
Callable,
|
|
4
|
+
Optional,
|
|
5
|
+
TypeVar,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from fred.settings import logger_manager
|
|
9
|
+
from fred.monad.interface import MonadInterface
|
|
10
|
+
from fred.monad.catalog import EitherMonad
|
|
11
|
+
from fred.future.result import (
|
|
12
|
+
FutureResult,
|
|
13
|
+
FutureUndefinedPending,
|
|
14
|
+
FutureUndefinedInProgress,
|
|
15
|
+
FutureDefined,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
A = TypeVar("A")
|
|
19
|
+
B = TypeVar("B")
|
|
20
|
+
|
|
21
|
+
logger = logger_manager.get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Future(MonadInterface[A]):
|
|
25
|
+
"""A Future represents a computation that will complete at some point in the future,
|
|
26
|
+
yielding a result of type A or failing with an exception. It allows for asynchronous
|
|
27
|
+
programming by enabling non-blocking operations and chaining of computations.
|
|
28
|
+
This implementation uses threading to execute the computation in a separate thread.
|
|
29
|
+
|
|
30
|
+
The Future class provides methods to wait for the computation to complete,
|
|
31
|
+
retrieve the result, and chain further computations using flat_map and map.
|
|
32
|
+
|
|
33
|
+
Note: This implementation is a simplified version and may not cover all edge cases
|
|
34
|
+
or provide advanced features found in more comprehensive concurrency libraries.
|
|
35
|
+
|
|
36
|
+
Note: The Future class is designed to work with the Either monad to encapsulate
|
|
37
|
+
successful results (Right) and exceptions (Left).
|
|
38
|
+
|
|
39
|
+
Note: The Future class uses a backend storage system to persist the state
|
|
40
|
+
and result of the computation, allowing for distributed or long-running tasks.
|
|
41
|
+
|
|
42
|
+
Note: The Future class is generic and can be used with any type A, allowing for
|
|
43
|
+
flexibility in the types of computations it can represent.
|
|
44
|
+
|
|
45
|
+
Note: Even though the Future class implements the MonadInterface, it is not a full monad
|
|
46
|
+
in the traditional sense, as it represents an asynchronous computation rather than
|
|
47
|
+
a pure value. However, it provides similar capabilities for chaining and transforming
|
|
48
|
+
computations in a functional style.
|
|
49
|
+
|
|
50
|
+
TODO: Consider adding cancellation support to allow aborting ongoing computations.
|
|
51
|
+
|
|
52
|
+
TODO: Analyze impact on performance and resource usage when using many Futures. Specially
|
|
53
|
+
on garbage collection and memory leaks.
|
|
54
|
+
|
|
55
|
+
TODO: Consider using more advanced concurrency primitives for better control and efficiency.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, function: Callable[..., A], **kwargs):
|
|
59
|
+
"""Initializes a Future with the provided function to be executed asynchronously.
|
|
60
|
+
The function is executed in a separate thread, allowing for non-blocking operations.
|
|
61
|
+
Args:
|
|
62
|
+
function (Callable[..., A]): The function to be executed asynchronously.
|
|
63
|
+
**kwargs: Additional keyword arguments to be passed to the function when executed.
|
|
64
|
+
"""
|
|
65
|
+
# Create a new available future
|
|
66
|
+
future = FutureUndefinedPending.auto()
|
|
67
|
+
# Register the Future-ID and define the available future via the provided function.
|
|
68
|
+
# Note: The 'apply' method is blocking by itself; thus, we run it in a separate thread.
|
|
69
|
+
# Note: The thread is a daemon to ensure it does not block program exit.
|
|
70
|
+
self.future_id = future.future_id
|
|
71
|
+
self.thread = Thread(
|
|
72
|
+
target=lambda: future.apply(function=function, **kwargs),
|
|
73
|
+
daemon=True,
|
|
74
|
+
)
|
|
75
|
+
# Start the thread to execute the function asynchronously
|
|
76
|
+
self.thread.start()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def _result(self) -> Optional[FutureResult[A]]:
|
|
80
|
+
return FutureResult.from_backend(future_id=self.future_id)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def _status(self) -> Optional[str]:
|
|
84
|
+
return FutureResult._get_status_key(future_id=self.future_id).get()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def _output(self) -> Optional[str]:
|
|
88
|
+
return FutureResult._get_output_key(future_id=self.future_id).get()
|
|
89
|
+
|
|
90
|
+
def wait(self, timeout: Optional[float] = None) -> EitherMonad.Either[A]:
|
|
91
|
+
"""Waits for the future to complete and returns the result as an Either monad.
|
|
92
|
+
|
|
93
|
+
TL;DR Waiting will collapse the future into a concrete result
|
|
94
|
+
which can be either a value (Right) or an exception (Left).
|
|
95
|
+
|
|
96
|
+
* If the future completes successfully, returns Right(value).
|
|
97
|
+
* If the future fails, returns Left(exception).
|
|
98
|
+
* If the timeout is reached before completion, raises a TimeoutError.
|
|
99
|
+
Args:
|
|
100
|
+
timeout (Optional[float]): Maximum time to wait for the future to complete.
|
|
101
|
+
If None, waits indefinitely.
|
|
102
|
+
Returns:
|
|
103
|
+
Either[A]: An Either monad containing the result or exception.
|
|
104
|
+
Raises:
|
|
105
|
+
TimeoutError: If the future does not complete within the specified timeout.
|
|
106
|
+
RuntimeError: If the future status is inconsistent after waiting.
|
|
107
|
+
TypeError: If the future result type is unknown.
|
|
108
|
+
"""
|
|
109
|
+
# Wait for the thread to complete or timeout
|
|
110
|
+
self.thread.join(timeout=timeout)
|
|
111
|
+
if self.thread.is_alive():
|
|
112
|
+
raise TimeoutError("Future did not complete within the specified timeout.")
|
|
113
|
+
# After the thread has completed, check the status consistency
|
|
114
|
+
if not self._status:
|
|
115
|
+
raise RuntimeError("Future status should be set but isn't.")
|
|
116
|
+
if not self._status.startswith("DEFINED"):
|
|
117
|
+
raise RuntimeError("Future status should be DEFINED after wait.")
|
|
118
|
+
# Check the result-type consistency
|
|
119
|
+
# If the future is still pending or in-progress, it's an error and should not happen
|
|
120
|
+
# If the future is defined, return the contained value or exception
|
|
121
|
+
match self._result:
|
|
122
|
+
case FutureUndefinedPending():
|
|
123
|
+
raise RuntimeError("Future is still pending after wait.")
|
|
124
|
+
case FutureUndefinedInProgress():
|
|
125
|
+
raise RuntimeError("Future is still in progress after wait.")
|
|
126
|
+
case FutureDefined(value=value):
|
|
127
|
+
return value
|
|
128
|
+
case _:
|
|
129
|
+
raise TypeError("Unknown FutureResult type")
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def state(self) -> Optional[str]:
|
|
133
|
+
"""Returns the current state of the future as a string.
|
|
134
|
+
The state is derived from the status key in the backend storage.
|
|
135
|
+
Returns:
|
|
136
|
+
Optional[str]: The current state of the future, or None if not available.
|
|
137
|
+
Possible states include:
|
|
138
|
+
- "UNDEFINED:PENDING"
|
|
139
|
+
- "UNDEFINED:IN_PROGRESS"
|
|
140
|
+
- "DEFINED:SUCCESS"
|
|
141
|
+
- "DEFINED:FAILURE"
|
|
142
|
+
"""
|
|
143
|
+
return ":".join(self._status.split(":")[:2]) if self._status else None
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
return f"FUTURE[{self.state}]('{self.future_id}')"
|
|
147
|
+
|
|
148
|
+
def __str__(self) -> str:
|
|
149
|
+
return self.future_id
|
|
150
|
+
|
|
151
|
+
def wait_and_resolve(self, timeout: Optional[float] = None) -> A:
|
|
152
|
+
"""Waits for the future to complete and resolves the result into
|
|
153
|
+
the expected output type of the function call; raises if the future failed.
|
|
154
|
+
This is a convenience method that combines waiting for the future to complete
|
|
155
|
+
and resolving the result, raising any exceptions that occurred during execution.
|
|
156
|
+
Args:
|
|
157
|
+
timeout (Optional[float]): Maximum time to wait for the future to complete.
|
|
158
|
+
If None, waits indefinitely.
|
|
159
|
+
Returns:
|
|
160
|
+
A: The resolved value of the future if it completed successfully.
|
|
161
|
+
Raises:
|
|
162
|
+
TimeoutError: If the future does not complete within the specified timeout.
|
|
163
|
+
Exception: If the future failed during execution, the original exception is raised.
|
|
164
|
+
"""
|
|
165
|
+
return self.wait(timeout=timeout).resolve()
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_value(cls, val: A) -> 'Future[A]':
|
|
169
|
+
return Future(function=lambda: val)
|
|
170
|
+
|
|
171
|
+
def flat_map(self, function: Callable[[A], 'Future[B]'], timeout: Optional[float] = None) -> 'Future[B]':
|
|
172
|
+
"""Chains the current future with another future-producing function.
|
|
173
|
+
This method allows for sequential asynchronous operations where the output
|
|
174
|
+
of one future is used as the input to another.
|
|
175
|
+
|
|
176
|
+
The simple implementation of this method is: function(self.wait_and_resolve())
|
|
177
|
+
Nonetheless, the simple implementation would block the current thread
|
|
178
|
+
until the first future is resolved, which is not ideal in an asynchronous context.
|
|
179
|
+
Instead, we create a new Future that encapsulates the entire operation,
|
|
180
|
+
allowing it to be executed asynchronously.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
function (Callable[[A], Future[B]]): A function that takes the result of the
|
|
184
|
+
current future and returns a new Future.
|
|
185
|
+
timeout (Optional[float]): Maximum time to wait for the current future to complete.
|
|
186
|
+
If None, waits indefinitely.
|
|
187
|
+
Returns:
|
|
188
|
+
Future[B]: A new Future representing the chained operation."""
|
|
189
|
+
# TODO: Is there a more efficient implementation?
|
|
190
|
+
return Future(
|
|
191
|
+
function=lambda:
|
|
192
|
+
function(self.wait_and_resolve(timeout=timeout))
|
|
193
|
+
.wait_and_resolve(timeout=timeout)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def map(self, function: Callable[[A], B]) -> 'Future[B]':
|
|
197
|
+
"""Applies a function to the result of the future, returning a new future.
|
|
198
|
+
This method allows for transforming the result of a future without blocking.
|
|
199
|
+
Args:
|
|
200
|
+
function (Callable[[A], B]): A function that takes the result of the current future
|
|
201
|
+
and returns a new value.
|
|
202
|
+
Returns: Future[B]: A new Future containing the transformed result.
|
|
203
|
+
"""
|
|
204
|
+
return self.flat_map(function=lambda value: type(self).from_value(function(value)))
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from time import perf_counter
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import (
|
|
5
|
+
Callable,
|
|
6
|
+
Generic,
|
|
7
|
+
Optional,
|
|
8
|
+
TypeVar,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from fred.settings import (
|
|
12
|
+
get_environ_variable,
|
|
13
|
+
logger_manager,
|
|
14
|
+
)
|
|
15
|
+
from fred.future.settings import (
|
|
16
|
+
FRD_FUTURE_BACKEND,
|
|
17
|
+
FRD_FUTURE_DEFAULT_EXPIRATION,
|
|
18
|
+
FRD_FUTURE_DEFAULT_TIMEOUT,
|
|
19
|
+
)
|
|
20
|
+
from fred.utils.dateops import datetime_utcnow
|
|
21
|
+
from fred.dao.service.catalog import ServiceCatalog
|
|
22
|
+
from fred.dao.comp.catalog import FredKeyVal
|
|
23
|
+
from fred.monad.catalog import EitherMonad
|
|
24
|
+
|
|
25
|
+
logger = logger_manager.get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
A = TypeVar("A")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FutureBackend:
|
|
31
|
+
keyval: type[FredKeyVal]
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def with_backend(cls, service: ServiceCatalog, **kwargs) -> type['FutureBackend']:
|
|
35
|
+
"""Dynamically creates a FutureBackend subclass with the specified service backend.
|
|
36
|
+
This method uses the service catalog to fetch the appropriate components for the
|
|
37
|
+
given service and constructs a new subclass of FutureBackend with those components.
|
|
38
|
+
Args:
|
|
39
|
+
service (ServiceCatalog): The service catalog entry representing the backend service.
|
|
40
|
+
**kwargs: Additional keyword arguments to pass to the component catalog.
|
|
41
|
+
Returns:
|
|
42
|
+
type[FutureBackend]: A new subclass of FutureBackend configured with the specified backend.
|
|
43
|
+
"""
|
|
44
|
+
components = service.component_catalog(**kwargs)
|
|
45
|
+
return type(
|
|
46
|
+
f"{service.name.title()}{cls.__name__}",
|
|
47
|
+
(cls,),
|
|
48
|
+
{
|
|
49
|
+
"keyval": components.KEYVAL.value
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def infer_backend(cls, env: Optional[str] = None, **kwargs) -> type['FutureBackend']:
|
|
55
|
+
"""Infers the backend service from the environment variable or defaults to FRD_FUTURE_BACKEND.
|
|
56
|
+
This method checks for an environment variable to determine the backend service. If the
|
|
57
|
+
environment variable is not set, it defaults to the value of FRD_FUTURE_BACKEND. It then
|
|
58
|
+
uses the inferred service to create a FutureBackend subclass with the appropriate components.
|
|
59
|
+
Args:
|
|
60
|
+
env (Optional[str]): The name of the environment variable to check for the backend service.
|
|
61
|
+
If None, defaults to FRD_FUTURE_BACKEND.
|
|
62
|
+
**kwargs: Additional keyword arguments to pass to the component catalog.
|
|
63
|
+
Returns:
|
|
64
|
+
type[FutureBackend]: A new subclass of FutureBackend configured with the inferred backend.
|
|
65
|
+
"""
|
|
66
|
+
service_key = get_environ_variable(env, default=ServiceCatalog.STDLIB.name) \
|
|
67
|
+
if env else FRD_FUTURE_BACKEND
|
|
68
|
+
return cls.with_backend(
|
|
69
|
+
service=ServiceCatalog[service_key.upper()],
|
|
70
|
+
**kwargs
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=False)
|
|
75
|
+
class FutureResult(Generic[A], FutureBackend.infer_backend()):
|
|
76
|
+
"""Represents the result of an asynchronous computation (Future).
|
|
77
|
+
This class provides methods to manage and retrieve the status and output of a Future.
|
|
78
|
+
It uses a key-value store to persist the state and result of the Future, allowing for
|
|
79
|
+
retrieval and management of asynchronous tasks."""
|
|
80
|
+
future_id: str
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _get_future_keyname(future_id: str) -> str:
|
|
84
|
+
return ":".join(["frd", "future", future_id])
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def future_keyname(self) -> str:
|
|
88
|
+
return self._get_future_keyname(future_id=self.future_id)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def _get_status_key(cls, future_id: str) -> FredKeyVal:
|
|
92
|
+
return cls.keyval(key=":".join([cls._get_future_keyname(future_id=future_id), "status"]))
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def status(self) -> FredKeyVal:
|
|
96
|
+
return self._get_status_key(future_id=self.future_id)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def _get_output_key(cls, future_id: str) -> FredKeyVal:
|
|
100
|
+
return cls.keyval(key=":".join([cls._get_future_keyname(future_id=future_id), "output"]))
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def output(self) -> FredKeyVal:
|
|
104
|
+
return self._get_output_key(future_id=self.future_id)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def _get_obj_key(cls, future_id: str) -> FredKeyVal:
|
|
108
|
+
return cls.keyval(key=":".join([cls._get_future_keyname(future_id=future_id), "obj"]))
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def obj(self) -> FredKeyVal:
|
|
112
|
+
return self._get_obj_key(future_id=self.future_id)
|
|
113
|
+
|
|
114
|
+
def stringify(self) -> str:
|
|
115
|
+
import pickle
|
|
116
|
+
import base64
|
|
117
|
+
return base64.b64encode(pickle.dumps(self)).decode("ascii")
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_string(cls, payload: str) -> 'FutureResult[A]':
|
|
121
|
+
import pickle
|
|
122
|
+
import base64
|
|
123
|
+
return pickle.loads(base64.b64decode(payload))
|
|
124
|
+
|
|
125
|
+
def _from_backend(self) -> Optional['FutureResult[A]']:
|
|
126
|
+
payload = self.obj.get()
|
|
127
|
+
if not payload:
|
|
128
|
+
return
|
|
129
|
+
return self.from_string(payload=payload)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_backend(cls, future_id: str) -> Optional['FutureResult[A]']:
|
|
133
|
+
return FutureResult(future_id=future_id)._from_backend()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=False)
|
|
137
|
+
class FutureUndefinedPending(FutureResult[A]):
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def auto(cls, **kwargs) -> 'FutureUndefinedPending[A]':
|
|
141
|
+
"""Creates a FutureUndefinedPending instance with an auto-generated future_id.
|
|
142
|
+
This method generates a unique identifier for the future_id if one is not provided
|
|
143
|
+
in the keyword arguments. It ensures that each instance has a distinct future_id,
|
|
144
|
+
which is essential for tracking and managing asynchronous tasks.
|
|
145
|
+
Args:
|
|
146
|
+
**kwargs: Additional keyword arguments to pass to the FutureUndefinedPending constructor.
|
|
147
|
+
Returns:
|
|
148
|
+
FutureUndefinedPending[A]: A new instance of FutureUndefinedPending with a unique future_id.
|
|
149
|
+
"""
|
|
150
|
+
import uuid
|
|
151
|
+
future_id = kwargs.pop("future_id", None) or str(uuid.uuid4())
|
|
152
|
+
return FutureUndefinedPending[A](future_id=future_id, **kwargs)
|
|
153
|
+
|
|
154
|
+
def __post_init__(self):
|
|
155
|
+
logger.debug(f"Future[{self.future_id}] initialized and pending execution")
|
|
156
|
+
self.status.set(
|
|
157
|
+
value=f"UNDEFINED:PENDING:{datetime_utcnow().isoformat()}",
|
|
158
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
159
|
+
)
|
|
160
|
+
self.obj.set(
|
|
161
|
+
value=self.stringify(),
|
|
162
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def apply(self, function: Callable[..., A], **kwargs) -> 'FutureDefined[A]':
|
|
166
|
+
"""Applies a function to the Future, transitioning it from pending to in-progress and finalizing as defined.
|
|
167
|
+
This method executes the provided function with the given keyword arguments,
|
|
168
|
+
transitioning the Future from a pending state to an in-progress state. It captures
|
|
169
|
+
the result of the function execution, which can be either a successful value or an
|
|
170
|
+
exception, and returns a FutureDefined instance representing the completed state.
|
|
171
|
+
Args:
|
|
172
|
+
function (Callable[..., A]): The function to execute, which should return a value of type A.
|
|
173
|
+
**kwargs: Additional keyword arguments to pass to the function during execution.
|
|
174
|
+
Returns:
|
|
175
|
+
FutureDefined[A]: A new instance of FutureDefined representing the completed state of the Future.
|
|
176
|
+
"""
|
|
177
|
+
fip = FutureUndefinedInProgress[A](
|
|
178
|
+
future_id=self.future_id,
|
|
179
|
+
started_at=perf_counter(),
|
|
180
|
+
function_name=function.__name__,
|
|
181
|
+
)
|
|
182
|
+
return fip.exec(function=function, **kwargs)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True, slots=False)
|
|
186
|
+
class FutureUndefinedInProgress(FutureResult[A]):
|
|
187
|
+
started_at: float
|
|
188
|
+
function_name: str
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def __post_init__(self):
|
|
192
|
+
logger.debug(f"Future[{self.future_id}] started execution of function '{self.function_name}'")
|
|
193
|
+
self.status.set(
|
|
194
|
+
value=f"UNDEFINED:IN_PROGRESS:{datetime_utcnow().isoformat()}",
|
|
195
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
196
|
+
)
|
|
197
|
+
self.obj.set(
|
|
198
|
+
value=self.stringify(),
|
|
199
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def exec(self, function: Callable[..., A], fail: bool = False, **kwargs) -> 'FutureDefined[A]':
|
|
203
|
+
"""Executes the function associated with the Future, capturing its result or exception.
|
|
204
|
+
This method runs the provided function with the given keyword arguments, capturing
|
|
205
|
+
its output. If the function executes successfully, it wraps the result in a Right
|
|
206
|
+
monad; if it raises an exception, it captures the exception in a Left monad.
|
|
207
|
+
The method returns a FutureDefined instance containing the result of the execution.
|
|
208
|
+
If the 'fail' parameter is set to True, any exception raised during function execution
|
|
209
|
+
will be propagated instead of being captured in the Left monad.
|
|
210
|
+
Args:
|
|
211
|
+
function (Callable[..., A]): The function to execute, which should return a value of type A.
|
|
212
|
+
fail (bool): If True, exceptions raised during function execution will be propagated. Defaults to False.
|
|
213
|
+
**kwargs: Additional keyword arguments to pass to the function during execution.
|
|
214
|
+
Returns:
|
|
215
|
+
FutureDefined[A]: A new instance of FutureDefined representing the completed state of the Future.
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
ok = False
|
|
219
|
+
match function(**kwargs):
|
|
220
|
+
case EitherMonad.Right(value=value):
|
|
221
|
+
ok = True
|
|
222
|
+
value =EitherMonad.Right.from_value(val=value)
|
|
223
|
+
case EitherMonad.Left(exception=exception):
|
|
224
|
+
ok = False
|
|
225
|
+
value = EitherMonad.Left.from_value(val=exception)
|
|
226
|
+
case value:
|
|
227
|
+
ok = True
|
|
228
|
+
value = EitherMonad.Right.from_value(val=value)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
ok = False
|
|
231
|
+
value = EitherMonad.Left.from_value(val=e)
|
|
232
|
+
if fail:
|
|
233
|
+
raise e
|
|
234
|
+
return FutureDefined(
|
|
235
|
+
future_id=self.future_id,
|
|
236
|
+
value=value,
|
|
237
|
+
ok=ok,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass(frozen=True, slots=False)
|
|
242
|
+
class FutureDefined(FutureResult[A]):
|
|
243
|
+
value: EitherMonad.Either[A]
|
|
244
|
+
ok: bool
|
|
245
|
+
|
|
246
|
+
def __post_init__(self):
|
|
247
|
+
state = "SUCCESS" if self.ok else "FAILURE"
|
|
248
|
+
logger.debug(f"FutureDefined[{self.future_id}] completed with state: {state}")
|
|
249
|
+
self.status.set(
|
|
250
|
+
value=f"DEFINED:{state}:{datetime_utcnow().isoformat()}",
|
|
251
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
252
|
+
)
|
|
253
|
+
self.obj.set(
|
|
254
|
+
value=self.stringify(),
|
|
255
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
256
|
+
)
|
|
257
|
+
match self.value:
|
|
258
|
+
case EitherMonad.Right(value=value):
|
|
259
|
+
# TODO: Consider using a more robust serialization method...
|
|
260
|
+
# TODO: Should we consider collapsing nested-futures? i.e., Future[Future[A]] => Future[A]
|
|
261
|
+
payload = json.dumps(value)
|
|
262
|
+
self.output.set(
|
|
263
|
+
value=payload,
|
|
264
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
265
|
+
)
|
|
266
|
+
case EitherMonad.Left(exception=exception):
|
|
267
|
+
payload = json.dumps({"error": str(exception)})
|
|
268
|
+
self.output.set(
|
|
269
|
+
value=str(exception),
|
|
270
|
+
expire=FRD_FUTURE_DEFAULT_EXPIRATION
|
|
271
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from fred.settings import get_environ_variable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
FRD_FUTURE_BACKEND = get_environ_variable(
|
|
5
|
+
"FRD_FUTURE_BACKEND",
|
|
6
|
+
default="STDLIB",
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
FRD_FUTURE_DEFAULT_EXPIRATION = int(get_environ_variable(
|
|
10
|
+
"FRD_FUTURE_DEFAULT_EXPIRATION",
|
|
11
|
+
default="3600", # 1 hour
|
|
12
|
+
))
|
|
13
|
+
|
|
14
|
+
# TODO: This is currently not used, but reserved for future use...
|
|
15
|
+
FRD_FUTURE_DEFAULT_TIMEOUT = int(get_environ_variable(
|
|
16
|
+
"FRD_FUTURE_DEFAULT_TIMEOUT",
|
|
17
|
+
default="900", # 15 minutes
|
|
18
|
+
))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.22.0
|
|
@@ -23,6 +23,10 @@ src/main/fred/dao/service/_stdlib.py
|
|
|
23
23
|
src/main/fred/dao/service/catalog.py
|
|
24
24
|
src/main/fred/dao/service/interface.py
|
|
25
25
|
src/main/fred/dao/service/utils.py
|
|
26
|
+
src/main/fred/future/__init__.py
|
|
27
|
+
src/main/fred/future/impl.py
|
|
28
|
+
src/main/fred/future/result.py
|
|
29
|
+
src/main/fred/future/settings.py
|
|
26
30
|
src/main/fred/integrations/databricks/__init__.py
|
|
27
31
|
src/main/fred/integrations/databricks/cli_ext.py
|
|
28
32
|
src/main/fred/integrations/databricks/runtime.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.21.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/__init__.py
RENAMED
|
File without changes
|
{fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
{fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/wrappers/__init__.py
RENAMED
|
File without changes
|
{fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|