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.
Files changed (74) hide show
  1. {fred_oss-0.21.0/src/main/fred_oss.egg-info → fred_oss-0.22.0}/PKG-INFO +1 -1
  2. fred_oss-0.22.0/src/main/fred/future/__init__.py +12 -0
  3. fred_oss-0.22.0/src/main/fred/future/impl.py +204 -0
  4. fred_oss-0.22.0/src/main/fred/future/result.py +271 -0
  5. fred_oss-0.22.0/src/main/fred/future/settings.py +18 -0
  6. fred_oss-0.22.0/src/main/fred/version +1 -0
  7. {fred_oss-0.21.0 → fred_oss-0.22.0/src/main/fred_oss.egg-info}/PKG-INFO +1 -1
  8. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/SOURCES.txt +4 -0
  9. fred_oss-0.21.0/src/main/fred/version +0 -1
  10. {fred_oss-0.21.0 → fred_oss-0.22.0}/MANIFEST.in +0 -0
  11. {fred_oss-0.21.0 → fred_oss-0.22.0}/NOTICE.txt +0 -0
  12. {fred_oss-0.21.0 → fred_oss-0.22.0}/README.md +0 -0
  13. {fred_oss-0.21.0 → fred_oss-0.22.0}/requirements.txt +0 -0
  14. {fred_oss-0.21.0 → fred_oss-0.22.0}/setup.cfg +0 -0
  15. {fred_oss-0.21.0 → fred_oss-0.22.0}/setup.py +0 -0
  16. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/__init__.py +0 -0
  17. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/__main__.py +0 -0
  18. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/interface.py +0 -0
  19. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/cli/main.py +0 -0
  20. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/__init__.py +0 -0
  21. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/__init__.py +0 -0
  22. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/_keyval.py +0 -0
  23. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/_queue.py +0 -0
  24. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/catalog.py +0 -0
  25. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/comp/interface.py +0 -0
  26. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/__init__.py +0 -0
  27. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/_redis.py +0 -0
  28. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/_stdlib.py +0 -0
  29. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/catalog.py +0 -0
  30. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/interface.py +0 -0
  31. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/dao/service/utils.py +0 -0
  32. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/__init__.py +0 -0
  33. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/cli_ext.py +0 -0
  34. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtime.py +0 -0
  35. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/__init__.py +0 -0
  36. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/scanner.py +0 -0
  37. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/runtimes/sync.py +0 -0
  38. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/wrappers/__init__.py +0 -0
  39. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py +0 -0
  40. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/runpod/__init__.py +0 -0
  41. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/runpod/cli_ext.py +0 -0
  42. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/integrations/runpod/helper.py +0 -0
  43. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/maturity.py +0 -0
  44. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/__init__.py +0 -0
  45. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/_either.py +0 -0
  46. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/catalog.py +0 -0
  47. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/monad/interface.py +0 -0
  48. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/settings.py +0 -0
  49. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/utils/__init__.py +0 -0
  50. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/utils/dateops.py +0 -0
  51. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/utils/runtime.py +0 -0
  52. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/version.py +0 -0
  53. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/__init__.py +0 -0
  54. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/interface.py +0 -0
  55. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/__init__.py +0 -0
  56. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/client.py +0 -0
  57. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/handler.py +0 -0
  58. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/info.py +0 -0
  59. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/__init__.py +0 -0
  60. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/_local.py +0 -0
  61. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/catalog.py +0 -0
  62. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/plugins/interface.py +0 -0
  63. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/__init__.py +0 -0
  64. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/cli_ext.py +0 -0
  65. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/__init__.py +0 -0
  66. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/_runner.py +0 -0
  67. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/catalog.py +0 -0
  68. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/routers/interface.py +0 -0
  69. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/rest/server.py +0 -0
  70. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred/worker/runner/utils.py +0 -0
  71. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/dependency_links.txt +0 -0
  72. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/entry_points.txt +0 -0
  73. {fred_oss-0.21.0 → fred_oss-0.22.0}/src/main/fred_oss.egg-info/requires.txt +0 -0
  74. {fred_oss-0.21.0 → fred_oss-0.22.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.21.0
3
+ Version: 0.22.0
4
4
  Summary: FREDOSS
5
5
  Home-page: https://fred.fahera.mx
6
6
  Author: Fahera Research, Education, and Development
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-oss
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: FREDOSS
5
5
  Home-page: https://fred.fahera.mx
6
6
  Author: Fahera Research, Education, and Development
@@ -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