def download_file_from_variable(self, variable_name: str) -> str: """ For performance reasons, files are not retrieved automatically during `fetch workload`. If your task requires a file that is attached to a process instance, you need to download the file explicitly. Example: | ${variables} | *fetch workload* | _first_task_in_demo_ | | | *Dictionary Should Contain Key* | _${variables}_ | _my_file_ | | ${file} | *Download File From Variable* | ${variables}[my_file] | | """ if not self.FETCH_RESPONSE: logger.warn( 'Could not download file for variable. Maybe you did not fetch and lock a workitem before?' ) else: with self._shared_resources.api_client as api_client: api_instance = openapi_client.ProcessInstanceApi(api_client) try: response = api_instance.get_process_instance_variable_binary( id=self.FETCH_RESPONSE.process_instance_id, var_name=variable_name) logger.debug(response) except ApiException as e: raise ApiException( f"Exception when calling ExternalTaskApi->get_process_instance_variable_binary: {e}\n" ) return response
def deploy_multiple_files(self, *args): """ # Due to https://jira.camunda.com/browse/CAM-13105 we cannot use generic camunda client when dealing with # multiple files. We have to use plain REST then. """ fields = { 'deployment-name': f'{os.path.basename(args[0])}', } for file in args: filename = os.path.basename(file) fields[f'{filename}'] = (filename, open(file, 'rb'), 'application/octet-stream') multipart_data = MultipartEncoder(fields=fields) headers = self._shared_resources.api_client.default_headers.copy() headers['Content-Type'] = multipart_data.content_type logger.debug(multipart_data.fields) response = requests.post( f'{self._shared_resources.camunda_url}/deployment/create', data=multipart_data, headers=headers) json = response.json() try: response.raise_for_status() logger.debug(json) except HTTPError as e: raise ApiException(json) return json
def throw_bpmn_error(self, error_code: str, error_message: str = None, variables: Dict[str, Any] = None, files: Dict = None): if not self.FETCH_RESPONSE: logger.warn( 'No task to complete. Maybe you did not fetch and lock a workitem before?' ) else: with self._shared_resources.api_client as api_client: api_instance = openapi_client.ExternalTaskApi(api_client) variables = CamundaResources.convert_dict_to_openapi_variables( variables) openapi_files = CamundaResources.convert_file_dict_to_openapi_variables( files) variables.update(openapi_files) bpmn_error = openapi_client.ExternalTaskBpmnError( worker_id=self.WORKER_ID, error_message=error_message, error_code=error_code, variables=variables) try: logger.debug(f"Sending BPMN error for task:\n{bpmn_error}") api_instance.handle_external_task_bpmn_error( self.FETCH_RESPONSE.id, external_task_bpmn_error=bpmn_error) self.drop_fetch_response() except ApiException as e: raise ApiException( f"Exception when calling ExternalTaskApi->handle_external_task_bpmn_error: {e}\n" )
def deliver_message(self, message_name, **kwargs): """ Delivers a message using Camunda REST API: https://docs.camunda.org/manual/latest/reference/rest/message/post-message/ Example: | ${result} | deliver message | msg_payment_received | | ${result} | deliver message | msg_payment_received | process_variables = ${variable_dictionary} | | ${result} | deliver message | msg_payment_received | business_key = ${correlating_business_key} | """ with self._shared_resources.api_client as api_client: correlation_message: CorrelationMessageDto = CorrelationMessageDto( **kwargs) correlation_message.message_name = message_name if not 'result_enabled' in kwargs: correlation_message.result_enabled = True if 'process_variables' in kwargs: correlation_message.process_variables = CamundaResources.dict_to_camunda_json( kwargs['process_variables']) serialized_message = api_client.sanitize_for_serialization( correlation_message) logger.debug(f'Message:\n{serialized_message}') headers = self._shared_resources.api_client.default_headers.copy() headers['Content-Type'] = 'application/json' try: response = requests.post( f'{self._shared_resources.camunda_url}/message', json=serialized_message, headers=headers) except ApiException as e: raise ApiException(f'Failed to deliver message:\n{e}') try: response.raise_for_status() except HTTPError as e: logger.error(e) raise ApiException(response.text) if correlation_message.result_enabled: json = response.json() logger.debug(json) return json else: return {}
def complete_task(self, result_set: Dict[str, Any] = None, files: Dict = None): """ Completes the task that was fetched before with `fetch workload`. *Requires `fetch workload` to run before this one, logs warning instead.* Additional variables can be provided as dictionary in _result_set_ . Files can be provided as dictionary of filename and patch. Examples: | _# fetch and immediately complete_ | | | *fetch workload* | _my_topic_ | | | *complete task* | | | | | _# fetch and complete with return values_ | | | *fetch workload* | _decide_on_dish_ | | ${new_variables} | *Create Dictionary* | _my_dish=salad_ | | | *complete task* | _result_set=${new_variables}_ | | | | _# fetch and complete with return values and files_ | | | *fetch workload* | _decide_on_haircut_ | | ${return_values} | *Create Dictionary* | _style=short hair_ | | ${files} | *Create Dictionary* | _should_look_like=~/favorites/beckham.jpg_ | | | *complete task* | _${return_values}_ | _${files}_ | """ if not self.FETCH_RESPONSE: logger.warn( 'No task to complete. Maybe you did not fetch and lock a workitem before?' ) else: with self._shared_resources.api_client as api_client: api_instance = openapi_client.ExternalTaskApi(api_client) variables = CamundaResources.convert_dict_to_openapi_variables( result_set) openapi_files = CamundaResources.convert_file_dict_to_openapi_variables( files) variables.update(openapi_files) complete_task_dto = openapi_client.CompleteExternalTaskDto( worker_id=self.WORKER_ID, variables=variables) try: logger.debug( f"Sending to Camunda for completing Task:\n{complete_task_dto}" ) api_instance.complete_external_task_resource( self.FETCH_RESPONSE.id, complete_external_task_dto=complete_task_dto) self.drop_fetch_response() except ApiException as e: raise ApiException( f"Exception when calling ExternalTaskApi->complete_external_task_resource: {e}\n" )
def delete_process_instance(self, process_instance_id): """ USE WITH CARE: Deletes a process instance by id. All data in this process instance will be lost. """ with self._shared_resources.api_client as api_client: api_instance = openapi_client.ProcessInstanceApi(api_client) try: response = api_instance.delete_process_instance( id=process_instance_id) except ApiException as e: raise ApiException( f'Failed to delete process instance {process_instance_id}:\n{e}' )
def get_amount_of_workloads(self, topic: str, **kwargs) -> int: """ Retrieves count of tasks. By default expects a topic name, but all parameters from the original endpoint may be provided: https://docs.camunda.org/manual/latest/reference/rest/external-task/get-query-count/ """ with self._shared_resources.api_client as api_client: api_instance = openapi_client.ExternalTaskApi(api_client) try: response: CountResultDto = api_instance.get_external_tasks_count( topic_name=topic, **kwargs) except ApiException as e: raise ApiException( f'Failed to count workload for topic "{topic}":\n{e}') logger.info(f'Amount of workloads for "{topic}":\t{response.count}') return response.count
def unlock(self): """ Unlocks recent task. """ if not self.FETCH_RESPONSE: logger.warn( 'No task to unlock. Maybe you did not fetch and lock a workitem before?' ) else: with self._shared_resources.api_client as api_client: api_instance = openapi_client.ExternalTaskApi(api_client) try: api_instance.unlock(self.FETCH_RESPONSE.id) self.drop_fetch_response() except ApiException as e: raise ApiException( f"Exception when calling ExternalTaskApi->unlock: {e}\n" )
def get_process_definitions(self, **kwargs): """ Returns a list of process definitions that fulfill given parameters. See Rest API documentation on ``https://docs.camunda.org/manual`` for available parameters. == Example == | ${list} | Get Process Definitions | name=my_process_definition | """ with self._shared_resources.api_client as api_client: api_instance: ProcessDefinitionApi = openapi_client.ProcessDefinitionApi( api_client) try: response = api_instance.get_process_definitions(**kwargs) except ApiException as e: raise ApiException(f'Failed to get process definitions:\n{e}') return response
def notify_failure(self, **kwargs): """ Raises a failure to Camunda. When retry counter is less than 1, an incident is created by Camunda. You can specify number of retries with the *retries* argument. If current fetched process instance already has *retries* parameter set, the *retries* argument of this keyword is ignored. Instead, the retries counter will be decreased by 1. CamundaLibrary takes care of providing the worker_id and task_id. *retry_timeout* is equal to *lock_duration* for external tasks. Check for camunda client documentation for all parameters of the request body: https://noordsestern.gitlab.io/camunda-client-for-python/7-15-0/docs/ExternalTaskApi.html#handle_failure Example: | *notify failure* | | | | *notify failure* | retries=3 | error_message=Task failed due to... | """ current_process_instance = self.FETCH_RESPONSE if not current_process_instance: logger.warn( 'No task to notify failure for. Maybe you did not fetch and lock a workitem before?' ) else: with self._shared_resources.api_client as api_client: api_instance = openapi_client.ExternalTaskApi(api_client) if 'retry_timeout' not in kwargs or None is kwargs[ 'retry_timeout'] or not kwargs['retry_timeout']: kwargs['retry_timeout'] = self.DEFAULT_LOCK_DURATION if None is not current_process_instance.retries: kwargs['retries'] = current_process_instance.retries - 1 external_task_failure_dto = ExternalTaskFailureDto( worker_id=self.WORKER_ID, **kwargs) try: api_instance.handle_failure( id=current_process_instance.id, external_task_failure_dto=external_task_failure_dto) self.drop_fetch_response() except ApiException as e: raise ApiException( "Exception when calling ExternalTaskApi->handle_failure: %s\n" % e)
def get_incidents(self, **kwargs): """ Retrieves incidents matching given filter arguments. For full parameter list checkout: https://noordsestern.gitlab.io/camunda-client-for-python/7-15-0/docs/IncidentApi.html#get_incidents Example: | ${all_incidents} | *get incidents* | | | ${incidents_of_process_instance | *get incidentse* | process_instance_id=${process_instance}[process_instance_id] | """ with self._shared_resources.api_client as api_client: api_instance: IncidentApi = openapi_client.IncidentApi(api_client) try: response: List[IncidentDto] = api_instance.get_incidents( **kwargs) except ApiException as e: raise ApiException(f'Failed to get incidents:\n{e}') return [incident.to_dict() for incident in response]
def get_activity_instance(self, id: str): """ Returns an Activity Instance (Tree) for a given process instance. == Example == | ${tree} | Get Activity Instance | id=fcab43bc-b970-11eb-be75-0242ac110002 | https://docs.camunda.org/manual/7.5/reference/rest/process-instance/get-activity-instances/ """ with self._shared_resources.api_client as api_client: api_instance: ProcessInstanceApi = openapi_client.ProcessInstanceApi( api_client) try: response: ActivityInstanceDto = api_instance.get_activity_instance_tree( id) except ApiException as e: raise ApiException( f'failed to get activity tree for process instance with id {id}:\n{e}' ) return response.to_dict()
def deploy(self, *args): """Creates a deployment from all given files and uploads them to camunda. Return response from camunda rest api as dictionary. Further documentation: https://docs.camunda.org/manual/latest/reference/rest/deployment/post-deployment/ By default, this keyword only deploys changed models and filters duplicates. Deployment name is the filename of the first file. Example: | ${response} | *Deploy model from file* | _../bpmn/my_model.bpnm_ | _../forms/my_forms.html_ | """ if not args: raise ValueError( 'Failed deploying model, because no file provided.') if len(args) > 1: # We have to use plain REST then when uploading more than 1 file. return self.deploy_multiple_files(*args) filename = os.path.basename(args[0]) with self._shared_resources.api_client as api_client: api_instance = openapi_client.DeploymentApi(api_client) data = [*args] deployment_name = filename try: response: DeploymentWithDefinitionsDto = api_instance.create_deployment( deploy_changed_only=True, enable_duplicate_filtering=True, deployment_name=deployment_name, data=data) logger.info(f'Response from camunda:\t{response}') except ApiException as e: raise ApiException(f'Failed to upload {filename}:\n{e}') return response.to_dict()
def evaluate_decision(self, key: str, variables: dict) -> list: """ Evaluates a given decision and returns the result. The input values of the decision have to be supplied with `variables`. == Example == | ${variables} | Create Dictionary | my_input=42 | | ${response} | Evaluate Decision | my_decision_table | ${variables} | """ with self._shared_resources.api_client as api_client: api_instance = openapi_client.DecisionDefinitionApi(api_client) dto = CamundaResources.convert_dict_to_openapi_variables(variables) try: response = api_instance.evaluate_decision_by_key( key=key, evaluate_decision_dto=openapi_client.EvaluateDecisionDto( dto)) return [ CamundaResources.convert_openapi_variables_to_dict(r) for r in response ] except ApiException as e: raise ApiException(f'Failed to evaluate decision {key}:\n{e}')
def get_deployments(self, deployment_id: str = None, **kwargs): """ Retrieves all deployments that match given criteria. All parameters are available from https://docs.camunda.org/manual/latest/reference/rest/deployment/get-query/ Example: | ${list_of_deployments} | get deployments | ${my_deployments_id} | | ${list_of_deployments} | get deployments | id=${my_deployments_id} | | ${list_of_deployments} | get deployments | after=2013-01-23T14:42:45.000+0200 | """ if deployment_id: kwargs['id'] = deployment_id with self._shared_resources.api_client as api_client: api_instance = openapi_client.DeploymentApi(api_client) try: response: List[DeploymentDto] = api_instance.get_deployments( **kwargs) logger.info(f'Response from camunda:\t{response}') except ApiException as e: raise ApiException(f'Failed get deployments:\n{e}') return [r.to_dict() for r in response]
def get_process_instance_variable(self, process_instance_id: str, variable_name: str, auto_type_conversion: bool = True): """ Returns the variable with the given name from the process instance with the given process_instance_id. Parameters: - ``process_instance_id``: ID of the target process instance - ``variable_name``: name of the variable to read - ``auto_type_conversion``: Converts JSON structures automatically in to python data structures. Default: True. When False, values are not retrieved for JSON variables, but metadata is. Only useful when you want to verify that Camunda holds certain data types.% == Example == | ${variable} | Get Process Instance Variable | | ... | process_instance_id=fcab43bc-b970-11eb-be75-0242ac110002 | | ... | variable_name=foo | See also: https://docs.camunda.org/manual/latest/reference/rest/process-instance/variables/get-single-variable/ """ with self._shared_resources.api_client as api_client: api_instance: ProcessInstanceApi = openapi_client.ProcessInstanceApi( api_client) try: response = api_instance.get_process_instance_variable( id=process_instance_id, var_name=variable_name, deserialize_value=not auto_type_conversion) except ApiException as e: raise ApiException( f'Failed to get variable {variable_name} from ' f'process instance {process_instance_id}:\n{e}') if auto_type_conversion: return CamundaResources.convert_variable_dto(response) return response
def get_process_instances(self, **kwargs): """ Queries Camunda for process instances that match certain criteria. *Be aware, that boolean value must be strings either 'true' or 'false'* == Arguments == | async_req | execute request asynchronously | | sort_by | Sort the results lexicographically by a given criterion. Must be used in conjunction with the sortOrder parameter. | | sort_order | Sort the results in a given order. Values may be asc for ascending order or desc for descending order. Must be used in conjunction with the sortBy parameter. | | first_result | Pagination of results. Specifies the index of the first result to return. | | max_results | Pagination of results. Specifies the maximum number of results to return. Will return less results if there are no more results left. | | process_instance_ids | Filter by a comma-separated list of process instance ids. | | business_key | Filter by process instance business key. | | business_key_like | Filter by process instance business key that the parameter is a substring of. | | case_instance_id | Filter by case instance id. | | process_definition_id | Filter by the deployment the id belongs to. | | process_definition_key | Filter by the key of the process definition the instances run on. | | process_definition_key_in | Filter by a comma-separated list of process definition keys. A process instance must have one of the given process definition keys. | | process_definition_key_not_in | Exclude instances by a comma-separated list of process definition keys. A process instance must not have one of the given process definition keys. | | deployment_id | Filter by the deployment the id belongs to. | | super_process_instance | Restrict query to all process instances that are sub process instances of the given process instance. Takes a process instance id. | | sub_process_instance | Restrict query to all process instances that have the given process instance as a sub process instance. Takes a process instance id. | | super_case_instance | Restrict query to all process instances that are sub process instances of the given case instance. Takes a case instance id. | | sub_case_instance | Restrict query to all process instances that have the given case instance as a sub case instance. Takes a case instance id. | | active | Only include active process instances. Value may only be true, as false is the default behavior. | | suspended | Only include suspended process instances. Value may only be true, as false is the default behavior. | | with_incident | Filter by presence of incidents. Selects only process instances that have an incident. | | incident_id | Filter by the incident id. | | incident_type | Filter by the incident type. See the [User Guide](https://docs.camunda.org/manual/latest/user-guide/process-engine/incidents/#incident-types) for a list of incident types. | | incident_message | Filter by the incident message. Exact match. | | incident_message_like | Filter by the incident message that the parameter is a substring of. | | tenant_id_in | Filter by a comma-separated list of tenant ids. A process instance must have one of the given tenant ids. | | without_tenant_id | Only include process instances which belong to no tenant. | | process_definition_without_tenant_id | Only include process instances which process definition has no tenant id. | | activity_id_in | Filter by a comma-separated list of activity ids. A process instance must currently wait in a leaf activity with one of the given activity ids. | | root_process_instances | Restrict the query to all process instances that are top level process instances. | | leaf_process_instances | Restrict the query to all process instances that are leaf instances. (i.e. don't have any sub instances). | | variables | Only include process instances that have variables with certain values. Variable filtering expressions are comma-separated and are structured as follows: A valid parameter value has the form `key_operator_value`. `key` is the variable name, `operator` is the comparison operator to be used and `value` the variable value. **Note**: Values are always treated as String objects on server side. Valid `operator` values are: `eq` - equal to; `neq` - not equal to; `gt` - greater than; `gteq` - greater than or equal to; `lt` - lower than; `lteq` - lower than or equal to; `like`. `key` and `value` may not contain underscore or comma characters. | | variable_names_ignore_case | Match all variable names in this query case-insensitively. If set to true variableName and variablename are treated as equal. | | variable_values_ignore_case | Match all variable values in this query case-insensitively. If set to true variableValue and variablevalue are treated as equal. | | _preload_content | if False, the urllib3.HTTPResponse object will be returned without reading/decoding response data. Default is True. | | _request_timeout | timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of (connection, read) timeouts. | """ with self._shared_resources.api_client as api_client: api_instance: ProcessInstanceApi = openapi_client.ProcessInstanceApi( api_client) try: response: List[ ProcessInstanceDto] = api_instance.get_process_instances( **kwargs) except ApiException as e: raise ApiException( f'Failed to get process instances of process:\n{e}') return [process_instance.to_dict() for process_instance in response]
def start_process_instance(self, process_key: str, variables: Dict = None, files: Dict = None, before_activity_id: str = None, after_activity_id: str = None, **kwargs) -> Dict: """ Starts a new process instance from a process definition with given key. variables: _optional_ dictionary like: {'variable name' : 'value'} files: _optional_ dictionary like: {'variable name' : path}. will be attached to variables in Camunda before_activity_id: _optional_ id of activity at which the process starts before. *CANNOT BE USED TOGETHER WITH _after_activity_id_* after_activity_id: _optional_ id of activity at which the process starts after. *CANNOT BE USED TOGETHER WITH _before_activity_id_* Returns response from Camunda as dictionary == Examples == | `start process` | apply for job promotion | _variables_= { 'employee' : 'John Doe', 'permission_for_application_granted' : True} | _files_ = { 'cv' : 'documents/my_life.md'} | _after_activity_id_ = 'Activity_ask_boss_for_persmission' | | `start process` | apply for promotion | business_key=John again | """ if not process_key: raise ValueError( 'Error starting process. No process key provided.') if before_activity_id and after_activity_id: raise AssertionError( '2 activity ids provided. Cannot start before and after an activity.' ) with self._shared_resources.api_client as api_client: api_instance: ProcessDefinitionApi = openapi_client.ProcessDefinitionApi( api_client) openapi_variables = CamundaResources.convert_dict_to_openapi_variables( variables) openapi_files = CamundaResources.convert_file_dict_to_openapi_variables( files) openapi_variables.update(openapi_files) if before_activity_id or after_activity_id: instruction: ProcessInstanceModificationInstructionDto = ProcessInstanceModificationInstructionDto( type='startBeforeActivity' if before_activity_id else 'startAfterActivity', activity_id=before_activity_id if before_activity_id else after_activity_id, ) kwargs.update(start_instructions=[instruction]) start_process_instance_dto: StartProcessInstanceDto = StartProcessInstanceDto( variables=openapi_variables, **kwargs) try: response: ProcessInstanceWithVariablesDto = api_instance.start_process_instance_by_key( key=process_key, start_process_instance_dto=start_process_instance_dto) except ApiException as e: raise ApiException( f'Failed to start process {process_key}:\n{e}') logger.info(f'Response:\n{response}') return response.to_dict()
def fetch_workload(self, topic: str, async_response_timeout=None, use_priority=None, **kwargs) -> Dict: """ Locks and fetches workloads from camunda on a given topic. Returns a list of variable dictionary. Each dictionary representing 1 workload from a process instance. If a process instance was fetched, the process instance is cached and can be retrieved by keyword `Get Fetch Response` The only mandatory parameter for this keyword is *topic* which is the name of the topic to fetch workload from. More parameters can be added from the Camunda documentation: https://docs.camunda.org/manual/latest/reference/rest/external-task/fetch/ If not provided, this keyword will use a lock_duration of 60000 ms (10 minutes) and set {{deserialize_value=True}} Examples: | ${input_variables} | *Create Dictionary* | _name=Robot_ | | | *start process* | _my_demo_ | _${input_variables}_ | | ${variables} | *fetch workload* | _first_task_in_demo_ | | | *Dictionary Should Contain Key* | _${variables}_ | _name_ | | | *Should Be Equal As String* | _Robot_ | _${variables}[name]_ | Example deserializing only some variables: | ${input_variables} | *Create Dictionary* | _name=Robot_ | _profession=Framework_ | | | *start process* | _my_demo_ | _${input_variables}_ | | ${variables_of_interest} | *Create List* | _profession_ | | ${variables} | *Fetch Workload* | _first_task_in_demo_ | _variables=${variables_of_interest}_ | | | *Dictionary Should Not Contain Key* | _${variables}_ | _name_ | | | *Dictionary Should Contain Key* | _${variables}_ | _profession_ | | | *Should Be Equal As String* | _Framework_ | _${variables}[profession]_ | """ api_response = [] with self._shared_resources.api_client as api_client: # Create an instance of the API class api_instance = openapi_client.ExternalTaskApi(api_client) if 'lock_duration' not in kwargs: kwargs['lock_duration'] = self.DEFAULT_LOCK_DURATION if 'deserialize_values' not in kwargs: kwargs['deserialize_values'] = False topic_dto = FetchExternalTaskTopicDto(topic_name=topic, **kwargs) fetch_external_tasks_dto = FetchExternalTasksDto( worker_id=self.WORKER_ID, max_tasks=1, async_response_timeout=async_response_timeout, use_priority=use_priority, topics=[topic_dto]) try: api_response = api_instance.fetch_and_lock( fetch_external_tasks_dto=fetch_external_tasks_dto) logger.info(api_response) except ApiException as e: raise ApiException( "Exception when calling ExternalTaskApi->fetch_and_lock: %s\n" % e) work_items: List[LockedExternalTaskDto] = api_response if work_items: logger.debug( f'Received {len(work_items)} work_items from camunda engine for topic:\t{topic}' ) else: logger.debug( f'Received no work items from camunda engine for topic:\t{topic}' ) if not work_items: return {} self.FETCH_RESPONSE = work_items[0] variables: Dict[str, VariableValueDto] = self.FETCH_RESPONSE.variables return CamundaResources.convert_openapi_variables_to_dict(variables)