Merge pull request #1230 from kendallroden/python-jobs-sdk-quickstart

init jobs quickstart for python sdk
This commit is contained in:
Alice Gibbons 2025-09-01 11:32:14 +01:00 committed by GitHub
commit 93becdd9a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 618 additions and 60 deletions

View File

@ -4,7 +4,6 @@ In this quickstart, you'll schedule, get, and delete a job using Dapr's Job API.
Visit [this](https://docs.dapr.io/developing-applications/building-blocks/jobs/) link for more information about Dapr and the Jobs API.
This quickstart includes two apps:
- `job-scheduler/app.py`, responsible for scheduling, retrieving and deleting jobs.
@ -21,31 +20,42 @@ This quickstart includes two apps:
- `JOB_SERVICE_DAPR_HTTP_PORT`: The Dapr HTTP port of the job-service (default: 6280)
- `DAPR_HOST`: The Dapr host address (default: http://localhost)
## Install dependencies
<!-- STEP
name: Install python dependencies
-->
```bash
pip3 install -r requirements.txt
```
<!-- END_STEP -->
## Run all apps with multi-app run template file
This section shows how to run both applications at once using [multi-app run template files](https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/) with `dapr run -f .`. This enables you to test the interactions between multiple applications and will `schedule`, `run`, `get`, and `delete` jobs within a single process.
Open a new terminal window and run the multi app run template:
1. Build the apps:
<!-- STEP
name: Install python dependencies
-->
```bash
pip3 install -r requirements.txt
```
<!-- END_STEP -->
2. Run the multi app run template:
<!-- STEP
name: Run multi app run template
expected_stdout_lines:
- '== APP - job-scheduler == Sending request to schedule job: R2-D2'
- '== APP - job-scheduler == Job scheduled: R2-D2'
- '== APP - job-scheduler == Sending request to retrieve job: R2-D2'
- '== APP - job-scheduler == Job details for R2-D2: {"name":"R2-D2", "dueTime":"15s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"R2-D2:Oil Change"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}'
- '== APP - job-scheduler == Sending request to schedule job: C-3PO'
- '== APP - job-scheduler == Job scheduled: C-3PO'
- '== APP - job-service == Received job request...'
- '== APP - job-service == Starting droid: R2-D2'
- '== APP - job-service == Executing maintenance job: Oil Change'
- '== APP - job-scheduler == Job Scheduled: C-3PO'
- '== APP - job-scheduler == Sending request to retrieve job: C-3PO'
- '== APP - job-scheduler == Job details for C-3PO: {"name":"C-3PO", "dueTime":"20s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"C-3PO:Limb Calibration"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}'
- '== APP - job-service == Received job request...'
- '== APP - job-service == Starting droid: C-3PO'
- '== APP - job-service == Executing maintenance job: Limb Calibration'
expected_stderr_lines:
expected_stderr_lines: []
output_match_mode: substring
match_order: none
background: true
@ -60,33 +70,37 @@ dapr run -f .
The terminal console output should look similar to this, where:
- The `R2-D2` job is being scheduled.
- The `R2-D2` job is being executed after 2 seconds.
- The `R2-D2` job is being retrieved.
- The `C-3PO` job is being scheduled.
- The `C-3PO` job is being retrieved.
- The `R2-D2` job is being executed after 15 seconds.
- The `C-3PO` job is being executed after 20 seconds.
```text
== APP - job-scheduler == Job Scheduled: R2-D2
== APP - job-scheduler == Sending request to schedule job: R2-D2
== APP - job-scheduler == Job scheduled: R2-D2
== APP - job-scheduler == Sending request to retrieve job: R2-D2
== APP - job-scheduler == Job details for R2-D2: {"name":"R2-D2", "dueTime":"15s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"R2-D2:Oil Change"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}
== APP - job-scheduler == Sending request to schedule job: C-3PO
== APP - job-scheduler == Job scheduled: C-3PO
== APP - job-service == Received job request...
== APP - job-service == Starting droid: R2-D2
== APP - job-service == Executing maintenance job: Oil Change
== APP - job-scheduler == Job Scheduled: C-3PO
== APP - job-scheduler == Job details: {"name":"C-3PO", "dueTime":"30s", "data":{"@type":"ttype.googleapis.com/google.protobuf.StringValue", "expression":"C-3PO:Limb Calibration"}}
```
After 30 seconds, the terminal output should present the `C-3PO` job being processed:
```text
== APP - job-service == 127.0.0.1 - - "POST /job/R2-D2 HTTP/1.1" 200 -
== APP - job-scheduler == Sending request to retrieve job: C-3PO
== APP - job-scheduler == Job details for C-3PO: {"name":"C-3PO", "dueTime":"20s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"C-3PO:Limb Calibration"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}
== APP - job-service == Received job request...
== APP - job-service == Starting droid: C-3PO
== APP - job-service == Executing maintenance job: Limb Calibration
== APP - job-service == 127.0.0.1 - - "POST /job/C-3PO HTTP/1.1" 200 -
```
<!-- END_STEP -->
2. Stop and clean up application processes
2. Stop and clean up application processes using a new terminal window.
<!-- STEP
name: Stop multi-app run
name: Stop multi-app run
sleep: 5
-->
@ -98,18 +112,20 @@ dapr stop -f .
## Run apps individually
### Start the job service
### Schedule jobs
1. Open a terminal and run the `job-service` app:
1. Open a terminal and run the `job-service` app. Build the dependencies if you haven't already.
```bash
pip3 install -r requirements.txt
```
```bash
cd job-service
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 -- python app.py
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 --dapr-grpc-port 6281 -- python app.py
```
### Schedule jobs
1. On a new terminal window, schedule the `R2-D2` Job using the Jobs API:
2. In a new terminal window, schedule the `R2-D2` Job using the Jobs API:
```bash
curl -X POST \
@ -124,7 +140,7 @@ curl -X POST \
}'
```
Back at the `job-service` app terminal window, the output should be:
In the `job-service` app terminal window, the output should be:
```text
== APP - job-service == Received job request...
@ -132,7 +148,7 @@ Back at the `job-service` app terminal window, the output should be:
== APP - job-service == Executing maintenance job: Oil Change
```
2. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
3. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
```bash
curl -X POST \
@ -158,7 +174,7 @@ curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: appli
You should see the following:
```text
{"name":"C-3PO", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.StringValue", "expression":"C-3PO:Limb Calibration"}}
{"name":"c-3po", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"C-3PO:Limb Calibration"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}
```
### Delete a scheduled job
@ -178,5 +194,5 @@ curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: appli
You should see an error message indicating that the job was not found:
```text
{"errorCode":"ERR_JOBS_NOT_FOUND","message":"job not found: app||default||job-service||c-3po"}
```
{"errorCode":"DAPR_SCHEDULER_GET_JOB","message":"failed to get job due to: rpc error: code = NotFound desc = job not found: c-3po","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","domain":"dapr.io","metadata":{"appID":"job-service","namespace":"default"},"reason":"DAPR_SCHEDULER_GET_JOB"}]}
```

View File

@ -5,17 +5,23 @@ import json
C3PO_JOB_BODY = {
"data": {"@type": "type.googleapis.com/google.protobuf.StringValue", "value": "C-3PO:Limb Calibration"},
"dueTime": "10s",
"dueTime": "20s",
}
R2D2_JOB_BODY = {
"data": {"@type": "type.googleapis.com/google.protobuf.StringValue", "value": "R2-D2:Oil Change"},
"dueTime": "2s"
"dueTime": "15s"
}
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
job_service_dapr_http_port = os.getenv('JOB_SERVICE_DAPR_HTTP_PORT', '6280')
def schedule_job(host: str, port: str, job_name: str, job_body: dict) -> None:
req_url = f"{host}:{port}/v1.0-alpha1/jobs/{job_name}"
print(f"Sending request to schedule job: {job_name}", flush=True)
try:
response = requests.post(
req_url,
@ -23,51 +29,57 @@ def schedule_job(host: str, port: str, job_name: str, job_body: dict) -> None:
headers={"Content-Type": "application/json"},
timeout=15
)
# Accept both 200 and 204 as success codes
if response.status_code not in [200, 204]:
raise Exception(f"Failed to schedule job. Status code: {response.status_code}, Response: {response.text}")
print(f"Job Scheduled: {job_name}")
raise Exception(
f"Failed to schedule job. Status code: {response.status_code}, Response: {response.text}")
print(f"Job scheduled: {job_name}", flush=True)
if response.text:
print(f"Response: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Error scheduling job {job_name}: {str(e)}")
print(f"Error scheduling job {job_name}: {str(e)}", flush=True)
raise
def get_job_details(host: str, port: str, job_name: str) -> None:
req_url = f"{host}:{port}/v1.0-alpha1/jobs/{job_name}"
print(f"Sending request to retrieve job: {job_name}", flush=True)
try:
response = requests.get(req_url, timeout=15)
if response.status_code in [200, 204]:
print(f"Job details for {job_name}: {response.text}")
print(f"Job details for {job_name}: {response.text}", flush=True)
else:
print(f"Failed to get job details. Status code: {response.status_code}, Response: {response.text}")
print(
f"Failed to get job details. Status code: {response.status_code}, Response: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Error getting job details for {job_name}: {str(e)}")
print(
f"Error getting job details for {job_name}: {str(e)}", flush=True)
raise
def main():
# Wait for services to be ready
time.sleep(5)
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
job_service_dapr_http_port = os.getenv('JOB_SERVICE_DAPR_HTTP_PORT', '6280')
# Schedule R2-D2 job
schedule_job(dapr_host, job_service_dapr_http_port, "R2-D2", R2D2_JOB_BODY)
time.sleep(5)
# Get R2-D2 job details
get_job_details(dapr_host, job_service_dapr_http_port, "R2-D2")
time.sleep(5)
# Schedule C-3PO job
schedule_job(dapr_host, job_service_dapr_http_port, "C-3PO", C3PO_JOB_BODY)
time.sleep(5)
# Get C-3PO job details
get_job_details(dapr_host, job_service_dapr_http_port, "C-3PO")
time.sleep(5)
time.sleep(30)
if __name__ == "__main__":
main()
main()

View File

@ -25,7 +25,6 @@ class JobHandler(BaseHTTPRequestHandler):
def do_POST(self):
print('Received job request...', flush=True)
try:
# Check if path starts with /job/
if not self.path.startswith('/job/'):
@ -51,7 +50,7 @@ class JobHandler(BaseHTTPRequestHandler):
self._send_response(200)
except Exception as e:
print("Error processing job request:", flush= True)
print("Error processing job request:", flush=True)
print(traceback.format_exc())
self._send_response(400, f"Error processing job: {str(e)}")

210
jobs/python/sdk/README.md Normal file
View File

@ -0,0 +1,210 @@
# Dapr Jobs API (SDK)
In this quickstart, you'll schedule, get, and delete a job using Dapr's Job API. This API is responsible for scheduling and running jobs at a specific time or interval.
Visit [this](https://docs.dapr.io/developing-applications/building-blocks/jobs/) link for more information about Dapr and the Jobs API.
> **Note:** This example leverages the Python SDK. If you are looking for the example using only HTTP requests, [click here](../http/).
This quickstart includes two apps:
- `job-scheduler/app.py`, responsible for scheduling, retrieving and deleting jobs.
- `job-service/app.py`, responsible for handling the triggered jobs.
## Prerequisites
- [Python 3.8+](https://www.python.org/downloads/)
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
## Run all apps with multi-app run template file
This section shows how to run both applications at once using [multi-app run template files](https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/) with `dapr run -f .`. This enables you to test the interactions between multiple applications and will `schedule`, `run`, `get`, and `delete` jobs within a single process.
1. Build the apps:
<!-- STEP
name: Install python dependencies
-->
```bash
pip3 install -r requirements.txt
```
<!-- END_STEP -->
2. Run the multi app run template:
<!-- STEP
name: Run multi app run template
expected_stdout_lines:
- '== APP - job-scheduler-sdk == Sending request to schedule job: R2-D2'
- '== APP - job-service-sdk == Scheduling job: R2-D2'
- '== APP - job-service-sdk == Job scheduled: R2-D2'
- '== APP - job-scheduler-sdk == Response: {"name":"R2-D2","job":"Oil Change","dueTime":15}'
- '== APP - job-scheduler-sdk == Sending request to retrieve job: R2-D2'
- '== APP - job-service-sdk == Retrieving job: R2-D2'
- '== APP - job-scheduler-sdk == Job details for R2-D2: {"name":"R2-D2","due_time":"15s","data":{"droid":"R2-D2","task":"Oil Change"}}'
- '== APP - job-scheduler-sdk == Sending request to schedule job: C-3PO'
- '== APP - job-service-sdk == Scheduling job: C-3PO'
- '== APP - job-service-sdk == Job scheduled: C-3PO'
- '== APP - job-service-sdk == Starting droid: R2-D2'
- '== APP - job-service-sdk == Executing maintenance job: Oil Change'
- '== APP - job-scheduler-sdk == Response: {"name":"C-3PO","job":"Limb Calibration","dueTime":20}'
- '== APP - job-scheduler-sdk == Sending request to retrieve job: C-3PO'
- '== APP - job-service-sdk == Retrieving job: C-3PO'
- '== APP - job-scheduler-sdk == Job details for C-3PO: {"name":"C-3PO","due_time":"20s","data":{"droid":"C-3PO","task":"Limb Calibration"}}'
expected_stderr_lines:
output_match_mode: substring
match_order: none
background: true
sleep: 60
timeout_seconds: 120
-->
```bash
dapr run -f .
```
The terminal console output should look similar to this, where:
- The `R2-D2` job is being scheduled.
- The `R2-D2` job is being retrieved.
- The `C-3PO` job is being scheduled.
- The `C-3PO` job is being retrieved.
- The `R2-D2` job is being executed after 15 seconds.
- The `C-3PO` job is being executed after 20 seconds.
```text
== APP - job-scheduler-sdk == Sending request to schedule job: R2-D2
== APP - job-service-sdk == Scheduling job: R2-D2
== APP - job-service-sdk == client.schedule_job_alpha1(job=job, overwrite=True)
== APP - job-service-sdk == Job scheduled: R2-D2
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "POST /scheduleJob HTTP/1.1" 200 OK
== APP - job-scheduler-sdk == Response: {"name":"R2-D2","job":"Oil Change","dueTime":15}
== APP - job-scheduler-sdk == Sending request to retrieve job: R2-D2
== APP - job-service-sdk == job = client.get_job_alpha1(name)
== APP - job-service-sdk == Retrieving job: R2-D2
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "GET /getJob/R2-D2 HTTP/1.1" 200 OK
== APP - job-scheduler-sdk == Job details for R2-D2: {"name":"R2-D2","due_time":"15s","data":{"droid":"R2-D2","task":"Oil Change"}}
== APP - job-scheduler-sdk == Sending request to schedule job: C-3PO
== APP - job-service-sdk == Scheduling job: C-3PO
== APP - job-service-sdk == Job scheduled: C-3PO
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "POST /scheduleJob HTTP/1.1" 200 OK
== APP - job-service-sdk == Starting droid: R2-D2
== APP - job-service-sdk == Executing maintenance job: Oil Change
== APP - job-service-sdk == INFO: 127.0.0.1:57206 - "POST /job/R2-D2 HTTP/1.1" 200 OK
== APP - job-scheduler-sdk == Response: {"name":"C-3PO","job":"Limb Calibration","dueTime":20}
== APP - job-scheduler-sdk == Sending request to retrieve job: C-3PO
== APP - job-service-sdk == Retrieving job: C-3PO
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "GET /getJob/C-3PO HTTP/1.1" 200 OK
== APP - job-scheduler-sdk == Job details for C-3PO: {"name":"C-3PO","due_time":"20s","data":{"droid":"C-3PO","task":"Limb Calibration"}}
```
<!-- END_STEP -->
2. Stop and clean up application processes using a new terminal window
<!-- STEP
name: Stop multi-app run
sleep: 5
-->
```bash
dapr stop -f .
```
<!-- END_STEP -->
## Run apps individually
### Schedule jobs
1. Open a terminal and run the `job-service-sdk` app. Build the dependencies if you haven't already.
```bash
pip3 install -r requirements.txt
```
```bash
cd job-service-sdk
dapr run --app-id job-service-sdk --app-port 6200 --dapr-http-port 6280 --dapr-grpc-port 6281 -- python app.py
```
2. In a new terminal window, schedule the `R2-D2` Job using the Jobs API:
```bash
curl -X POST \
http://localhost:6200/scheduleJob \
-H "Content-Type: application/json" \
-d '{
"name": "R2-D2",
"job": "Oil Change",
"dueTime": 2
}'
```
In the `job-service-sdk` terminal window, the output should be:
```text
== APP == Scheduling job: R2-D2
== APP == Job scheduled: R2-D2
== APP == INFO: 127.0.0.1:59756 - "POST /scheduleJob HTTP/1.1" 200 OK
== APP == Starting droid: R2-D2
== APP == Executing maintenance job: Oil Change
== APP == INFO: 127.0.0.1:59759 - "POST /job/R2-D2 HTTP/1.1" 200 OK
```
3. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
```bash
curl -X POST \
http://localhost:6200/scheduleJob \
-H "Content-Type: application/json" \
-d '{
"name": "C-3PO",
"job": "Limb Calibration",
"dueTime": 30
}'
```
### Get a scheduled job
1. On the same terminal window, run the command below to get the recently scheduled `C-3PO` job:
```bash
curl -X GET http://localhost:6200/getJob/C-3PO -H "Content-Type: application/json"
```
You should see the following:
```text
{"name":"C-3PO","due_time":"30s","data":{"droid":"C-3PO","task":"Limb Calibration"}}
```
### Delete a scheduled job
1. On the same terminal window, run the command below to delete the recently scheduled `C-3PO` job:
```bash
curl -X DELETE http://localhost:6200/deleteJob/C-3PO -H "Content-Type: application/json"
```
You should see the following:
```text
{"message":"Job deleted"}
```
2. Run the command below to attempt to retrieve the deleted job:
```bash
curl -X GET http://localhost:6200/getJob/C-3PO -H "Content-Type: application/json"
```
In the `job-service-sdk` terminal window, the output should be similar to the following:
```text
{"detail":"<_InactiveRpcError of RPC that terminated with:\n\tstatus = StatusCode.INTERNAL\n\tdetails = \"failed to get job due to: rpc error: code = NotFound desc = job not found: C-3PO\"\n\tdebug_error_string = \"UNKNOWN:Error received from peer ipv4:127.0.0.1:6281 {grpc_status:13, grpc_message:\"failed to get job due to: rpc error: code = NotFound desc = job not found: C-3PO\"}\"\n>"}
```

18
jobs/python/sdk/dapr.yaml Normal file
View File

@ -0,0 +1,18 @@
version: 1
apps:
- appDirPath: ./job-service/
appID: job-service-sdk
appPort: 6200
daprHTTPPort: 6280
daprGRPCPort: 6281
appLogDestination: console
daprdLogDestination: console
command: ["python3", "app.py"]
- appDirPath: ./job-scheduler/
appID: job-scheduler-sdk
appPort: 6300
daprHTTPPort: 6380
daprGRPCPort: 6381
appLogDestination: console
daprdLogDestination: console
command: ["python3", "app.py"]

View File

@ -0,0 +1,108 @@
import os
import json
import time
from typing import Optional
from dataclasses import dataclass
import requests
@dataclass
class DroidJob:
name: Optional[str] = None
job: Optional[str] = None
due_time: int = 1
# Job details
r2d2_job = DroidJob(name="R2-D2", job="Oil Change", due_time=15)
c3po_job = DroidJob(name="C-3PO", job="Limb Calibration", due_time=20)
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
dapr_port = os.getenv('DAPR_HTTP_PORT', '3500')
def schedule_job(job: DroidJob) -> None:
print(f"Sending request to schedule job: {job.name}", flush=True)
try:
# Convert the job to a dictionary for JSON serialization
job_data = {
"name": job.name,
"job": job.job,
"dueTime": job.due_time
}
# Use HTTP client to call the job-service-sdk via Dapr
req_url = f"{dapr_host}:{dapr_port}/v1.0/invoke/job-service-sdk/method/scheduleJob"
response = requests.post(
req_url,
json=job_data,
headers={"Content-Type": "application/json"},
timeout=15
)
# Accept both 200 and 204 as success codes
if response.status_code not in [200, 204]:
raise Exception(
f"Failed to schedule job. Status code: {response.status_code}, Response: {response.text}", flush=True)
if response.text:
print(f"Response: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Error scheduling job {job.name}: {str(e)}", flush=True)
raise
def get_job_details(job: DroidJob) -> None:
print(f"Sending request to retrieve job: {job.name}", flush=True)
try:
# Use HTTP client to call the job-service-sdk via Dapr
req_url = f"{dapr_host}:{dapr_port}/v1.0/invoke/job-service-sdk/method/getJob/{job.name}"
response = requests.get(req_url)
if response.status_code in [200, 204]:
print(f"Job details for {job.name}: {response.text}", flush=True)
else:
print(
f"Failed to get job details. Status code: {response.status_code}, Response: {response.text}")
except requests.exceptions.RequestException as e:
print(
f"Error getting job details for {job.name}: {str(e)}", flush=True)
raise
def main():
# Allow time for the job-service-sdk to start
time.sleep(5)
try:
# Schedule R2-D2 job
schedule_job(r2d2_job)
time.sleep(5)
# Get R2-D2 job details
get_job_details(r2d2_job)
time.sleep(5)
# Schedule C-3PO job
schedule_job(c3po_job)
time.sleep(5)
# Get C-3PO job details
get_job_details(c3po_job)
time.sleep(30) # Allow time for jobs to complete
except Exception as e:
print(f"Error: {e}")
exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,188 @@
import os
import json
import logging
from datetime import timedelta
from typing import Optional
from fastapi import FastAPI, HTTPException, Response
from pydantic import BaseModel
from dapr.clients import DaprClient, Job, DropFailurePolicy, ConstantFailurePolicy
# Add protobuf availability check
try:
from google.protobuf.any_pb2 import Any as GrpcAny
PROTOBUF_AVAILABLE = True
except ImportError:
PROTOBUF_AVAILABLE = False
print('Warning: protobuf not available, jobs with data will be scheduled without data', flush=True)
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize FastAPI app
app = FastAPI(title="Dapr Jobs Service", version="1.0.0")
# Get app port from environment
app_port = int(os.getenv('APP_PORT', '6200'))
# Pydantic models for request/response
class JobData(BaseModel):
droid: Optional[str] = None
task: Optional[str] = None
class DroidJob(BaseModel):
name: Optional[str] = None
job: Optional[str] = None
dueTime: int
def create_job_data(data_dict):
# Create job data from a dictionary
if not PROTOBUF_AVAILABLE:
return None
data = GrpcAny()
data.value = json.dumps(data_dict).encode('utf-8')
return data
@app.post("/scheduleJob")
def schedule_job(droid_job: DroidJob, response: Response):
print(f"Scheduling job: {droid_job.name}", flush=True)
if not droid_job.name or not droid_job.job:
raise HTTPException(
status_code=400, detail="Job must contain a name and a task")
try:
# Create job data payload
job_data = JobData(
droid=droid_job.name,
task=droid_job.job
)
# Create the job
job = Job(
name=droid_job.name,
due_time=f"{droid_job.dueTime}s",
data=create_job_data(job_data.model_dump())
)
with DaprClient() as d:
# Schedule the job
d.schedule_job_alpha1(job=job, overwrite=True)
print(f"Job scheduled: {droid_job.name}", flush=True)
# Set 200 status and return the payload
response.status_code = 200
return droid_job
except Exception as e:
print(f"Error scheduling job: {e}")
raise HTTPException(
status_code=500, detail=f"Error scheduling job: {str(e)}")
@app.get("/getJob/{name}")
async def get_job(name: str):
print(f"Retrieving job: {name}")
if not name:
raise HTTPException(status_code=400, detail="Job name required")
try:
with DaprClient() as d:
job = d.get_job_alpha1(name)
if job is None:
raise HTTPException(status_code=404, detail="Job not found")
# Convert protobuf job object to dict for JSON serialization
job_dict = {
"name": job.name,
"due_time": job.due_time,
}
# Handle job data if present
if job.data:
try:
payload = json.loads(job.data.value.decode('utf-8'))
job_dict["data"] = payload
except Exception:
job_dict["data"] = f"<binary data, {len(job.data.value)} bytes>"
else:
job_dict["data"] = None
return job_dict
except Exception as e:
print(f"Error getting job: {e}")
raise HTTPException(status_code=400, detail=str(e))
@app.delete("/deleteJob/{name}")
async def delete_job(name: str):
print(f"Deleting job: {name}")
if not name:
raise HTTPException(status_code=400, detail="Job name required")
try:
with DaprClient() as d:
job_details = d.delete_job_alpha1(name)
print(f"Job deleted: {name}")
return {"message": "Job deleted"}
except Exception as e:
print(f"Error deleting job: {e}")
raise HTTPException(status_code=400, detail="Error deleting job")
@app.post("/job/{job_name}")
async def handle_job(job_name: str, job_payload: dict):
try:
# Extract job data from payload
# The payload structure depends on how the job was scheduled
if "droid" in job_payload and "task" in job_payload:
droid = job_payload["droid"]
task = job_payload["task"]
else:
# Fallback: try to extract from the raw payload
payload_str = str(job_payload)
print(f"Raw payload: {payload_str}")
# Try to parse as JSON if it's a string
if isinstance(payload_str, str):
try:
parsed = json.loads(payload_str)
droid = parsed.get("droid", "Unknown")
task = parsed.get("task", "Unknown")
except json.JSONDecodeError:
droid = "Unknown"
task = payload_str
else:
droid = "Unknown"
task = str(job_payload)
# Execute the job
print(f"Starting droid: {droid}", flush=True)
print(f"Executing maintenance job: {task}", flush=True)
return {"status": "success", "droid": droid, "task": task}
except Exception as ex:
print(f"Failed to handle job {job_name}")
print(f"Error handling job: {ex}")
raise HTTPException(
status_code=500, detail=f"Error handling job: {str(ex)}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=app_port)

2
jobs/python/sdk/makefile Normal file
View File

@ -0,0 +1,2 @@
include ../../../docker.mk
include ../../../validate.mk

View File

@ -0,0 +1,5 @@
requests==2.31.0
dapr==1.16.0rc1
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0