mirror of https://github.com/dapr/quickstarts.git
Merge pull request #1160 from rochabr/jobs-python
Jobs API Quickstart for Python - HTTP
This commit is contained in:
commit
b060c50210
|
|
@ -0,0 +1,173 @@
|
||||||
|
# Dapr Jobs API (HTTP Client)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
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/)
|
||||||
|
|
||||||
|
## Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Run multi app run template
|
||||||
|
expected_stdout_lines:
|
||||||
|
- '== APP - job-service == Received job request...'
|
||||||
|
- '== APP - job-service == Executing maintenance job: Oil Change'
|
||||||
|
- '== APP - job-scheduler == Job Scheduled: C-3PO'
|
||||||
|
- '== APP - job-service == Received job request...'
|
||||||
|
- '== APP - job-service == Executing maintenance job: 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 executed after 2 seconds.
|
||||||
|
- The `C-3PO` job is being scheduled.
|
||||||
|
- The `C-3PO` job is being retrieved.
|
||||||
|
|
||||||
|
```text
|
||||||
|
== APP - job-scheduler == Job Scheduled: R2-D2
|
||||||
|
== 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 == Received job request...
|
||||||
|
== APP - job-service == Starting droid: C-3PO
|
||||||
|
== APP - job-service == Executing maintenance job: Limb Calibration
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
||||||
|
|
||||||
|
2. Stop and clean up application processes
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Stop multi-app run
|
||||||
|
sleep: 5
|
||||||
|
-->
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dapr stop -f .
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
||||||
|
|
||||||
|
## Run apps individually
|
||||||
|
|
||||||
|
### Start the job service
|
||||||
|
|
||||||
|
1. Open a terminal and run the `job-service` app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd job-service
|
||||||
|
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 -- python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schedule jobs
|
||||||
|
|
||||||
|
1. On a new terminal window, schedule the `R2-D2` Job using the Jobs API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
http://localhost:6280/v1.0-alpha1/jobs/R2D2 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"@type": "type.googleapis.com/google.protobuf.StringValue",
|
||||||
|
"value": "R2-D2:Oil Change"
|
||||||
|
},
|
||||||
|
"dueTime": "2s"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Back at the `job-service` app terminal window, the output should be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
== APP - job-service == Received job request...
|
||||||
|
== APP - job-service == Starting droid: R2-D2
|
||||||
|
== APP - job-service == Executing maintenance job: Oil Change
|
||||||
|
```
|
||||||
|
|
||||||
|
2. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
http://localhost:6280/v1.0-alpha1/jobs/c-3po \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"@type": "type.googleapis.com/google.protobuf.StringValue",
|
||||||
|
"value": "C-3PO:Limb Calibration"
|
||||||
|
},
|
||||||
|
"dueTime": "30s"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the following:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{"name":"C-3PO", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.StringValue", "expression":"C-3PO: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:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the command below to attempt to retrieve the deleted job:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
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"}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
version: 1
|
||||||
|
apps:
|
||||||
|
- appDirPath: ./job-service/
|
||||||
|
appID: job-service
|
||||||
|
appPort: 6200
|
||||||
|
daprHTTPPort: 6280
|
||||||
|
command: ["python3", "app.py"]
|
||||||
|
- appDirPath: ./job-scheduler/
|
||||||
|
appID: job-scheduler
|
||||||
|
appPort: 6300
|
||||||
|
daprHTTPPort: 6380
|
||||||
|
command: ["python3", "app.py"]
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
C3PO_JOB_BODY = {
|
||||||
|
"data": {"@type": "type.googleapis.com/google.protobuf.StringValue", "value": "C-3PO:Limb Calibration"},
|
||||||
|
"dueTime": "10s",
|
||||||
|
}
|
||||||
|
|
||||||
|
R2D2_JOB_BODY = {
|
||||||
|
"data": {"@type": "type.googleapis.com/google.protobuf.StringValue", "value": "R2-D2:Oil Change"},
|
||||||
|
"dueTime": "2s"
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
req_url,
|
||||||
|
json=job_body,
|
||||||
|
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}")
|
||||||
|
if response.text:
|
||||||
|
print(f"Response: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error scheduling job {job_name}: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_job_details(host: str, port: str, job_name: str) -> None:
|
||||||
|
req_url = f"{host}:{port}/v1.0-alpha1/jobs/{job_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(req_url, timeout=15)
|
||||||
|
if response.status_code in [200, 204]:
|
||||||
|
print(f"Job details for {job_name}: {response.text}")
|
||||||
|
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)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Wait for services to be ready
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
|
||||||
|
scheduler_dapr_http_port = os.getenv('SCHEDULER_DAPR_HTTP_PORT', '6280')
|
||||||
|
|
||||||
|
# Schedule R2-D2 job
|
||||||
|
schedule_job(dapr_host, scheduler_dapr_http_port, "R2-D2", R2D2_JOB_BODY)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Schedule C-3PO job
|
||||||
|
schedule_job(dapr_host, scheduler_dapr_http_port, "C-3PO", C3PO_JOB_BODY)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Get C-3PO job details
|
||||||
|
get_job_details(dapr_host, scheduler_dapr_http_port, "C-3PO")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
|
class DroidJob:
|
||||||
|
def __init__(self, droid: str, task: str):
|
||||||
|
self.droid = droid
|
||||||
|
self.task = task
|
||||||
|
|
||||||
|
def set_droid_job(decoded_value: str) -> DroidJob:
|
||||||
|
# Remove newlines from decoded value and split into droid and task
|
||||||
|
droid_str = decoded_value.replace('\n', '')
|
||||||
|
droid_array = droid_str.split(':')
|
||||||
|
return DroidJob(droid_array[0], droid_array[1])
|
||||||
|
|
||||||
|
class JobHandler(BaseHTTPRequestHandler):
|
||||||
|
def _send_response(self, status_code: int, message: str = ""):
|
||||||
|
self.send_response(status_code)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
if message:
|
||||||
|
self.wfile.write(json.dumps({"message": message}).encode('utf-8'))
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
print('Received job request...', flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if path starts with /job/
|
||||||
|
if not self.path.startswith('/job/'):
|
||||||
|
self._send_response(404, "Not Found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read request body
|
||||||
|
content_length = int(self.headers.get('Content-Length', 0))
|
||||||
|
raw_data = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
|
||||||
|
# Parse outer JSON data
|
||||||
|
job_data = json.loads(raw_data)
|
||||||
|
|
||||||
|
# Extract value directly from the job data
|
||||||
|
value = job_data.get('value', '')
|
||||||
|
|
||||||
|
# Create DroidJob from value
|
||||||
|
droid_job = set_droid_job(value)
|
||||||
|
|
||||||
|
print("Starting droid: " + droid_job.droid, flush=True)
|
||||||
|
print("Executing maintenance job: " + droid_job.task, flush=True)
|
||||||
|
|
||||||
|
self._send_response(200)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("Error processing job request:", flush= True)
|
||||||
|
print(traceback.format_exc())
|
||||||
|
self._send_response(400, f"Error processing job: {str(e)}")
|
||||||
|
|
||||||
|
def run_server(port: int):
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = HTTPServer(server_address, JobHandler)
|
||||||
|
print("Server started on port " + str(port), flush=True)
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app_port = int(os.getenv('APP_PORT', '6200'))
|
||||||
|
run_server(app_port)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
include ../../../docker.mk
|
||||||
|
include ../../../validate.mk
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
requests==2.31.0
|
||||||
Loading…
Reference in New Issue