def get_min_distance_house_travel_distance(task: ExternalTask) -> TaskResult: """ Gets the distance between the user's house and a travel company. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("get_min_distance_house_travel_distance") GEOGRAPHICAL_DISTACE_SERVICE = environ.get( "GEOGRAPHICAL_DISTACE_SERVICE", "http://geographical_distances:8080") travel_company = str(task.get_variable("travel_company")) travel_company_url = travel_company.split(';')[0] travel_company_address = travel_company.split(';')[1] offer_purchase_data = OfferPurchaseData.from_dict( json.loads(task.get_variable("offer_purchase_data"))) distances = json.loads(str(task.get_variable("distances"))) request = { "address_1": travel_company_address, "address_2": str(offer_purchase_data.address) } distance_request = requests.post(GEOGRAPHICAL_DISTACE_SERVICE + "/distance", json=request) distances.get("distances").append({ "company": travel_company_url, "distance": distance_request.text }) return task.complete(global_variables={"distances": json.dumps(distances)})
def send_wrong_offer_code(task: ExternalTask) -> TaskResult: """ Notifies the user that the code is invalid, expired or already in use. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("send_wrong_offer_code") user_communication_code = str(task.get_variable("user_communication_code")) # Connects to RabbitMQ and publishes the message connection = pika.BlockingConnection(pika.ConnectionParameters("acmesky_mq")) channel = connection.channel() channel.queue_declare(queue=user_communication_code, durable=True) error = PurchaseProcessInformation( message="Il codice offerta inserito non è valido, è in uso da parte di un altro utente o sono passate più di 24 ore da quando è stato inviato.", communication_code=user_communication_code, is_error=True, ) channel.basic_publish( exchange="", routing_key=user_communication_code, body=bytes(json.dumps(error.to_dict()), "utf-8"), properties=pika.BasicProperties(delivery_mode=2), ) connection.close() return task.complete()
def rehabilitation_offer_code(task: ExternalTask) -> TaskResult: """ Compensation task: if the verification offer code and payment sub process fail, this task rehabilitates the offer code for another try. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("rehabilitation_offer_code") offer_purchase_data = OfferPurchaseData.from_dict( json.loads(task.get_variable("offer_purchase_data"))) offer_code = offer_purchase_data.offer_code # Connects to postgreSQL Session = sessionmaker(bind=create_sql_engine()) session = Session() """ Gets the offer match that has to be rehabilitated from the DB. The offer match is the one blocked and with offer_code equal to the process offer_code variable. """ affected_rows = session.query(OfferMatch).filter( OfferMatch.offer_code == offer_code).update( {"blocked": False}, synchronize_session="fetch") if affected_rows < 1: session.rollback() logger.error( f"{affected_rows} matches were found for the given offer code. The offer code will not be rehabilitated." ) return task.complete() logger.info(f"{affected_rows} match was found for the given offer code.") session.commit() return task.complete()
def notify_user_via_prontogram(task: ExternalTask) -> TaskResult: """ For each offer code, sends a message to the related ProntoGram user. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("notify_user_via_prontogram") offer_codes = json.loads(task.get_variable("offer_codes")) offer_infos = json.loads(task.get_variable("offer_infos")) # Quotes are added at the beginning and end of the pg_username, [1:-1] removes them. prontogram_username = str(task.get_variable("prontogram_username"))[1:-1] logger.info(f"prontogram username: {prontogram_username}") logger.info(f"offer codes: {offer_codes}") for offer_code, offer_info in zip(offer_codes, offer_infos): prontogram_message = { "sender": "ACMESky", "receiver": prontogram_username, "body": f"ACMESky ha trovato per te la seguente offerta:\n{offer_info}\nInserisci il codice offerta {offer_code} sul sito di ACMESky per poterne usufruire. Affrettati, sarà valido per sole 24 ore!" } # logger.info(json.dumps(prontogram_message)) r = requests.post("http://prontogram_backend:8080/messages", json=prontogram_message) logger.info(f"ProntoGram response: {r.status_code}") if r.status_code >= 300: logger.warn(r.text) return task.complete()
def send_correct_offer_code(task: ExternalTask) -> TaskResult: """ Sends the confirmation that the offer code inserted is valid to the user :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("send_correct_offer_code") user_communication_code = str(task.get_variable("user_communication_code")) # Connects to RabbitMQ and publishes the message connection = pika.BlockingConnection(pika.ConnectionParameters(host="acmesky_mq")) channel = connection.channel() channel.queue_declare(queue=user_communication_code, durable=True) success = PurchaseProcessInformation(message=f"Il codice offerta inserito è valido.", communication_code=user_communication_code) channel.basic_publish( exchange="", routing_key=user_communication_code, body=bytes(json.dumps(success.to_dict()), "utf-8"), properties=pika.BasicProperties(delivery_mode=2), ) connection.close() return task.complete()
def fail_task_handler(task: ExternalTask): log_context = {"WORKER_ID": task.get_worker_id(), "TASK_ID": task.get_task_id(), "TOPIC": task.get_topic_name()} log_with_context("executing fail_task_handler", log_context) return task.failure("task failed", "task failed forced", 0, 10)
def register_interest(task: ExternalTask) -> TaskResult: """ Saves the interest in MongoDB. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("register_interest") interest = json.loads(task.get_variable("interest")) # The user pushes the interest without the field "offer_codes" (which is necessary for later updates). interest["offer_codes"] = [] """ Connection and save on MongoDB """ username = environ.get("MONGO_USER", "root") password = environ.get("MONGO_PASSWORD", "password") client = MongoClient(f"mongodb://{username}:{password}@acmesky_mongo:27017" ) # Connects to MongoDB acmesky_db = client['ACMESky'] # Selects the right DB interests_collection = acmesky_db[ 'interests'] # Selects the right document # Inserting into the DB only if it does not already exist. if not interests_collection.find_one(interest): interests_collection.insert_one(interest) return task.complete({"operation_result": "OK"})
def handle_task(task: ExternalTask) -> TaskResult: """ This task handler you need to implement with your business logic. After completion of business logic call either task.complete() or task.failure() or task.bpmn_error() to report status of task to Camunda """ # add your business logic here # ... print('Service invoked') cars = task.get_variable("cars") car_dict = json.loads(cars) de = [] da = [] for key in car_dict: if car_dict.get(key).get('country') == 'DE': de.append(car_dict.get(key).get('price')) if car_dict.get(key).get('country') == 'DK': da.append(car_dict.get(key).get('price')) duty = (sum(da) / len(da)) - (sum(de) / len(de)) # mark task either complete/failure/bpmnError based on outcome of your business logic # if failure: # # this marks task as failed in Camunda # return task.failure(error_message="task failed", error_details="failed task details", # max_retries=3, retry_timeout=5000) # elif bpmn_error: # return task.bpmn_error(error_code="BPMN_ERROR_CODE", error_message="BPMN Error occurred", # variables={"var1": "value1", "success": False}) # pass any output variables you may want to send to Camunda as dictionary to complete() return task.complete({"duty": duty})
def get_flight_offers(task: ExternalTask) -> TaskResult: """ Gets the flights from a Flight Company :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("get_flight_offers") url = task.get_variable("company") logger.info("Contacting: " + url) new_flights = requests.get(url + "/flights/offers").json() # Workaround for Camunda limitation on the length of the string that can be saved as process variable. if new_flights == {}: logger.info(f"Empty json from the flight company at URL {url}") return task.complete({'offers_0': dumps([]), 'offers_packets': 1}) else: # Workaround: Camunda string global variables can hold maximum 4000 chars per string. # Therefore we must split the dumped string every 3500 characters (just to be sure). stringified_flights = dumps(new_flights.get('flights')) offers_packets = (len(stringified_flights) // 3500) + 1 global_vars = {'offers_packets': offers_packets} for packet in range(offers_packets): start = packet * 3500 end = start + 3500 global_vars[f'offers_{packet}'] = stringified_flights[start:end] return task.complete(global_variables=global_vars)
def test_task_with_retries_returns_failure_task_result_with_decremented_retries(self): retries = 3 task = ExternalTask(context={"retries": retries}) task_result = task.failure(error_message="unknown error", error_details="error details here", max_retries=10, retry_timeout=1000) self.assertEqual(retries - 1, task_result.retries)
def generic_task_handler(task: ExternalTask): log_context = {"WORKER_ID": task.get_worker_id(), "TASK_ID": task.get_task_id(), "TOPIC": task.get_topic_name()} log_with_context("executing generic task handler", log_context) return task.complete()
def send_wrong_payment_status(task: ExternalTask) -> TaskResult: """ Notifies the user that the payment has timed out. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("send_wrong_payment_status") user_communication_code = str(task.get_variable("user_communication_code")) # Connect to RabbitMQ and publish the message connection = pika.BlockingConnection( pika.ConnectionParameters("acmesky_mq")) channel = connection.channel() channel.queue_declare(queue=user_communication_code, durable=True) error = PurchaseProcessInformation( message="Il processo di acquisto è fallito. Riprova nuovamente.", communication_code=user_communication_code, is_error=True, ) channel.basic_publish( exchange="", routing_key=user_communication_code, body=bytes(json.dumps(error.to_dict()), "utf-8"), properties=pika.BasicProperties(delivery_mode=2), ) connection.close() return task.complete()
def handle_task(task: ExternalTask): log_context = { "WORKER_ID": task.get_worker_id(), "TASK_ID": task.get_task_id(), "TOPIC": task.get_topic_name() } log_with_context( f"handle_task started: business key = {task.get_business_key()}", log_context) # simulate task execution execution_time = randint(0, 10) log_with_context( f"handle_task - business logic execution started for task: " f"it will execute for {execution_time} seconds", log_context) time.sleep(execution_time) # simulate that task results randomly into failure/BPMN error/complete failure = random_true() bpmn_error = False if failure else random_true() # override the values to simulate success/failure/BPMN error explicitly (if needed) failure, bpmn_error = False, False log_with_context( f"handle_task - business logic executed: failure: {failure}, bpmn_error: {bpmn_error}", log_context) return __handle_task_result(task, failure, bpmn_error)
def test_get_property_returns_value_for_property_present(self): task = ExternalTask( context={"extensionProperties": { "var1": "one", "var2": "two" }}) prop = task.get_extension_property("var1") self.assertEqual("one", prop)
def test_complete_returns_success_task_result(self): task = ExternalTask(context={}) task_result = task.complete({}) self.assertEqual(task, task_result.get_task()) self.assertEqual(task_result, task.get_task_result()) self.assertTrue(task_result.is_success()) self.assertFalse(task_result.is_failure()) self.assertFalse(task_result.is_bpmn_error())
def test_external_task_creation_from_context(self): context = { "id": "123", "workerId": "321", "topicName": "my_topic", "tenantId": "tenant1", "processInstanceId": "processInstanceId1", "variables": { "applicationId": { "type": "String", "value": "appId987", "valueInfo": {} } } } task = ExternalTask(context=context) self.assertEqual("123", task.get_task_id()) self.assertEqual("321", task.get_worker_id()) self.assertEqual("my_topic", task.get_topic_name()) self.assertEqual("tenant1", task.get_tenant_id()) self.assertEqual("processInstanceId1", task.get_process_instance_id()) self.assertDictEqual({"applicationId": "appId987"}, task.get_variables()) self.assertEqual("empty_task_result", str(task.get_task_result()))
def test_task_bpmn_error(self): task = ExternalTask({"id": "1", "topicName": "my_topic"}) expected_task_result = TaskResult.bpmn_error(task, error_code="bpmn_err_code_1", error_message="bpmn error") external_task_client = ExternalTaskClient(worker_id=1) responses.add(responses.POST, external_task_client.get_task_bpmn_error_url(task.get_task_id()), status=HTTPStatus.NO_CONTENT) executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client) actual_task_result = executor.execute_task(task, self.task_bpmn_error_action) self.assertEqual(str(expected_task_result), str(actual_task_result))
def verify_offer_code_validity(task: ExternalTask) -> TaskResult: """ Verifies that the offer code is valid, not expired and not already in use by another user. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("verify_offer_code_validity") offer_purchase_data = OfferPurchaseData.from_dict( json.loads(task.get_variable("offer_purchase_data"))) offer_code = offer_purchase_data.offer_code # Connects to PostgreSQL Session = sessionmaker(bind=create_sql_engine()) session = Session() user_communication_code = str(hash(offer_purchase_data)) # Checks if the offer matched is blocked matches = session.query(OfferMatch).filter( OfferMatch.offer_code == offer_code, OfferMatch.blocked == True).all() if len(matches) == 1: logger.error(f"Offer code is BLOCKED.") return task.complete( global_variables={ 'offer_code_validity': False, 'user_communication_code': user_communication_code }) # Checks if the offer match is not expired and sets it to blocked=True. affected_rows = session.query(OfferMatch).filter( OfferMatch.offer_code == offer_code, OfferMatch.creation_date >= datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(hours=24)).update({"blocked": True}, synchronize_session="fetch") if affected_rows < 1: session.rollback() logger.error( f"{affected_rows} matches were found for the given offer code.") return task.complete( global_variables={ 'offer_code_validity': False, 'user_communication_code': user_communication_code }) logger.info(f"{affected_rows} match was found for the given offer code.") session.commit() return task.complete( global_variables={ 'offer_code_validity': True, 'user_communication_code': user_communication_code })
async def number_check(task: ExternalTask) -> ExternalTaskResult: try: number = task.context_variables["number"] print(f"We received {number} for checking...") task.local_variables.set_variable( "result", "true" if int(number) % 2 != 0 else "false", Variables.ValueType.STRING ) return task.complete() except Exception as err: print(f"Oh no! Something went wrong: {err}") return task.failure()
async def test_task_fail(context, mocker: MockerFixture): error_message = "NotImplementedError" error_details = "This method has not been implemented" max_retries = 3 retry_timeout = 5 task = ExternalTask(context) res = task.failure( error_message=error_message, error_details=error_details, max_retries=max_retries, retry_timeout=retry_timeout, )
def test_task_complete(self): task = ExternalTask({"id": "1", "topicName": "my_topic"}) output_vars = {"var1": 1, "var2": "value", "var3": True} expected_task_result = TaskResult.success(task, output_vars) external_task_client = ExternalTaskClient(worker_id=1) responses.add(responses.POST, external_task_client.get_task_complete_url(task.get_task_id()), status=HTTPStatus.NO_CONTENT) executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client) actual_task_result = executor.execute_task(task, self.task_success_action) self.assertEqual(str(expected_task_result), str(actual_task_result))
def test_bpmn_error_returns_bpmn_error_task_result(self): task = ExternalTask(context={}) task_result = task.bpmn_error(error_code="bpmn_error_code_1", error_message="bpmn error") self.assertEqual(task, task_result.get_task()) self.assertEqual(task_result, task.get_task_result()) self.assertFalse(task_result.is_success()) self.assertFalse(task_result.is_failure()) self.assertTrue(task_result.is_bpmn_error()) self.assertEqual("bpmn_error_code_1", task_result.bpmn_error_code)
def book_transfer(task: ExternalTask) -> TaskResult: """ Contacts the chosen Travel Company and requests to book a travel using the SOAP protocol :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("get_min_distance_house_travel_distance") distances = json.loads(str(task.get_variable("distances"))) distances = distances.get("distances") offer_purchase_data = OfferPurchaseData.from_dict( json.loads(task.get_variable("offer_purchase_data"))) tickets = json.loads(str(task.get_variable("tickets"))) # Connects to PostgreSQL to get the offer match information Session = sessionmaker(bind=create_sql_engine()) session = Session() offer_match: OfferMatch = session.query(OfferMatch).get( {"offer_code": offer_purchase_data.offer_code}) # Identifies the Travel Company to contact, choosing the one nearer to the user's address. travel_company_to_contact = min(distances, key=lambda tc: tc.get("distance")) # Creates the SOAP Client and the datetime when then transfer will be booked for. # We need to replace the port for accessing the web server serving the WSDL interface. wsdl_url = travel_company_to_contact.get("company").replace( ":8080", ":8000") + "/travel_company.wsdl" soap_client = Client(wsdl=wsdl_url) outbound_departure_transfer_datetime = offer_match.outbound_flight.departure_datetime - timedelta( hours=4) comeback_arrival_transfer_datetime = offer_match.comeback_flight.arrival_datetime + timedelta( minutes=10) try: soap_response = soap_client.service.buyTransfers( departure_transfer_datetime=outbound_departure_transfer_datetime. strftime("%Y-%m-%dT%H:%M:%S"), customer_address=str(offer_purchase_data.address), airport_code=offer_match.outbound_flight.departure_airport_code, customer_name= f"{offer_purchase_data.name} {offer_purchase_data.surname}", arrival_transfer_datetime=comeback_arrival_transfer_datetime. strftime("%Y-%m-%dT%H:%M:%S")) tickets["transfers"] = [soap_response] return task.complete(global_variables={"tickets": json.dumps(tickets)}) except Fault: return task.failure("Book ticket", "Failure in booking ticket from travel company", max_retries=5, retry_timeout=10)
async def get_iovation_data(task: ExternalTask): # put the business logic here logger.info(f"get_iovation_data: {task}") failure = random_true() if failure: return task.failure("iovation task failed", "failed iovation task details", 3, 5000) return task.complete({ "success": True, "iovation_task_completed_on": str(datetime.now()) })
async def get_sentilink_data(task: ExternalTask): # put the business logic here logger.info(f"get_sentilink_data: {task}") is_bpmn_error = random_true() if is_bpmn_error: return task.bpmn_error("SentlinkDetectedFraud") return task.complete({ "success": True, "sentilink_task_completed_on": str(datetime.now()) })
def test_task_failure(self): task = ExternalTask({"id": "1", "topicName": "my_topic"}) expected_task_result = TaskResult.failure(task, error_message="unknown task failure", error_details="unknown error", retries=3, retry_timeout=30000) external_task_client = ExternalTaskClient(worker_id=1) responses.add(responses.POST, external_task_client.get_task_failure_url(task.get_task_id()), status=HTTPStatus.NO_CONTENT) executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client) actual_task_result = executor.execute_task(task, self.task_failure_action) self.assertEqual(str(expected_task_result), str(actual_task_result))
def validate_image(task: ExternalTask): """ To simulate BPMN/Failure/Success, this handler uses image name variable (to be passed when launching the process) """ log_context = { "WORKER_ID": task.get_worker_id(), "TASK_ID": task.get_task_id(), "TOPIC": task.get_topic_name() } log_with_context("executing validate_image", log_context) img_name = task.get_variable('imgName') if "poor" in img_name: return task.bpmn_error( "POOR_QUALITY_IMAGE", "Image quality is bad", { "img_rejection_code": "POOR_QUALITY_CODE_XX", "img_rejection_reason": f"Image quality must be at least GOOD" }) elif "jpg" in img_name: return task.complete({"img_approved": True}) elif "corrupt" in img_name: return task.failure("Cannot validate image", "image is corrupted", 0, default_config.get("retryTimeout")) else: return task.bpmn_error( "INVALID_IMAGE", "Image extension must be jpg", { "img_rejection_code": "INVALID_IMG_NAME", "img_rejection_reason": f"Image name {img_name} is invalid" })
def check_distance_house_airport(task: ExternalTask) -> TaskResult: """ Checks if the distance (that we got when contacting the Geographical Distance service) is congruent with the transfer bundle. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("check_distance_house_airport") GEOGRAPHICAL_DISTACE_SERVICE = environ.get( "GEOGRAPHICAL_DISTACE_SERVICE", "http://geographical_distances:8080") offer_purchase_data = OfferPurchaseData.from_dict( json.loads(task.get_variable("offer_purchase_data"))) # Connects to postgreSQL and get the offer purchased Session = sessionmaker(bind=create_sql_engine()) session = Session() offer_match: OfferMatch = session.query(OfferMatch).get( {"offer_code": offer_purchase_data.offer_code}) # Finds the name (used for the airport) of the departure airport airports_file = open("./camundaworkers/airports.csv", 'r') airports = csv.reader(airports_file) airport_address = None for row in airports: if row[4] == offer_match.outbound_flight.departure_airport_code: airport_address = row[1] airports_file.close() # Failure case: the airport cannot be found in the CSV. if not airport_address: logger.error( f"Cannot find airport associated with: {offer_match.outbound_flight.departure_airport_code}" ) return task.complete(global_variables={ "distance": "35" }) # 35 > 30, then the transfer won't be booked. request = { "address_1": airport_address, "address_2": str(offer_purchase_data.address) } distance_request = requests.post(GEOGRAPHICAL_DISTACE_SERVICE + "/distance", json=request) return task.complete(global_variables={"distance": distance_request.text})
def test_failure_returns_failure_task_result(self): task = ExternalTask(context={}) task_result = task.failure(error_message="unknown error", error_details="error details here", max_retries=3, retry_timeout=1000) self.assertEqual(task, task_result.get_task()) self.assertEqual(task_result, task.get_task_result()) self.assertFalse(task_result.is_success()) self.assertTrue(task_result.is_failure()) self.assertFalse(task_result.is_bpmn_error()) self.assertEqual("unknown error", task_result.error_message) self.assertEqual("error details here", task_result.error_details) self.assertEqual(3, task_result.retries) self.assertEqual(1000, task_result.retry_timeout)
def verify_condition_for_travel_booking(task: ExternalTask) -> TaskResult: """ Verifies the price condition for the transfer bundle. :param task: the current task instance :return: the task result """ logger = get_logger() logger.info("verify_condition_for_travel_booking") total_amount = float(task.get_variable("total_amount")) if total_amount >= 1000: return task.complete( global_variables={"can_book_travel_company": True}) else: return task.complete( global_variables={"can_book_travel_company": False})