fastprocesses 0.7.4__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.
@@ -0,0 +1,8 @@
1
+ """fastProcesses package.
2
+
3
+ A library to create a FastAPI-based OGC API Processes wrapper around existing projects.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ __all__: list[str] = []
File without changes
@@ -0,0 +1,377 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime
3
+ import json
4
+ from typing import Any, Dict, List, Tuple
5
+
6
+ from celery.result import AsyncResult
7
+
8
+ from fastprocesses.common import redis_cache
9
+ from fastprocesses.core.logging import logger
10
+ from fastprocesses.core.models import (
11
+ CalculationTask,
12
+ ExecutionMode,
13
+ ProcessDescription,
14
+ ProcessExecRequestBody,
15
+ ProcessExecResponse,
16
+ )
17
+ from fastprocesses.processes.process_registry import get_process_registry
18
+ from fastprocesses.common import celery_app
19
+
20
+
21
+ class ExecutionStrategy(ABC):
22
+ """
23
+ Abstract base class implementing the Strategy pattern for process execution.
24
+ Different execution modes (sync/async) implement this interface.
25
+ """
26
+
27
+ def __init__(self, process_manager):
28
+ self.process_manager: ProcessManager = process_manager
29
+
30
+ @abstractmethod
31
+ def execute(self, process_id: str, calculation_task: CalculationTask) -> ProcessExecResponse:
32
+ pass
33
+
34
+ class AsyncExecutionStrategy(ExecutionStrategy):
35
+ """
36
+ Handles asynchronous process execution by:
37
+ 1. Submitting task to Celery queue
38
+ 2. Creating initial job status in cache
39
+ 3. Returning immediately with job ID
40
+ """
41
+
42
+ def execute(self, process_id: str, calculation_task: CalculationTask) -> ProcessExecResponse:
43
+
44
+ # dump data to json
45
+ serialized_data = json.dumps(calculation_task.model_dump(
46
+ include={"inputs", "outputs", "response"}
47
+ ))
48
+
49
+ # Submit task to Celery worker queue for background processing
50
+ task = self.process_manager.celery_app.send_task(
51
+ 'execute_process',
52
+ args=[process_id, serialized_data]
53
+ )
54
+
55
+ # Initialize job metadata in cache with status 'accepted'
56
+ job_info = {
57
+ "status": "accepted",
58
+ "type": "process",
59
+ "process_id": process_id,
60
+ "created": datetime.utcnow().isoformat(),
61
+ "updated": datetime.utcnow().isoformat(),
62
+ "progress": 0
63
+ }
64
+ self.process_manager.cache.put(f"job:{task.id}", job_info)
65
+
66
+ return ProcessExecResponse(status="accepted", jobID=task.id, type="process")
67
+
68
+ class SyncExecutionStrategy(ExecutionStrategy):
69
+ """Strategy for synchronous execution."""
70
+
71
+ def execute(self, process_id: str, calculation_task: CalculationTask) -> ProcessExecResponse:
72
+ service = self.process_manager.service_registry.get_service(process_id)
73
+ # TODO: response type and outputs must be passed, too
74
+ result = service.execute(calculation_task.inputs)
75
+
76
+ task = self.process_manager.celery_app.send_task(
77
+ 'store_result',
78
+ args=[process_id, calculation_task.celery_key, result]
79
+ )
80
+
81
+ job_info = {
82
+ "status": "successful",
83
+ "type": "process",
84
+ "process_id": process_id,
85
+ "created": datetime.utcnow().isoformat(),
86
+ "started": datetime.utcnow().isoformat(),
87
+ "finished": datetime.utcnow().isoformat(),
88
+ "updated": datetime.utcnow().isoformat(),
89
+ "progress": 100,
90
+ "result": result
91
+ }
92
+ self.process_manager.cache.put(f"job:{task.id}", job_info)
93
+
94
+ return ProcessExecResponse(status="successful", jobID=task.id, type="process", value=result)
95
+
96
+ class ProcessManager:
97
+ """Manages processes, including execution, status checking, and job management."""
98
+
99
+ def __init__(self):
100
+ """Initializes the ProcessManager with Celery app and service registry."""
101
+ self.celery_app = celery_app
102
+ self.service_registry = get_process_registry()
103
+ self.cache = redis_cache
104
+
105
+ def get_available_processes(
106
+ self, limit: int, offset: int
107
+ ) -> Tuple[List[ProcessDescription], str]:
108
+ logger.info("Retrieving available processes")
109
+ """
110
+ Retrieves a list of available processes.
111
+
112
+ Returns:
113
+ List[ProcessDescription]: A list of process descriptions.
114
+ """
115
+ process_ids = self.service_registry.get_service_ids()
116
+ processes = [
117
+ self.get_process_description(process_id)
118
+ for process_id
119
+ in process_ids[offset:offset+limit]
120
+ ]
121
+ next_link = None
122
+ if offset + limit < len(process_ids):
123
+ next_link = f"/processes?limit={limit}&offset={offset+limit}"
124
+ return processes, next_link
125
+
126
+
127
+ def get_process_description(self, process_id: str) -> ProcessDescription:
128
+ logger.info(f"Retrieving description for process ID: {process_id}")
129
+ """
130
+ Retrieves the description of a specific process.
131
+
132
+ Args:
133
+ process_id (str): The ID of the process.
134
+
135
+ Returns:
136
+ ProcessDescription: The description of the process.
137
+
138
+ Raises:
139
+ ValueError: If the process is not found.
140
+ """
141
+ if not self.service_registry.has_service(process_id):
142
+ logger.error(f"Process {process_id} not found!")
143
+ raise ValueError(f"Process {process_id} not found!")
144
+
145
+ service = self.service_registry.get_service(process_id)
146
+ return service.get_description()
147
+
148
+ def execute_process(self, process_id: str, data: ProcessExecRequestBody) -> ProcessExecResponse:
149
+ """
150
+ Main process execution orchestration:
151
+ 1. Validates process existence and input data
152
+ 2. Checks result cache to avoid recomputation
153
+ 3. Selects execution strategy (sync/async)
154
+ 4. Delegates execution to appropriate strategy
155
+
156
+ Args:
157
+ process_id: Identifier for the process to execute
158
+ data: Contains input parameters and execution mode
159
+
160
+ Returns:
161
+ ProcessExecResponse with job status and ID
162
+
163
+ Raises:
164
+ ValueError: If process not found or input validation fails
165
+ """
166
+ logger.info(f"Executing process ID: {process_id}")
167
+
168
+ # Validate process exists
169
+ if not self.service_registry.has_service(process_id):
170
+ logger.error(f"Process {process_id} not found!")
171
+ raise ValueError(f"Process {process_id} not found!")
172
+
173
+ # Get service and validate inputs
174
+ service = self.service_registry.get_service(process_id)
175
+ try:
176
+ service.validate_inputs(data.inputs)
177
+ except ValueError as e:
178
+ logger.error(f"Input validation failed for process {process_id}: {str(e)}")
179
+ raise ValueError(f"Input validation failed: {str(e)}")
180
+
181
+ try:
182
+ service.validate_outputs(data.outputs)
183
+ except ValueError as e:
184
+ logger.error(f"Output validation failed for process {process_id}: {str(e)}")
185
+ raise ValueError(f"Output validation failed: {str(e)}")
186
+
187
+ # Create calculation task
188
+ calculation_task = CalculationTask(
189
+ inputs=data.inputs,
190
+ outputs=data.outputs,
191
+ response=data.response
192
+ )
193
+
194
+ # Check cache first
195
+ cached_result = self._check_cache(calculation_task)
196
+ if cached_result:
197
+ return cached_result
198
+
199
+ # Select execution strategy based on mode
200
+ execution_strategies = {
201
+ ExecutionMode.SYNC: SyncExecutionStrategy(self),
202
+ ExecutionMode.ASYNC: AsyncExecutionStrategy(self)
203
+ }
204
+
205
+ strategy: SyncExecutionStrategy | AsyncExecutionStrategy = execution_strategies[data.mode]
206
+ return strategy.execute(process_id, calculation_task)
207
+
208
+ def get_job_status(self, job_id: str) -> Dict[str, Any]:
209
+ """
210
+ Retrieves the status of a specific job.
211
+
212
+ Args:
213
+ job_id (str): The ID of the job.
214
+
215
+ Returns:
216
+ Dict[str, Any]: The status of the job.
217
+
218
+ Raises:
219
+ ValueError: If the job is not found.
220
+ """
221
+ # Check if job exists in Redis first
222
+ job_info = self.cache.get(f"job:{job_id}")
223
+ if not job_info:
224
+ logger.error(f"Job {job_id} not found in cache")
225
+ raise ValueError(f"Job {job_id} not found")
226
+
227
+ # Now check Celery status
228
+ result = AsyncResult(job_id)
229
+ if result.ready():
230
+ if result.successful():
231
+ return {"status": "successful", "type": "process"}
232
+ return {"status": "failed", "type": "process", "message": str(result.result)}
233
+ return {"status": "running", "type": "process"}
234
+
235
+ def get_job_result(self, job_id: str) -> Dict[str, Any]:
236
+ """
237
+ Retrieves the result of a specific job.
238
+
239
+ Args:
240
+ job_id (str): The ID of the job.
241
+
242
+ Returns:
243
+ Dict[str, Any]: The result of the job.
244
+
245
+ Raises:
246
+ ValueError: If the job is not found.
247
+ """
248
+ # Check if job exists in Redis first
249
+ job_info = self.cache.get(f"job:{job_id}")
250
+ if not job_info:
251
+ logger.error(f"Job {job_id} not found in cache")
252
+ raise ValueError(f"Job {job_id} not found")
253
+
254
+ result = AsyncResult(job_id)
255
+ if result.state == 'PENDING':
256
+ logger.error(f"Result for job ID {job_id} is not ready")
257
+ raise ValueError("Result not ready")
258
+ elif result.state == 'FAILURE':
259
+ logger.error(f"Job ID {job_id} failed with error: {result.result}")
260
+ raise ValueError(f"Job failed: {result.result}")
261
+ elif result.state == 'SUCCESS':
262
+ logger.info(f"Job ID {job_id} completed successfully")
263
+ return result.result
264
+ else:
265
+ return {"status": "running", "type": "process"}
266
+
267
+ def delete_job(self, job_id: str) -> Dict[str, Any]:
268
+ logger.info(f"Deleting job ID: {job_id}")
269
+ """
270
+ Deletes a specific job.
271
+
272
+ Args:
273
+ job_id (str): The ID of the job.
274
+
275
+ Returns:
276
+ Dict[str, Any]: The status of the deletion.
277
+
278
+ Raises:
279
+ ValueError: If the job is not found.
280
+ """
281
+ result = AsyncResult(job_id)
282
+ if not result:
283
+ logger.error("Job not found")
284
+ raise ValueError("Job not found")
285
+ result.forget()
286
+ return {"status": "dismissed", "message": "Job dismissed"}
287
+
288
+ def get_jobs(self, limit: int, offset: int) -> List[Dict[str, Any]]:
289
+ """
290
+ Retrieves a list of all jobs and their status.
291
+
292
+ Returns:
293
+ List[Dict[str, Any]]: List of job status information
294
+ """
295
+ # Get all job IDs from Redis
296
+ job_keys = self.cache.keys("job:*")
297
+ jobs = []
298
+
299
+ for job_key in job_keys[offset:offset+limit]:
300
+ try:
301
+ job_info = self.cache.get(job_key)
302
+ if job_info:
303
+ # Remove "job:" prefix for consistent job ID handling
304
+ job_id = job_key.replace("job:", "")
305
+ status_info = {
306
+ "jobID": job_id, # Clean job ID without prefix
307
+ "status": job_info.get("status", "unknown"),
308
+ "type": "process",
309
+ "processID": job_info.get("process_id"),
310
+ "created": job_info.get("created"),
311
+ "started": job_info.get("started"),
312
+ "finished": job_info.get("finished"),
313
+ "updated": job_info.get("updated"),
314
+ "progress": job_info.get("progress"),
315
+ "links": [
316
+ {
317
+ "href": f"/jobs/{job_id}", # Clean job ID in links
318
+ "rel": "self",
319
+ "type": "application/json"
320
+ },
321
+ {
322
+ "href": f"/jobs/{job_id}/results", # Clean job ID in links
323
+ "rel": "results",
324
+ "type": "application/json"
325
+ }
326
+ ]
327
+ }
328
+ if "message" in job_info:
329
+ status_info["message"] = job_info["message"]
330
+ jobs.append(status_info)
331
+ except Exception as e:
332
+ logger.error(f"Error retrieving job {job_key}: {e}")
333
+
334
+ next_link = None
335
+ if offset + limit < len(job_keys):
336
+ next_link = f"/jobs?limit={limit}&offset={offset+limit}"
337
+
338
+ return jobs, next_link
339
+
340
+ def _check_cache(self, calculation_task: CalculationTask) -> ProcessExecResponse | None:
341
+ """
342
+ Optimizes performance by checking if identical calculation exists in cache.
343
+ Uses task input hash as cache key.
344
+
345
+ Args:
346
+ calculation_task: Task containing input parameters
347
+
348
+ Returns:
349
+ Cached response if found, None otherwise
350
+ """
351
+ cache_check = self.celery_app.send_task(
352
+ 'check_cache',
353
+ args=[calculation_task.model_dump()]
354
+ )
355
+
356
+ cache_status = cache_check.get(timeout=10)
357
+ if cache_status["status"] != "HIT":
358
+ return None
359
+
360
+ task = self.celery_app.send_task(
361
+ 'find_result_in_cache',
362
+ args=[calculation_task.celery_key]
363
+ )
364
+
365
+ job_info = {
366
+ "status": "successful",
367
+ "type": "process",
368
+ "created": datetime.utcnow().isoformat(),
369
+ "started": datetime.utcnow().isoformat(),
370
+ "finished": datetime.utcnow().isoformat(),
371
+ "updated": datetime.utcnow().isoformat(),
372
+ "progress": 100,
373
+ "message": "Result retrieved from cache"
374
+ }
375
+ self.cache.put(f"job:{task.id}", job_info)
376
+
377
+ return ProcessExecResponse(status="successful", jobID=task.id, type="process")
@@ -0,0 +1,177 @@
1
+ from fastapi import APIRouter, HTTPException, Query, Response, status
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from fastprocesses.api.manager import ProcessManager
5
+ from fastprocesses.core.logging import logger
6
+ from fastprocesses.core.models import (
7
+ Conformance,
8
+ ExecutionMode,
9
+ JobList,
10
+ Landing,
11
+ Link,
12
+ ProcessDescription,
13
+ ProcessExecRequestBody,
14
+ ProcessExecResponse,
15
+ ProcessList
16
+ )
17
+
18
+
19
+ def get_router(
20
+ process_manager: ProcessManager,
21
+ title: str,
22
+ description: str
23
+ ) -> APIRouter:
24
+ router = APIRouter()
25
+
26
+ @router.get("/")
27
+ async def landing_page() -> Landing:
28
+ logger.debug("Landing page accessed")
29
+ return Landing(
30
+ title=title,
31
+ description=description,
32
+ links=[
33
+ Link(href="/", rel="self", type="application/json"),
34
+ Link(href="/conformance", rel="conformance", type="application/json"),
35
+ Link(href="/processes", rel="processes", type="application/json"),
36
+ Link(href="/jobs", rel="jobs", type="application/json"),
37
+ ]
38
+ )
39
+
40
+ @router.get("/conformance")
41
+ async def conformance() -> Conformance:
42
+ logger.debug("Conformance endpoint accessed")
43
+ return Conformance(
44
+ conformsTo=[
45
+ "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core",
46
+ "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/json"
47
+ "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/job-list"
48
+ ]
49
+ )
50
+
51
+ @router.get(
52
+ "/processes",
53
+ response_model_exclude_none=True,
54
+ response_model=ProcessList
55
+ )
56
+ async def list_processes(
57
+ limit: int = Query(10, ge=1, le=10000),
58
+ offset: int = Query(0, ge=0)
59
+ ):
60
+ logger.debug("List processes endpoint accessed")
61
+
62
+ processes, next_link = process_manager.get_available_processes(limit, offset)
63
+ links = [Link(href="/processes", rel="self", type="application/json")]
64
+ if next_link:
65
+ links.append(Link(href=next_link, rel="next", type="application/json"))
66
+
67
+ return ProcessList(
68
+ processes=processes,
69
+ links=links
70
+ )
71
+
72
+ @router.get(
73
+ "/processes/{process_id}",
74
+ response_model_exclude_none=True,
75
+ response_model=ProcessDescription
76
+ )
77
+ async def describe_process(process_id: str):
78
+ logger.debug(f"Describe process endpoint accessed for process ID: {process_id}")
79
+ try:
80
+ return process_manager.get_process_description(process_id)
81
+ except ValueError as e:
82
+ logger.error(f"Process {process_id} not found: {e}")
83
+ raise HTTPException(status_code=404, detail=str(e))
84
+
85
+ @router.post(
86
+ "/processes/{process_id}/execution",
87
+ response_model=ProcessExecResponse
88
+ )
89
+ async def execute_process(
90
+ process_id: str,
91
+ request: ProcessExecRequestBody,
92
+ response: Response
93
+ ) -> JSONResponse:
94
+ logger.debug(f"Execute process endpoint accessed for process ID: {process_id}")
95
+ try:
96
+ result = process_manager.execute_process(process_id, request)
97
+
98
+ # Set response status code based on execution mode
99
+ if request.mode == ExecutionMode.ASYNC:
100
+ response.status_code = status.HTTP_201_CREATED
101
+ # Add Location header for async execution
102
+ response.headers["Location"] = f"/jobs/{result.jobID}"
103
+ else:
104
+ # For sync execution with results
105
+ if result.value:
106
+ response.status_code = status.HTTP_200_OK
107
+ # For sync execution without results
108
+ else:
109
+ response.status_code = status.HTTP_204_NO_CONTENT
110
+
111
+ return result
112
+ except ValueError as e:
113
+ error_message = str(e)
114
+ if "Input validation failed" in error_message:
115
+ logger.error(f"Input validation error for process {process_id}: {error_message}")
116
+ raise HTTPException(
117
+ status_code=status.HTTP_400_BAD_REQUEST,
118
+ detail={
119
+ "type": "process",
120
+ "error": "InvalidParameterValue",
121
+ "message": error_message,
122
+ "process_id": process_id
123
+ }
124
+ )
125
+ logger.error(f"Process {process_id} not found: {error_message}")
126
+ raise HTTPException(
127
+ status_code=status.HTTP_404_NOT_FOUND,
128
+ detail={
129
+ "type": "process",
130
+ "error": "NotFound",
131
+ "message": error_message
132
+ }
133
+ )
134
+
135
+ @router.get(
136
+ "/jobs",
137
+ response_model_exclude_none=True,
138
+ response_model=JobList
139
+ )
140
+ async def list_jobs(
141
+ limit: int = Query(10, ge=1, le=1000),
142
+ offset: int = Query(0, ge=0)
143
+ ):
144
+ """
145
+ Lists all jobs.
146
+ """
147
+ logger.debug("List jobs endpoint accessed")
148
+ jobs, next_link = process_manager.get_jobs(limit, offset)
149
+ links = [Link(href="/jobs", rel="self", type="application/json")]
150
+ if next_link:
151
+ links.append(Link(href=next_link, rel="next", type="application/json"))
152
+
153
+ return JobList(
154
+ jobs=jobs,
155
+ links=links
156
+ )
157
+
158
+
159
+ @router.get("/jobs/{job_id}")
160
+ async def get_job_status(job_id: str):
161
+ logger.debug(f"Get job status endpoint accessed for job ID: {job_id}")
162
+ try:
163
+ return process_manager.get_job_status(job_id)
164
+ except ValueError as e:
165
+ logger.error(f"Job {job_id} not found: {e}")
166
+ raise HTTPException(status_code=404, detail=str(e))
167
+
168
+ @router.get("/jobs/{job_id}/results")
169
+ async def get_job_result(job_id: str):
170
+ logger.debug(f"Get job result endpoint accessed for job ID: {job_id}")
171
+ try:
172
+ return process_manager.get_job_result(job_id)
173
+ except ValueError as e:
174
+ logger.error(f"Job {job_id} not found: {e}")
175
+ raise HTTPException(status_code=404, detail=str(e))
176
+
177
+ return router
@@ -0,0 +1,24 @@
1
+ # src/fastprocesses/api/server.py
2
+ from fastapi import FastAPI
3
+
4
+ from fastprocesses.api.manager import ProcessManager
5
+ from fastprocesses.api.router import get_router
6
+
7
+
8
+ class OGCProcessesAPI:
9
+ def __init__(self, title: str, version: str, description: str):
10
+ self.process_manager = ProcessManager()
11
+ self.app = FastAPI(
12
+ title=title,
13
+ version=version,
14
+ description=description
15
+ )
16
+ self.app.include_router(
17
+ get_router(
18
+ self.process_manager,
19
+ self.app.title,
20
+ self.app.description)
21
+ )
22
+
23
+ def get_app(self) -> FastAPI:
24
+ return self.app
@@ -0,0 +1,45 @@
1
+ import signal
2
+ import subprocess
3
+ import sys
4
+
5
+ from fastprocesses.core.logging import logger
6
+
7
+
8
+ def main():
9
+ logger.info("Starting Celery worker")
10
+ # Construct the Celery command
11
+ celery_command = [
12
+ "celery",
13
+ "-A",
14
+ "src.fastprocesses.worker.celery_app",
15
+ "worker",
16
+ "--loglevel=info"
17
+ ]
18
+
19
+ # Append any additional arguments passed to the script
20
+ celery_command.extend(sys.argv[1:])
21
+
22
+ # Start the Celery process
23
+ process = subprocess.Popen(celery_command)
24
+ logger.info("Celery worker started")
25
+
26
+ def handle_signal(sig, frame):
27
+ logger.info(f"Received signal: {sig}")
28
+ # Forward the signal to the Celery process
29
+ process.send_signal(sig)
30
+
31
+ # Register signal handlers
32
+ signal.signal(signal.SIGINT, handle_signal)
33
+ signal.signal(signal.SIGTERM, handle_signal)
34
+
35
+ try:
36
+ # Wait for the Celery process to complete
37
+ process.wait()
38
+ except KeyboardInterrupt:
39
+ logger.info("KeyboardInterrupt received, shutting down Celery worker")
40
+ # Handle the KeyboardInterrupt to ensure a warm shutdown
41
+ process.send_signal(signal.SIGINT)
42
+ process.wait()
43
+
44
+ if __name__ == "__main__":
45
+ main()
@@ -0,0 +1,59 @@
1
+ import json
2
+
3
+ from celery import Celery
4
+ from fastapi.encoders import jsonable_encoder
5
+ from kombu.serialization import register
6
+
7
+ from fastprocesses.core.cache import Cache
8
+ from fastprocesses.core.config import settings
9
+
10
+
11
+ def custom_json_serializer(obj):
12
+ # Use jsonable_encoder to handle complex objects
13
+ return json.dumps(jsonable_encoder(obj))
14
+
15
+
16
+ def custom_json_deserializer(data):
17
+ # Deserialize JSON back into Python objects
18
+ return json.loads(data)
19
+
20
+ # Register the custom serializer
21
+ register(
22
+ "custom_json",
23
+ custom_json_serializer,
24
+ custom_json_deserializer,
25
+ content_type="application/x-custom-json",
26
+ content_encoding="utf-8",
27
+ )
28
+
29
+ celery_app = Celery(
30
+ "ogc_processes",
31
+ broker=str(settings.celery_broker_url),
32
+ backend=str(settings.celery_result_backend),
33
+ include=["fastprocesses.worker.celery_app"], # Ensure the module is included
34
+ )
35
+
36
+ celery_app.conf.update(
37
+ task_serializer="custom_json",
38
+ result_serializer="custom_json",
39
+ accept_content=["custom_json", "json"], # Accept only the custom serializer
40
+ timezone="UTC",
41
+ enable_utc=True,
42
+ task_routes={
43
+ "fastprocesses.worker.celery_app.execute_process": {
44
+ "queue": "process_tasks",
45
+ "routing_key": "process_tasks",
46
+ }
47
+ },
48
+ broker_connection_retry=True,
49
+ broker_connection_retry_on_startup=True,
50
+ # set limits for long-running tasks
51
+ task_time_limit=900, # Hard limit in seconds
52
+ task_soft_time_limit=600, # Soft limit in seconds
53
+ result_expires=3153600, # 1 year in seconds
54
+ worker_send_task_events=True, # Enable events to track task progress
55
+ # task_acks_late=True, # Acknowledge the task only after it has been executed)
56
+
57
+ )
58
+
59
+ redis_cache = Cache(key_prefix="process_results", ttl_days=7)
File without changes