ccflow-celery 0.1.0__py3-none-any.whl
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.
- ccflow_celery/__init__.py +4 -0
- ccflow_celery/app.py +61 -0
- ccflow_celery/config/__init__.py +32 -0
- ccflow_celery/config/base.yaml +2 -0
- ccflow_celery/config/evaluator/ccflow_celery.yaml +18 -0
- ccflow_celery/config/evaluator/ccflow_celery_eager.yaml +16 -0
- ccflow_celery/evaluators.py +216 -0
- ccflow_celery/examples/__init__.py +1 -0
- ccflow_celery/examples/fibonacci.py +79 -0
- ccflow_celery/tasks.py +42 -0
- ccflow_celery/tests/test_all.py +340 -0
- ccflow_celery-0.1.0.dist-info/METADATA +53 -0
- ccflow_celery-0.1.0.dist-info/RECORD +15 -0
- ccflow_celery-0.1.0.dist-info/WHEEL +4 -0
- ccflow_celery-0.1.0.dist-info/licenses/LICENSE +201 -0
ccflow_celery/app.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from ccflow import BaseModel
|
|
4
|
+
from celery import Celery
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"CeleryConfig",
|
|
9
|
+
"CeleryApp",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CeleryConfig(BaseModel):
|
|
14
|
+
"""Configuration for a Celery application."""
|
|
15
|
+
|
|
16
|
+
broker_url: str = Field(default="redis://localhost:6379/0", description="Celery broker URL")
|
|
17
|
+
result_backend: Optional[str] = Field(default="redis://localhost:6379/0", description="Celery result backend URL")
|
|
18
|
+
task_serializer: str = "json"
|
|
19
|
+
result_serializer: str = "json"
|
|
20
|
+
accept_content: list = Field(default_factory=lambda: ["json"])
|
|
21
|
+
task_track_started: bool = True
|
|
22
|
+
task_default_queue: str = "default"
|
|
23
|
+
task_routes: Optional[Dict[str, str]] = None
|
|
24
|
+
worker_concurrency: Optional[int] = None
|
|
25
|
+
task_always_eager: bool = Field(default=False, description="If True, tasks execute locally without a broker")
|
|
26
|
+
task_eager_propagates: bool = Field(default=True, description="If True, eager tasks propagate exceptions")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CeleryApp(BaseModel):
|
|
30
|
+
"""Wrapper around a Celery application instance."""
|
|
31
|
+
|
|
32
|
+
name: str = Field(default="ccflow", description="Celery app name")
|
|
33
|
+
config: CeleryConfig = Field(default_factory=CeleryConfig)
|
|
34
|
+
|
|
35
|
+
_app: Any = None
|
|
36
|
+
|
|
37
|
+
def get_app(self):
|
|
38
|
+
if self._app is None:
|
|
39
|
+
self._app = Celery(self.name)
|
|
40
|
+
self._app.conf.update(
|
|
41
|
+
broker_url=self.config.broker_url,
|
|
42
|
+
result_backend=self.config.result_backend,
|
|
43
|
+
task_serializer=self.config.task_serializer,
|
|
44
|
+
result_serializer=self.config.result_serializer,
|
|
45
|
+
accept_content=self.config.accept_content,
|
|
46
|
+
task_track_started=self.config.task_track_started,
|
|
47
|
+
task_default_queue=self.config.task_default_queue,
|
|
48
|
+
)
|
|
49
|
+
if self.config.task_routes:
|
|
50
|
+
self._app.conf.task_routes = self.config.task_routes
|
|
51
|
+
if self.config.worker_concurrency:
|
|
52
|
+
self._app.conf.worker_concurrency = self.config.worker_concurrency
|
|
53
|
+
if self.config.task_always_eager:
|
|
54
|
+
self._app.conf.task_always_eager = True
|
|
55
|
+
self._app.conf.task_eager_propagates = self.config.task_eager_propagates
|
|
56
|
+
# Register the default task so it can be invoked by name
|
|
57
|
+
# (required for eager mode; harmless in broker mode)
|
|
58
|
+
from .tasks import execute_model_task
|
|
59
|
+
|
|
60
|
+
self._app.task(execute_model_task, name="ccflow_celery.tasks.execute_model_task")
|
|
61
|
+
return self._app
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from ccflow import ModelRegistry
|
|
5
|
+
from ccflow.utils.hydra import ConfigLoadResult, load_config as base_load_config
|
|
6
|
+
|
|
7
|
+
__all__ = ("load_config",)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_config(
|
|
11
|
+
overrides: Optional[List[str]] = None,
|
|
12
|
+
overwrite: bool = False,
|
|
13
|
+
config_dir: Optional[str] = None,
|
|
14
|
+
config_key: Optional[str] = None,
|
|
15
|
+
version_base: Optional[str] = None,
|
|
16
|
+
) -> ConfigLoadResult:
|
|
17
|
+
parent_dir = str(Path(__file__).resolve().parent)
|
|
18
|
+
result = base_load_config(
|
|
19
|
+
root_config_dir=parent_dir,
|
|
20
|
+
root_config_name="base",
|
|
21
|
+
config_dir=config_dir,
|
|
22
|
+
overrides=overrides,
|
|
23
|
+
version_base=version_base,
|
|
24
|
+
basepath=parent_dir,
|
|
25
|
+
debug=False,
|
|
26
|
+
)
|
|
27
|
+
cfg = result.cfg
|
|
28
|
+
if config_key is not None:
|
|
29
|
+
cfg = cfg[config_key]
|
|
30
|
+
r = ModelRegistry.root()
|
|
31
|
+
r.load_config(cfg, overwrite=overwrite)
|
|
32
|
+
return r
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @package cli.model
|
|
2
|
+
# Celery evaluator — distributed graph execution via Celery workers.
|
|
3
|
+
# Apply via Hydra override: evaluator=ccflow_celery
|
|
4
|
+
#
|
|
5
|
+
# Requires a running Celery broker (e.g. Redis) and result backend.
|
|
6
|
+
# Set broker/backend URLs via environment variables or override in this file.
|
|
7
|
+
evaluator:
|
|
8
|
+
_target_: ccflow.evaluators.MultiEvaluator
|
|
9
|
+
evaluators:
|
|
10
|
+
- _target_: ccflow_celery.CeleryGraphEvaluator
|
|
11
|
+
celery_app:
|
|
12
|
+
_target_: ccflow_celery.CeleryApp
|
|
13
|
+
name: ccflow
|
|
14
|
+
config:
|
|
15
|
+
_target_: ccflow_celery.CeleryConfig
|
|
16
|
+
broker_url: ${oc.env:CELERY_BROKER_URL,redis://localhost:6379/0}
|
|
17
|
+
result_backend: ${oc.env:CELERY_RESULT_BACKEND,redis://localhost:6379/1}
|
|
18
|
+
- _target_: ccflow.evaluators.MemoryCacheEvaluator
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @package cli.model
|
|
2
|
+
# Celery evaluator in eager mode — tasks run locally, no broker required.
|
|
3
|
+
# Apply via Hydra override: evaluator=ccflow_celery_eager
|
|
4
|
+
evaluator:
|
|
5
|
+
_target_: ccflow.evaluators.MultiEvaluator
|
|
6
|
+
evaluators:
|
|
7
|
+
- _target_: ccflow_celery.CeleryGraphEvaluator
|
|
8
|
+
app:
|
|
9
|
+
_target_: ccflow_celery.CeleryApp
|
|
10
|
+
name: ccflow
|
|
11
|
+
config:
|
|
12
|
+
_target_: ccflow_celery.CeleryConfig
|
|
13
|
+
broker_url: "memory://"
|
|
14
|
+
result_backend: "cache+memory://"
|
|
15
|
+
task_always_eager: true
|
|
16
|
+
- _target_: ccflow.evaluators.MemoryCacheEvaluator
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import graphlib
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from ccflow import EvaluatorBase, GenericResult
|
|
6
|
+
from ccflow.callable import ModelEvaluationContext
|
|
7
|
+
from ccflow.evaluators.common import get_dependency_graph
|
|
8
|
+
from celery import group
|
|
9
|
+
from pydantic import Field, PrivateAttr
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
|
|
12
|
+
from .app import CeleryApp
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"CeleryEvaluator",
|
|
16
|
+
"CeleryGraphEvaluator",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_log = getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_origin(cls):
|
|
23
|
+
"""Resolve parameterized generics (e.g. GenericContext[int]) to their origin class."""
|
|
24
|
+
meta = getattr(cls, "__pydantic_generic_metadata__", None)
|
|
25
|
+
if meta and meta.get("origin") is not None:
|
|
26
|
+
return meta["origin"]
|
|
27
|
+
return cls
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _model_fqn(model) -> str:
|
|
31
|
+
"""Get the fully qualified class name of a model."""
|
|
32
|
+
cls = _resolve_origin(type(model))
|
|
33
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _context_fqn(context) -> str:
|
|
37
|
+
"""Get the fully qualified class name of a context."""
|
|
38
|
+
cls = _resolve_origin(type(context))
|
|
39
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _serialize_model(model):
|
|
43
|
+
"""Serialize a model to (fqn, config_dict) for Celery task dispatch."""
|
|
44
|
+
fqn = _model_fqn(model)
|
|
45
|
+
config = model.model_dump(exclude={"type_"}) if hasattr(model, "model_dump") else {}
|
|
46
|
+
return fqn, config
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _serialize_context(ctx):
|
|
50
|
+
"""Serialize a context to (fqn, config_dict) for Celery task dispatch."""
|
|
51
|
+
fqn = _context_fqn(ctx)
|
|
52
|
+
config = ctx.model_dump(exclude={"type_"}) if hasattr(ctx, "model_dump") else {}
|
|
53
|
+
return fqn, config
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _unwrap_eval_ctx(eval_ctx):
|
|
57
|
+
"""Unwrap evaluator-wrapping layers to get the actual model and context.
|
|
58
|
+
|
|
59
|
+
The dependency graph may wrap nodes as ModelEvaluationContext(model=evaluator,
|
|
60
|
+
context=ModelEvaluationContext(model=actual_model, context=actual_context)).
|
|
61
|
+
"""
|
|
62
|
+
inner = eval_ctx
|
|
63
|
+
while isinstance(inner.context, ModelEvaluationContext):
|
|
64
|
+
inner = inner.context
|
|
65
|
+
return inner.model, inner.context
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CeleryEvaluator(EvaluatorBase):
|
|
69
|
+
"""Evaluator that dispatches model execution to Celery workers.
|
|
70
|
+
|
|
71
|
+
Serializes the model and context, submits as a Celery task,
|
|
72
|
+
and waits for the result. The model must be reconstructable from its
|
|
73
|
+
Pydantic config dump.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
app: CeleryApp = Field(default_factory=CeleryApp)
|
|
77
|
+
timeout: Optional[float] = Field(default=300.0, description="Timeout in seconds for waiting on task result")
|
|
78
|
+
task_name: str = Field(default="ccflow_celery.tasks.execute_model_task", description="Registered Celery task name")
|
|
79
|
+
|
|
80
|
+
_celery_app: Any = PrivateAttr(default=None)
|
|
81
|
+
|
|
82
|
+
def _get_celery_app(self):
|
|
83
|
+
if self._celery_app is None:
|
|
84
|
+
self._celery_app = self.app.get_app()
|
|
85
|
+
return self._celery_app
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
def __call__(self, context: ModelEvaluationContext) -> Any:
|
|
89
|
+
model = context.model
|
|
90
|
+
ctx = context.context
|
|
91
|
+
|
|
92
|
+
# For __deps__ calls or non-__call__ calls, execute locally
|
|
93
|
+
if hasattr(context, "fn") and context.fn != "__call__":
|
|
94
|
+
return context()
|
|
95
|
+
|
|
96
|
+
# Serialize model and context
|
|
97
|
+
model_class, model_config = _serialize_model(model)
|
|
98
|
+
context_class, context_config = _serialize_context(ctx)
|
|
99
|
+
|
|
100
|
+
celery_app = self._get_celery_app()
|
|
101
|
+
|
|
102
|
+
_log.info("Submitting Celery task: %s(%s)", model_class, context_class)
|
|
103
|
+
|
|
104
|
+
# Send task to Celery
|
|
105
|
+
task = celery_app.send_task(
|
|
106
|
+
self.task_name,
|
|
107
|
+
args=[model_class, model_config, context_class, context_config],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Wait for result
|
|
111
|
+
result_data = task.get(timeout=self.timeout)
|
|
112
|
+
_log.info("Celery task completed: %s", task.id)
|
|
113
|
+
|
|
114
|
+
# Reconstruct result
|
|
115
|
+
if hasattr(model, "result_type"):
|
|
116
|
+
result_cls = model.result_type
|
|
117
|
+
try:
|
|
118
|
+
return result_cls(**result_data)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
return GenericResult(value=result_data)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CeleryGraphEvaluator(EvaluatorBase):
|
|
126
|
+
"""Evaluator that parallelizes DAG execution via Celery.
|
|
127
|
+
|
|
128
|
+
Builds the dependency graph (like GraphEvaluator), then submits
|
|
129
|
+
independent nodes as parallel Celery tasks using Celery groups.
|
|
130
|
+
Nodes that depend on other nodes wait for their dependencies first.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
app: CeleryApp = Field(default_factory=CeleryApp)
|
|
134
|
+
timeout: Optional[float] = Field(default=600.0, description="Timeout for the entire graph execution")
|
|
135
|
+
task_name: str = Field(default="ccflow_celery.tasks.execute_model_task", description="Registered Celery task name")
|
|
136
|
+
|
|
137
|
+
_celery_app: Any = PrivateAttr(default=None)
|
|
138
|
+
_is_evaluating: bool = PrivateAttr(False)
|
|
139
|
+
|
|
140
|
+
def _get_celery_app(self):
|
|
141
|
+
if self._celery_app is None:
|
|
142
|
+
self._celery_app = self.app.get_app()
|
|
143
|
+
return self._celery_app
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
def __call__(self, context: ModelEvaluationContext) -> Any:
|
|
147
|
+
# Avoid re-entrancy
|
|
148
|
+
if self._is_evaluating:
|
|
149
|
+
return context()
|
|
150
|
+
|
|
151
|
+
self._is_evaluating = True
|
|
152
|
+
root_result = None
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
graph = get_dependency_graph(context)
|
|
156
|
+
ts = graphlib.TopologicalSorter(graph.graph)
|
|
157
|
+
|
|
158
|
+
celery_app = self._get_celery_app()
|
|
159
|
+
results_map = {}
|
|
160
|
+
|
|
161
|
+
# Process nodes level by level for maximum parallelism
|
|
162
|
+
ts.prepare()
|
|
163
|
+
while ts.is_active():
|
|
164
|
+
ready_nodes = list(ts.get_ready())
|
|
165
|
+
|
|
166
|
+
# Submit all ready nodes in parallel
|
|
167
|
+
tasks = []
|
|
168
|
+
for key in ready_nodes:
|
|
169
|
+
eval_ctx = graph.ids[key]
|
|
170
|
+
model, ctx = _unwrap_eval_ctx(eval_ctx)
|
|
171
|
+
|
|
172
|
+
model_class, model_config = _serialize_model(model)
|
|
173
|
+
context_class, context_config = _serialize_context(ctx)
|
|
174
|
+
|
|
175
|
+
sig = celery_app.signature(
|
|
176
|
+
self.task_name,
|
|
177
|
+
args=[model_class, model_config, context_class, context_config],
|
|
178
|
+
)
|
|
179
|
+
tasks.append((key, sig))
|
|
180
|
+
|
|
181
|
+
if len(tasks) == 1:
|
|
182
|
+
# Single task — submit directly
|
|
183
|
+
key, sig = tasks[0]
|
|
184
|
+
async_result = sig.apply_async()
|
|
185
|
+
result = async_result.get(timeout=self.timeout)
|
|
186
|
+
results_map[key] = result
|
|
187
|
+
if key == graph.root_id:
|
|
188
|
+
root_result = result
|
|
189
|
+
ts.done(key)
|
|
190
|
+
else:
|
|
191
|
+
# Multiple tasks — use Celery group for parallelism
|
|
192
|
+
keys = [k for k, _ in tasks]
|
|
193
|
+
sigs = [s for _, s in tasks]
|
|
194
|
+
group_result = group(sigs).apply_async()
|
|
195
|
+
group_results = group_result.get(timeout=self.timeout)
|
|
196
|
+
|
|
197
|
+
for key, result in zip(keys, group_results):
|
|
198
|
+
results_map[key] = result
|
|
199
|
+
if key == graph.root_id:
|
|
200
|
+
root_result = result
|
|
201
|
+
ts.done(key)
|
|
202
|
+
|
|
203
|
+
finally:
|
|
204
|
+
self._is_evaluating = False
|
|
205
|
+
|
|
206
|
+
# Reconstruct result for the root node
|
|
207
|
+
if root_result is not None and hasattr(context, "model"):
|
|
208
|
+
model = context.model
|
|
209
|
+
if hasattr(model, "result_type"):
|
|
210
|
+
result_cls = model.result_type
|
|
211
|
+
try:
|
|
212
|
+
return result_cls(**root_result)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return GenericResult(value=root_result)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Fibonacci example using ccflow-celery with local (eager) execution.
|
|
2
|
+
|
|
3
|
+
Demonstrates:
|
|
4
|
+
- Defining a CallableModel with __deps__ for dependency-graph discovery
|
|
5
|
+
- Using CeleryGraphEvaluator in eager mode (no message broker required)
|
|
6
|
+
- Graph-parallel dispatch of Fibonacci sub-problems
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python -m ccflow_celery.examples.fibonacci
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ccflow import CallableModel, Flow, FlowOptionsOverride, GenericContext, GenericResult
|
|
13
|
+
from ccflow.evaluators import MemoryCacheEvaluator, MultiEvaluator
|
|
14
|
+
|
|
15
|
+
from ccflow_celery import CeleryApp, CeleryConfig, CeleryGraphEvaluator
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"FibonacciModel",
|
|
19
|
+
"main",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FibonacciModel(CallableModel):
|
|
24
|
+
"""Compute Fibonacci numbers via ccflow dependency graphs.
|
|
25
|
+
|
|
26
|
+
The graph evaluator calls __deps__ to discover the full dependency tree,
|
|
27
|
+
then dispatches each node as a Celery task in topological order.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@Flow.call
|
|
31
|
+
def __call__(self, context: GenericContext[int]) -> GenericResult[int]:
|
|
32
|
+
n = context.value
|
|
33
|
+
if n <= 1:
|
|
34
|
+
return GenericResult[int](value=n)
|
|
35
|
+
a = self(GenericContext[int](value=n - 1)).value
|
|
36
|
+
b = self(GenericContext[int](value=n - 2)).value
|
|
37
|
+
return GenericResult[int](value=a + b)
|
|
38
|
+
|
|
39
|
+
@Flow.deps
|
|
40
|
+
def __deps__(self, context: GenericContext[int]):
|
|
41
|
+
if context.value <= 1:
|
|
42
|
+
return []
|
|
43
|
+
return [
|
|
44
|
+
(
|
|
45
|
+
self,
|
|
46
|
+
[
|
|
47
|
+
GenericContext[int](value=context.value - 2),
|
|
48
|
+
GenericContext[int](value=context.value - 1),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
# Eager mode: tasks run in-process, no Redis/RabbitMQ needed
|
|
56
|
+
config = CeleryConfig(
|
|
57
|
+
broker_url="memory://",
|
|
58
|
+
result_backend="cache+memory://",
|
|
59
|
+
task_always_eager=True,
|
|
60
|
+
)
|
|
61
|
+
app = CeleryApp(name="fibonacci", config=config)
|
|
62
|
+
|
|
63
|
+
evaluator = MultiEvaluator(
|
|
64
|
+
evaluators=[
|
|
65
|
+
CeleryGraphEvaluator(app=app),
|
|
66
|
+
MemoryCacheEvaluator(),
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
model = FibonacciModel()
|
|
71
|
+
|
|
72
|
+
with FlowOptionsOverride(options={"cacheable": True, "evaluator": evaluator}):
|
|
73
|
+
for n in range(11):
|
|
74
|
+
result = model(GenericContext[int](value=n))
|
|
75
|
+
print(f"fib({n}) = {result.value}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
ccflow_celery/tasks.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
|
|
4
|
+
_log = getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
__all__ = ("execute_model_task",)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def execute_model_task(model_class: str, model_config: dict, context_class: str, context_config: dict) -> dict:
|
|
10
|
+
"""Execute a ccflow CallableModel on a Celery worker.
|
|
11
|
+
|
|
12
|
+
This is the Celery task function that reconstructs a model and context
|
|
13
|
+
from their serialized configurations and executes the model.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
model_class: Fully qualified class name (e.g., "ccflow_s3.S3Model")
|
|
17
|
+
model_config: Dict of model configuration (pydantic model_dump)
|
|
18
|
+
context_class: Fully qualified class name for the context
|
|
19
|
+
context_config: Dict of context configuration (pydantic model_dump)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dict representation of the result (model_dump)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def _import_class(class_path: str):
|
|
26
|
+
module_path, class_name = class_path.rsplit(".", 1)
|
|
27
|
+
module = importlib.import_module(module_path)
|
|
28
|
+
return getattr(module, class_name)
|
|
29
|
+
|
|
30
|
+
model_cls = _import_class(model_class)
|
|
31
|
+
context_cls = _import_class(context_class)
|
|
32
|
+
|
|
33
|
+
model = model_cls(**model_config)
|
|
34
|
+
context = context_cls(**context_config)
|
|
35
|
+
|
|
36
|
+
_log.info("Executing %s with context %s", model_class, context_class)
|
|
37
|
+
result = model(context)
|
|
38
|
+
|
|
39
|
+
if hasattr(result, "model_dump"):
|
|
40
|
+
# Exclude type_ to avoid ccflow's polymorphic validator interfering on reconstruction
|
|
41
|
+
return result.model_dump(exclude={"type_"})
|
|
42
|
+
return {"value": result}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from ccflow import CallableModel, Flow, GenericResult, NullContext
|
|
5
|
+
|
|
6
|
+
from ccflow_celery import * # noqa: F403
|
|
7
|
+
from ccflow_celery.app import CeleryApp, CeleryConfig
|
|
8
|
+
from ccflow_celery.evaluators import CeleryEvaluator, CeleryGraphEvaluator, _context_fqn, _model_fqn
|
|
9
|
+
from ccflow_celery.tasks import execute_model_task
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCeleryConfig:
|
|
13
|
+
def test_defaults(self):
|
|
14
|
+
cfg = CeleryConfig()
|
|
15
|
+
assert cfg.broker_url == "redis://localhost:6379/0"
|
|
16
|
+
assert cfg.result_backend == "redis://localhost:6379/0"
|
|
17
|
+
assert cfg.task_serializer == "json"
|
|
18
|
+
assert cfg.result_serializer == "json"
|
|
19
|
+
assert cfg.accept_content == ["json"]
|
|
20
|
+
assert cfg.task_track_started is True
|
|
21
|
+
assert cfg.task_default_queue == "default"
|
|
22
|
+
assert cfg.task_routes is None
|
|
23
|
+
assert cfg.worker_concurrency is None
|
|
24
|
+
|
|
25
|
+
def test_custom(self):
|
|
26
|
+
cfg = CeleryConfig(broker_url="amqp://broker", task_routes={"t": "q"}, worker_concurrency=4)
|
|
27
|
+
assert cfg.broker_url == "amqp://broker"
|
|
28
|
+
assert cfg.task_routes == {"t": "q"}
|
|
29
|
+
assert cfg.worker_concurrency == 4
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestCeleryApp:
|
|
33
|
+
def test_defaults(self):
|
|
34
|
+
app = CeleryApp()
|
|
35
|
+
assert app.name == "ccflow"
|
|
36
|
+
assert isinstance(app.config, CeleryConfig)
|
|
37
|
+
|
|
38
|
+
@patch("ccflow_celery.app.Celery")
|
|
39
|
+
def test_get_app_creates_celery(self, MockCelery):
|
|
40
|
+
mock_instance = MagicMock()
|
|
41
|
+
MockCelery.return_value = mock_instance
|
|
42
|
+
app = CeleryApp(name="myapp")
|
|
43
|
+
result = app.get_app()
|
|
44
|
+
MockCelery.assert_called_once_with("myapp")
|
|
45
|
+
mock_instance.conf.update.assert_called_once()
|
|
46
|
+
assert result is mock_instance
|
|
47
|
+
|
|
48
|
+
@patch("ccflow_celery.app.Celery")
|
|
49
|
+
def test_get_app_caches(self, MockCelery):
|
|
50
|
+
mock_instance = MagicMock()
|
|
51
|
+
MockCelery.return_value = mock_instance
|
|
52
|
+
app = CeleryApp()
|
|
53
|
+
first = app.get_app()
|
|
54
|
+
second = app.get_app()
|
|
55
|
+
assert first is second
|
|
56
|
+
MockCelery.assert_called_once()
|
|
57
|
+
|
|
58
|
+
@patch("ccflow_celery.app.Celery")
|
|
59
|
+
def test_get_app_applies_task_routes(self, MockCelery):
|
|
60
|
+
mock_instance = MagicMock()
|
|
61
|
+
MockCelery.return_value = mock_instance
|
|
62
|
+
cfg = CeleryConfig(task_routes={"task.name": "queue"})
|
|
63
|
+
app = CeleryApp(config=cfg)
|
|
64
|
+
app.get_app()
|
|
65
|
+
assert mock_instance.conf.task_routes == {"task.name": "queue"}
|
|
66
|
+
|
|
67
|
+
@patch("ccflow_celery.app.Celery")
|
|
68
|
+
def test_get_app_applies_worker_concurrency(self, MockCelery):
|
|
69
|
+
mock_instance = MagicMock()
|
|
70
|
+
MockCelery.return_value = mock_instance
|
|
71
|
+
cfg = CeleryConfig(worker_concurrency=8)
|
|
72
|
+
app = CeleryApp(config=cfg)
|
|
73
|
+
app.get_app()
|
|
74
|
+
assert mock_instance.conf.worker_concurrency == 8
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _DummyContext(NullContext):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _DummyModel(CallableModel):
|
|
82
|
+
@Flow.call
|
|
83
|
+
def __call__(self, context: _DummyContext) -> GenericResult:
|
|
84
|
+
return GenericResult(value=None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestFQNHelpers:
|
|
88
|
+
def test_model_fqn(self):
|
|
89
|
+
fqn = _model_fqn(_DummyModel())
|
|
90
|
+
assert fqn.endswith("_DummyModel")
|
|
91
|
+
assert __name__ in fqn or "test_all" in fqn
|
|
92
|
+
|
|
93
|
+
def test_context_fqn(self):
|
|
94
|
+
fqn = _context_fqn(_DummyContext())
|
|
95
|
+
assert fqn.endswith("_DummyContext")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestExecuteModelTask:
|
|
99
|
+
def test_basic_execution(self):
|
|
100
|
+
"""Round-trip: serialise a simple model+context, execute in the task."""
|
|
101
|
+
model = _DummyModel()
|
|
102
|
+
ctx = _DummyContext()
|
|
103
|
+
model_fqn = _model_fqn(model)
|
|
104
|
+
ctx_fqn = _context_fqn(ctx)
|
|
105
|
+
|
|
106
|
+
result = execute_model_task(model_fqn, model.model_dump(), ctx_fqn, ctx.model_dump())
|
|
107
|
+
assert isinstance(result, dict)
|
|
108
|
+
|
|
109
|
+
def test_result_has_model_dump(self):
|
|
110
|
+
"""If the model returns a ResultBase, execute_model_task returns model_dump()."""
|
|
111
|
+
model = _DummyModel()
|
|
112
|
+
ctx = _DummyContext()
|
|
113
|
+
result = execute_model_task(
|
|
114
|
+
_model_fqn(model),
|
|
115
|
+
model.model_dump(),
|
|
116
|
+
_context_fqn(ctx),
|
|
117
|
+
ctx.model_dump(),
|
|
118
|
+
)
|
|
119
|
+
assert isinstance(result, dict)
|
|
120
|
+
|
|
121
|
+
def test_bad_class_path_raises(self):
|
|
122
|
+
with pytest.raises((ModuleNotFoundError, AttributeError)):
|
|
123
|
+
execute_model_task("no.such.Module", {}, "no.such.Context", {})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _FakeEvalContext:
|
|
127
|
+
"""Mimics ModelEvaluationContext for unit testing."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, model, context, fn="__call__"):
|
|
130
|
+
self.model = model
|
|
131
|
+
self.context = context
|
|
132
|
+
self.fn = fn
|
|
133
|
+
self.options = {}
|
|
134
|
+
|
|
135
|
+
def __call__(self):
|
|
136
|
+
return getattr(self.model, self.fn)(self.context) if self.fn == "__call__" else getattr(self.model, self.fn)()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestCeleryEvaluator:
|
|
140
|
+
def _make_evaluator(self):
|
|
141
|
+
return CeleryEvaluator(app=CeleryApp())
|
|
142
|
+
|
|
143
|
+
@patch("ccflow_celery.app.Celery")
|
|
144
|
+
def test_remote_dispatch(self, MockCelery):
|
|
145
|
+
mock_app = MagicMock()
|
|
146
|
+
MockCelery.return_value = mock_app
|
|
147
|
+
mock_task = MagicMock()
|
|
148
|
+
mock_task.get.return_value = {"value": 42}
|
|
149
|
+
mock_app.send_task.return_value = mock_task
|
|
150
|
+
|
|
151
|
+
ev = self._make_evaluator()
|
|
152
|
+
model = _DummyModel()
|
|
153
|
+
ctx = _DummyContext()
|
|
154
|
+
eval_ctx = _FakeEvalContext(model, ctx)
|
|
155
|
+
|
|
156
|
+
result = ev(eval_ctx)
|
|
157
|
+
|
|
158
|
+
mock_app.send_task.assert_called_once()
|
|
159
|
+
call_args = mock_app.send_task.call_args
|
|
160
|
+
assert call_args[0][0] == "ccflow_celery.tasks.execute_model_task"
|
|
161
|
+
assert isinstance(result, GenericResult)
|
|
162
|
+
|
|
163
|
+
def test_non_call_fn_runs_locally(self):
|
|
164
|
+
"""When fn != '__call__', CeleryEvaluator executes locally."""
|
|
165
|
+
ev = self._make_evaluator()
|
|
166
|
+
model = _DummyModel()
|
|
167
|
+
ctx = _DummyContext()
|
|
168
|
+
eval_ctx = _FakeEvalContext(model, ctx, fn="__call__")
|
|
169
|
+
# Override fn to a non-__call__ to trigger local execution
|
|
170
|
+
eval_ctx.fn = "__deps__"
|
|
171
|
+
# __deps__ doesn't exist on the model, so let's mock it
|
|
172
|
+
model.__deps__ = MagicMock(return_value=[])
|
|
173
|
+
eval_ctx.model = model
|
|
174
|
+
eval_ctx.__call__ = lambda: model.__deps__()
|
|
175
|
+
|
|
176
|
+
_ = ev(eval_ctx)
|
|
177
|
+
model.__deps__.assert_called_once()
|
|
178
|
+
|
|
179
|
+
@patch("ccflow_celery.app.Celery")
|
|
180
|
+
def test_result_reconstruction_with_result_type(self, MockCelery):
|
|
181
|
+
"""When model has result_type, evaluator reconstructs from it."""
|
|
182
|
+
mock_app = MagicMock()
|
|
183
|
+
MockCelery.return_value = mock_app
|
|
184
|
+
mock_task = MagicMock()
|
|
185
|
+
mock_task.get.return_value = {"value": "reconstructed"}
|
|
186
|
+
mock_app.send_task.return_value = mock_task
|
|
187
|
+
|
|
188
|
+
ev = self._make_evaluator()
|
|
189
|
+
model = _DummyModel()
|
|
190
|
+
ctx = _DummyContext()
|
|
191
|
+
eval_ctx = _FakeEvalContext(model, ctx)
|
|
192
|
+
|
|
193
|
+
result = ev(eval_ctx)
|
|
194
|
+
# DummyModel inherits result_type from CallableModel → GenericResult
|
|
195
|
+
assert isinstance(result, GenericResult)
|
|
196
|
+
assert result.value == "reconstructed"
|
|
197
|
+
|
|
198
|
+
@patch("ccflow_celery.app.Celery")
|
|
199
|
+
def test_celery_app_caching(self, MockCelery):
|
|
200
|
+
mock_app = MagicMock()
|
|
201
|
+
MockCelery.return_value = mock_app
|
|
202
|
+
mock_task = MagicMock()
|
|
203
|
+
mock_task.get.return_value = {"value": None}
|
|
204
|
+
mock_app.send_task.return_value = mock_task
|
|
205
|
+
|
|
206
|
+
ev = self._make_evaluator()
|
|
207
|
+
model = _DummyModel()
|
|
208
|
+
ctx = _DummyContext()
|
|
209
|
+
|
|
210
|
+
ev(_FakeEvalContext(model, ctx))
|
|
211
|
+
ev(_FakeEvalContext(model, ctx))
|
|
212
|
+
|
|
213
|
+
# Celery instance created only once via CeleryApp.get_app()
|
|
214
|
+
MockCelery.assert_called_once()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TestCeleryGraphEvaluator:
|
|
218
|
+
def _make_evaluator(self):
|
|
219
|
+
return CeleryGraphEvaluator(app=CeleryApp())
|
|
220
|
+
|
|
221
|
+
def test_reentrant_calls_locally(self):
|
|
222
|
+
"""When _is_evaluating is True, graph evaluator executes context() directly."""
|
|
223
|
+
ev = self._make_evaluator()
|
|
224
|
+
ev._is_evaluating = True
|
|
225
|
+
|
|
226
|
+
sentinel = object()
|
|
227
|
+
eval_ctx = MagicMock()
|
|
228
|
+
eval_ctx.return_value = sentinel
|
|
229
|
+
|
|
230
|
+
result = ev(eval_ctx)
|
|
231
|
+
eval_ctx.assert_called_once()
|
|
232
|
+
assert result is sentinel
|
|
233
|
+
|
|
234
|
+
@patch("ccflow_celery.evaluators.get_dependency_graph")
|
|
235
|
+
@patch("ccflow_celery.app.Celery")
|
|
236
|
+
def test_single_node_graph(self, MockCelery, mock_get_dep):
|
|
237
|
+
"""Graph with one node dispatches via apply_async directly (no group)."""
|
|
238
|
+
mock_app = MagicMock()
|
|
239
|
+
MockCelery.return_value = mock_app
|
|
240
|
+
|
|
241
|
+
model = _DummyModel()
|
|
242
|
+
ctx = _DummyContext()
|
|
243
|
+
eval_ctx_inner = _FakeEvalContext(model, ctx)
|
|
244
|
+
|
|
245
|
+
# Build a mock dependency graph with one node
|
|
246
|
+
mock_graph = MagicMock()
|
|
247
|
+
root_key = "root_id"
|
|
248
|
+
mock_graph.graph = {root_key: set()} # one node, no deps
|
|
249
|
+
mock_graph.ids = {root_key: eval_ctx_inner}
|
|
250
|
+
mock_graph.root_id = root_key
|
|
251
|
+
mock_get_dep.return_value = mock_graph
|
|
252
|
+
|
|
253
|
+
# Mock signature and async result
|
|
254
|
+
mock_sig = MagicMock()
|
|
255
|
+
mock_async = MagicMock()
|
|
256
|
+
mock_async.get.return_value = {"value": "root_val"}
|
|
257
|
+
mock_sig.apply_async.return_value = mock_async
|
|
258
|
+
mock_app.signature.return_value = mock_sig
|
|
259
|
+
|
|
260
|
+
ev = self._make_evaluator()
|
|
261
|
+
outer_ctx = _FakeEvalContext(model, ctx)
|
|
262
|
+
|
|
263
|
+
result = ev(outer_ctx)
|
|
264
|
+
|
|
265
|
+
mock_app.signature.assert_called_once()
|
|
266
|
+
mock_sig.apply_async.assert_called_once()
|
|
267
|
+
assert isinstance(result, GenericResult)
|
|
268
|
+
|
|
269
|
+
@patch("ccflow_celery.evaluators.get_dependency_graph")
|
|
270
|
+
@patch("ccflow_celery.evaluators.group")
|
|
271
|
+
@patch("ccflow_celery.app.Celery")
|
|
272
|
+
def test_parallel_nodes_use_group(self, MockCelery, mock_group, mock_get_dep):
|
|
273
|
+
"""Multiple independent nodes at the same level use Celery group."""
|
|
274
|
+
mock_app = MagicMock()
|
|
275
|
+
MockCelery.return_value = mock_app
|
|
276
|
+
|
|
277
|
+
model = _DummyModel()
|
|
278
|
+
ctx = _DummyContext()
|
|
279
|
+
|
|
280
|
+
# Two independent leaf nodes, then a root depending on both
|
|
281
|
+
leaf1_ctx = _FakeEvalContext(model, ctx)
|
|
282
|
+
leaf2_ctx = _FakeEvalContext(model, ctx)
|
|
283
|
+
root_ctx = _FakeEvalContext(model, ctx)
|
|
284
|
+
|
|
285
|
+
mock_graph = MagicMock()
|
|
286
|
+
mock_graph.graph = {
|
|
287
|
+
"root": {"leaf1", "leaf2"},
|
|
288
|
+
"leaf1": set(),
|
|
289
|
+
"leaf2": set(),
|
|
290
|
+
}
|
|
291
|
+
mock_graph.ids = {
|
|
292
|
+
"leaf1": leaf1_ctx,
|
|
293
|
+
"leaf2": leaf2_ctx,
|
|
294
|
+
"root": root_ctx,
|
|
295
|
+
}
|
|
296
|
+
mock_graph.root_id = "root"
|
|
297
|
+
mock_get_dep.return_value = mock_graph
|
|
298
|
+
|
|
299
|
+
# Mock signature
|
|
300
|
+
mock_sig = MagicMock()
|
|
301
|
+
mock_app.signature.return_value = mock_sig
|
|
302
|
+
|
|
303
|
+
# Mock group result for the two leaves
|
|
304
|
+
mock_group_result = MagicMock()
|
|
305
|
+
mock_group_result.get.return_value = [{"value": "l1"}, {"value": "l2"}]
|
|
306
|
+
mock_group_instance = MagicMock()
|
|
307
|
+
mock_group_instance.apply_async.return_value = mock_group_result
|
|
308
|
+
mock_group.return_value = mock_group_instance
|
|
309
|
+
|
|
310
|
+
# Root will be a single task
|
|
311
|
+
mock_root_async = MagicMock()
|
|
312
|
+
mock_root_async.get.return_value = {"value": "root_val"}
|
|
313
|
+
mock_sig.apply_async.return_value = mock_root_async
|
|
314
|
+
|
|
315
|
+
ev = self._make_evaluator()
|
|
316
|
+
outer_ctx = _FakeEvalContext(model, ctx)
|
|
317
|
+
|
|
318
|
+
result = ev(outer_ctx)
|
|
319
|
+
|
|
320
|
+
# group() should have been called for the parallel leaf nodes
|
|
321
|
+
mock_group.assert_called_once()
|
|
322
|
+
assert isinstance(result, GenericResult)
|
|
323
|
+
|
|
324
|
+
@patch("ccflow_celery.evaluators.get_dependency_graph")
|
|
325
|
+
@patch("ccflow_celery.app.Celery")
|
|
326
|
+
def test_is_evaluating_reset_on_exception(self, MockCelery, mock_get_dep):
|
|
327
|
+
"""_is_evaluating is reset even when graph processing raises."""
|
|
328
|
+
mock_app = MagicMock()
|
|
329
|
+
MockCelery.return_value = mock_app
|
|
330
|
+
mock_get_dep.side_effect = RuntimeError("boom")
|
|
331
|
+
|
|
332
|
+
ev = self._make_evaluator()
|
|
333
|
+
model = _DummyModel()
|
|
334
|
+
ctx = _DummyContext()
|
|
335
|
+
outer_ctx = _FakeEvalContext(model, ctx)
|
|
336
|
+
|
|
337
|
+
with pytest.raises(RuntimeError, match="boom"):
|
|
338
|
+
ev(outer_ctx)
|
|
339
|
+
|
|
340
|
+
assert ev._is_evaluating is False
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ccflow-celery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Celery evaluator for ccflow
|
|
5
|
+
Project-URL: Repository, https://github.com/1kbgz/ccflow-celery
|
|
6
|
+
Project-URL: Homepage, https://github.com/1kbgz/ccflow-celery
|
|
7
|
+
Author-email: 1kbgz <dev@1kbgz.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: ccflow>=0.6.10
|
|
22
|
+
Requires-Dist: celery
|
|
23
|
+
Provides-Extra: develop
|
|
24
|
+
Requires-Dist: build; extra == 'develop'
|
|
25
|
+
Requires-Dist: bump-my-version; extra == 'develop'
|
|
26
|
+
Requires-Dist: check-dist; extra == 'develop'
|
|
27
|
+
Requires-Dist: codespell; extra == 'develop'
|
|
28
|
+
Requires-Dist: hatchling; extra == 'develop'
|
|
29
|
+
Requires-Dist: mdformat; extra == 'develop'
|
|
30
|
+
Requires-Dist: mdformat-tables>=1; extra == 'develop'
|
|
31
|
+
Requires-Dist: pytest; extra == 'develop'
|
|
32
|
+
Requires-Dist: pytest-cov; extra == 'develop'
|
|
33
|
+
Requires-Dist: ruff; extra == 'develop'
|
|
34
|
+
Requires-Dist: twine; extra == 'develop'
|
|
35
|
+
Requires-Dist: ty; extra == 'develop'
|
|
36
|
+
Requires-Dist: uv; extra == 'develop'
|
|
37
|
+
Requires-Dist: wheel; extra == 'develop'
|
|
38
|
+
Requires-Dist: yardang; extra == 'develop'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# ccflow-celery
|
|
42
|
+
|
|
43
|
+
Celery evaluator for ccflow
|
|
44
|
+
|
|
45
|
+
[](https://github.com/1kbgz/ccflow-celery/actions/workflows/build.yaml)
|
|
46
|
+
[](https://codecov.io/gh/1kbgz/ccflow-celery)
|
|
47
|
+
[](https://github.com/1kbgz/ccflow-celery)
|
|
48
|
+
[](https://pypi.python.org/pypi/ccflow-celery)
|
|
49
|
+
|
|
50
|
+
## Overview
|
|
51
|
+
|
|
52
|
+
> [!NOTE]
|
|
53
|
+
> This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ccflow_celery/__init__.py,sha256=VQmaEAeel5PlKx76UEpfi2VuU2EFD0xB455hrKmKzE4,68
|
|
2
|
+
ccflow_celery/app.py,sha256=Ci2ntwDNc2n7YHYrWwpzCjqbiUNyT5mafKtFPkrDuc4,2557
|
|
3
|
+
ccflow_celery/evaluators.py,sha256=Iy2luGeaJs2Oq0JtK-Zs9VdA4D7jYLMrxuxZD17LFzI,7550
|
|
4
|
+
ccflow_celery/tasks.py,sha256=WiOXkEld_8GhLcOQ7-oE7vcPX9a8dtGQepReyaPslpM,1490
|
|
5
|
+
ccflow_celery/config/__init__.py,sha256=crVs8X8MDPQGZ0Z6opSstXercD7bLHxQ5pU4eBnwjA8,905
|
|
6
|
+
ccflow_celery/config/base.yaml,sha256=qb0bPXzDk0pb-lyec9eYjipcRbbXzd9g0pW9285QmaQ,21
|
|
7
|
+
ccflow_celery/config/evaluator/ccflow_celery.yaml,sha256=_2cz2EDrdb0SDL-8BiEhirGBJGg1pOQNe3Jpau37XA4,770
|
|
8
|
+
ccflow_celery/config/evaluator/ccflow_celery_eager.yaml,sha256=wn3bdXA9p7UcI2ylu1Fp-jcvdm8eT3Fv3QQEprZPFI4,580
|
|
9
|
+
ccflow_celery/examples/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
|
+
ccflow_celery/examples/fibonacci.py,sha256=_sn0Sdze4BfQWvsCfSZzxL9AepDOS62T7zDPeJjdOIA,2300
|
|
11
|
+
ccflow_celery/tests/test_all.py,sha256=g9rjGrecEEX_9CnyIJX-xuc9jWOSr2E0Whya6Xd5xbs,11930
|
|
12
|
+
ccflow_celery-0.1.0.dist-info/METADATA,sha256=Za2gPukIR08UKhL8WEshu-OJAM6IkEWXAROPr0cUPzY,2414
|
|
13
|
+
ccflow_celery-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
14
|
+
ccflow_celery-0.1.0.dist-info/licenses/LICENSE,sha256=RcPqVYf2JyhlV4FlBQud0104R3fT1_-7XbT-vp4eLyE,11335
|
|
15
|
+
ccflow_celery-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2025 1kbgz
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|