You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by le...@apache.org on 2021/12/27 02:32:52 UTC
[dolphinscheduler] branch 2.0.2-prepare updated: [cherry-pick][python] Make dolphinscheduler python API works to 2.0.2 (#7608)
This is an automated email from the ASF dual-hosted git repository.
leonbao pushed a commit to branch 2.0.2-prepare
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/2.0.2-prepare by this push:
new 9e068d2 [cherry-pick][python] Make dolphinscheduler python API works to 2.0.2 (#7608)
9e068d2 is described below
commit 9e068d23e2743b322921febccd0b02d88725319a
Author: Jiajie Zhong <zh...@hotmail.com>
AuthorDate: Mon Dec 27 10:32:40 2021 +0800
[cherry-pick][python] Make dolphinscheduler python API works to 2.0.2 (#7608)
* [cherry-pick][python] Make it work to 2.0.2
* Remove unused ProcessExecutionTypeEnum
* Add queryByName to project
* Add checkTenantExists to tenant
* Add queryByTenantCode to tenant
* Add queryQueueName to queue
* Add all content from dev branch
* Add gitignore
* Add pydolphinscheduler content
* Add ds-py to bin test
* Py merge to 202
* Fix version
* Fix missing variable
* Add py4j as known deps
* Fix core database bug
---
.gitignore | 14 +
.../dolphinscheduler/supervisor/supervisor.ini | 15 +
docker/build/startup.sh | 19 +-
.../api/service/ProjectService.java | 9 +
.../dolphinscheduler/api/service/QueueService.java | 8 +
.../api/service/TenantService.java | 16 +
.../api/service/impl/ProjectServiceImpl.java | 15 +
.../api/service/impl/QueueServiceImpl.java | 26 +
.../api/service/impl/TenantServiceImpl.java | 19 +-
.../dolphinscheduler/dao/mapper/QueueMapper.java | 7 +
.../dolphinscheduler/dao/mapper/QueueMapper.xml | 9 +
dolphinscheduler-dist/pom.xml | 5 +
.../src/main/assembly/dolphinscheduler-bin.xml | 9 +
.../pom.xml | 40 +-
.../pydolphinscheduler/.coveragerc | 32 +
dolphinscheduler-python/pydolphinscheduler/.flake8 | 37 +
.../pydolphinscheduler/.isort.cfg | 19 +
.../pydolphinscheduler/README.md | 173 +++++
.../pydolphinscheduler/ROADMAP.md | 34 +
.../pydolphinscheduler/examples/bulk_create.py | 55 ++
.../examples/task_conditions_example.py | 55 ++
.../examples/task_datax_example.py | 50 ++
.../examples/task_dependent_example.py | 72 ++
.../examples/task_switch_example.py | 50 ++
.../pydolphinscheduler/examples/tutorial.py | 52 ++
.../pydolphinscheduler/pytest.ini | 22 +
.../pydolphinscheduler/requirements.txt | 18 +
.../pydolphinscheduler/requirements_dev.txt | 27 +
.../pydolphinscheduler/setup.cfg | 16 +
.../pydolphinscheduler/setup.py | 94 +++
.../src/pydolphinscheduler/__init__.py | 18 +
.../src/pydolphinscheduler/constants.py | 122 ++++
.../src/pydolphinscheduler/core/__init__.py | 18 +
.../src/pydolphinscheduler/core/base.py | 74 ++
.../src/pydolphinscheduler/core/base_side.py | 40 ++
.../src/pydolphinscheduler/core/database.py | 64 ++
.../pydolphinscheduler/core/process_definition.py | 360 ++++++++++
.../src/pydolphinscheduler/core/task.py | 268 +++++++
.../src/pydolphinscheduler/exceptions.py | 46 ++
.../src/pydolphinscheduler/java_gateway.py | 55 ++
.../src/pydolphinscheduler/side/__init__.py | 22 +
.../src/pydolphinscheduler/side/project.py | 42 ++
.../src/pydolphinscheduler/side/queue.py | 42 ++
.../src/pydolphinscheduler/side/tenant.py | 45 ++
.../src/pydolphinscheduler/side/user.py | 70 ++
.../src/pydolphinscheduler/side/worker_group.py | 30 +
.../src/pydolphinscheduler/tasks/__init__.py | 18 +
.../src/pydolphinscheduler/tasks/condition.py | 185 +++++
.../src/pydolphinscheduler/tasks/datax.py | 121 ++++
.../src/pydolphinscheduler/tasks/dependent.py | 274 +++++++
.../src/pydolphinscheduler/tasks/http.py | 101 +++
.../src/pydolphinscheduler/tasks/procedure.py | 60 ++
.../src/pydolphinscheduler/tasks/python.py | 51 ++
.../src/pydolphinscheduler/tasks/shell.py | 38 +
.../src/pydolphinscheduler/tasks/sql.py | 99 +++
.../src/pydolphinscheduler/tasks/sub_process.py | 55 ++
.../src/pydolphinscheduler/tasks/switch.py | 158 ++++
.../src/pydolphinscheduler/utils/__init__.py | 18 +
.../src/pydolphinscheduler/utils/date.py | 82 +++
.../src/pydolphinscheduler/utils/string.py | 39 +
.../pydolphinscheduler/tests/__init__.py | 18 +
.../pydolphinscheduler/tests/core/__init__.py | 18 +
.../pydolphinscheduler/tests/core/test_database.py | 54 ++
.../tests/core/test_process_definition.py | 342 +++++++++
.../pydolphinscheduler/tests/core/test_task.py | 224 ++++++
.../pydolphinscheduler/tests/tasks/__init__.py | 18 +
.../tests/tasks/test_condition.py | 439 ++++++++++++
.../pydolphinscheduler/tests/tasks/test_datax.py | 124 ++++
.../tests/tasks/test_dependent.py | 793 +++++++++++++++++++++
.../pydolphinscheduler/tests/tasks/test_http.py | 144 ++++
.../tests/tasks/test_procedure.py | 106 +++
.../pydolphinscheduler/tests/tasks/test_python.py | 122 ++++
.../pydolphinscheduler/tests/tasks/test_shell.py | 89 +++
.../pydolphinscheduler/tests/tasks/test_sql.py | 149 ++++
.../tests/tasks/test_sub_process.py | 114 +++
.../pydolphinscheduler/tests/tasks/test_switch.py | 300 ++++++++
.../pydolphinscheduler/tests/test_java_gateway.py | 52 ++
.../pydolphinscheduler/tests/testing/__init__.py | 18 +
.../pydolphinscheduler/tests/testing/task.py | 32 +
.../pydolphinscheduler/tests/utils/__init__.py | 18 +
.../pydolphinscheduler/tests/utils/test_date.py | 78 ++
.../pydolphinscheduler/tests/utils/test_string.py | 87 +++
.../server/PythonGatewayServer.java | 161 ++++-
.../src/main/resources/config/install_config.conf | 5 +
dolphinscheduler-standalone-server/pom.xml | 4 +
.../dolphinscheduler/server/StandaloneServer.java | 2 +-
pom.xml | 1 +
script/dolphinscheduler-daemon.sh | 5 +-
script/start-all.sh | 7 +
script/status-all.sh | 8 +
script/stop-all.sh | 7 +
tools/dependencies/known-dependencies.txt | 1 +
92 files changed, 7116 insertions(+), 56 deletions(-)
diff --git a/.gitignore b/.gitignore
index 9011db1..12a51b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,17 @@ dolphinscheduler-server/src/main/resources/logback.xml
dolphinscheduler-ui/dist
dolphinscheduler-ui/node
docker/build/apache-dolphinscheduler*
+
+# ------------------
+# pydolphinscheduler
+# ------------------
+# Cache
+__pycache__/
+
+# Build
+build/
+*egg-info/
+
+# Test coverage
+.coverage
+htmlcov/
diff --git a/docker/build/conf/dolphinscheduler/supervisor/supervisor.ini b/docker/build/conf/dolphinscheduler/supervisor/supervisor.ini
index 19166f4..e5314c1 100644
--- a/docker/build/conf/dolphinscheduler/supervisor/supervisor.ini
+++ b/docker/build/conf/dolphinscheduler/supervisor/supervisor.ini
@@ -105,3 +105,18 @@ killasgroup=true
redirect_stderr=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
+
+[program:python-gateway]
+command=%(ENV_DOLPHINSCHEDULER_BIN)s/dolphinscheduler-daemon.sh start python-gateway
+directory=%(ENV_DOLPHINSCHEDULER_HOME)s
+priority=999
+autostart=%(ENV_STANDALONE_START_ENABLED)s
+autorestart=true
+startsecs=5
+stopwaitsecs=3
+exitcodes=0
+stopasgroup=true
+killasgroup=true
+redirect_stderr=true
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0
diff --git a/docker/build/startup.sh b/docker/build/startup.sh
index 7f3b7d0..83f9ec2 100755
--- a/docker/build/startup.sh
+++ b/docker/build/startup.sh
@@ -25,6 +25,7 @@ export API_START_ENABLED=false
export ALERT_START_ENABLED=false
export LOGGER_START_ENABLED=false
export STANDALONE_START_ENABLED=false
+export PYTHON_GATEWAY_START_ENABLED=false
# wait database
waitDatabase() {
@@ -68,13 +69,14 @@ waitZK() {
printUsage() {
echo -e "Dolphin Scheduler is a distributed and easy-to-expand visual DAG workflow scheduling system,"
echo -e "dedicated to solving the complex dependencies in data processing, making the scheduling system out of the box for data processing.\n"
- echo -e "Usage: [ all | master-server | worker-server | api-server | alert-server | standalone-server ]\n"
- printf "%-13s: %s\n" "all" "Run master-server, worker-server, api-server and alert-server"
- printf "%-13s: %s\n" "master-server" "MasterServer is mainly responsible for DAG task split, task submission monitoring."
- printf "%-13s: %s\n" "worker-server" "WorkerServer is mainly responsible for task execution and providing log services."
- printf "%-13s: %s\n" "api-server" "ApiServer is mainly responsible for processing requests and providing the front-end UI layer."
- printf "%-13s: %s\n" "alert-server" "AlertServer mainly include Alarms."
- printf "%-13s: %s\n" "standalone-server" "Standalone server that uses embedded zookeeper and database, only for testing and demostration."
+ echo -e "Usage: [ all | master-server | worker-server | api-server | alert-server | standalone-server | python-gateway-server]\n"
+ printf "%-13s: %s\n" "all" "Run master-server, worker-server, api-server, alert-server, python-gateway-server"
+ printf "%-13s: %s\n" "master-server" "MasterServer is mainly responsible for DAG task split, task submission monitoring."
+ printf "%-13s: %s\n" "worker-server" "WorkerServer is mainly responsible for task execution and providing log services."
+ printf "%-13s: %s\n" "api-server" "ApiServer is mainly responsible for processing requests and providing the front-end UI layer."
+ printf "%-13s: %s\n" "alert-server" "AlertServer mainly include Alarms."
+ printf "%-13s: %s\n" "standalone-server" "Standalone server that uses embedded zookeeper and database, only for testing and demostration."
+ printf "%-13s: %s\n" "python-gateway-server" "Python gateway is a backend server for python API."
}
# init config file
@@ -115,6 +117,9 @@ case "$1" in
(standalone-server)
export STANDALONE_START_ENABLED=true
;;
+ (python-gateway-server)
+ export PYTHON_GATEWAY_START_ENABLED=true
+ ;;
(help)
printUsage
exit 1
diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ProjectService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ProjectService.java
index 1300834..38c0c53 100644
--- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ProjectService.java
+++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ProjectService.java
@@ -47,6 +47,15 @@ public interface ProjectService {
Map<String, Object> queryByCode(User loginUser, long projectCode);
/**
+ * query project details by name
+ *
+ * @param loginUser login user
+ * @param projectName project name
+ * @return project detail information
+ */
+ Map<String, Object> queryByName(User loginUser, String projectName);
+
+ /**
* check project and authorization
*
* @param loginUser login user
diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/QueueService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/QueueService.java
index 7013520..f978b96 100644
--- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/QueueService.java
+++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/QueueService.java
@@ -76,4 +76,12 @@ public interface QueueService {
*/
Result<Object> verifyQueue(String queue, String queueName);
+ /**
+ * query queue by queueName
+ *
+ * @param queueName queue name
+ * @return queue object for provide queue name
+ */
+ Map<String, Object> queryQueueName(String queueName);
+
}
diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TenantService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TenantService.java
index 30d98a1..47a4082 100644
--- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TenantService.java
+++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TenantService.java
@@ -92,4 +92,20 @@ public interface TenantService {
* @return true if tenant code can user, otherwise return false
*/
Result verifyTenantCode(String tenantCode);
+
+ /**
+ * check if provide tenant code object exists
+ *
+ * @param tenantCode tenant code
+ * @return true if tenant code exists, false if not
+ */
+ boolean checkTenantExists(String tenantCode);
+
+ /**
+ * query tenant by tenant code
+ *
+ * @param tenantCode tenant code
+ * @return tenant list
+ */
+ Map<String, Object> queryByTenantCode(String tenantCode);
}
diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java
index 3bb222b..3579cd8 100644
--- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java
+++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java
@@ -139,6 +139,21 @@ public class ProjectServiceImpl extends BaseServiceImpl implements ProjectServic
return result;
}
+ @Override
+ public Map<String, Object> queryByName(User loginUser, String projectName) {
+ Map<String, Object> result = new HashMap<>();
+ Project project = projectMapper.queryByName(projectName);
+ boolean hasProjectAndPerm = hasProjectAndPerm(loginUser, project, result);
+ if (!hasProjectAndPerm) {
+ return result;
+ }
+ if (project != null) {
+ result.put(Constants.DATA_LIST, project);
+ putMsg(result, Status.SUCCESS);
+ }
+ return result;
+ }
+
/**
* check project and authorization
*
diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/QueueServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/QueueServiceImpl.java
index 91889cd..8169541 100644
--- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/QueueServiceImpl.java
+++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/QueueServiceImpl.java
@@ -264,6 +264,32 @@ public class QueueServiceImpl extends BaseServiceImpl implements QueueService {
}
/**
+ * query queue by queueName
+ *
+ * @param queueName queue name
+ * @return queue object for provide queue name
+ */
+ @Override
+ public Map<String, Object> queryQueueName(String queueName) {
+ Map<String, Object> result = new HashMap<>();
+
+ if (StringUtils.isEmpty(queueName)) {
+ putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.QUEUE_NAME);
+ return result;
+ }
+
+ if (!checkQueueNameExist(queueName)) {
+ putMsg(result, Status.QUEUE_NOT_EXIST, queueName);
+ return result;
+ }
+
+ List<Queue> queueList = queueMapper.queryQueueName(queueName);
+ result.put(Constants.DATA_LIST, queueList);
+ putMsg(result, Status.SUCCESS);
+ return result;
+ }
+
+ /**
* check queue exist
* if exists return true,not exists return false
* check queue exist
diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TenantServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TenantServiceImpl.java
index c1295c1..7c90bcf 100644
--- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TenantServiceImpl.java
+++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TenantServiceImpl.java
@@ -318,8 +318,25 @@ public class TenantServiceImpl extends BaseServiceImpl implements TenantService
* @param tenantCode tenant code
* @return ture if the tenant code exists, otherwise return false
*/
- private boolean checkTenantExists(String tenantCode) {
+ public boolean checkTenantExists(String tenantCode) {
Boolean existTenant = tenantMapper.existTenant(tenantCode);
return existTenant == Boolean.TRUE;
}
+
+ /**
+ * query tenant by tenant code
+ *
+ * @param tenantCode tenant code
+ * @return tenant detail information
+ */
+ @Override
+ public Map<String, Object> queryByTenantCode(String tenantCode) {
+ Map<String, Object> result = new HashMap<>();
+ Tenant tenant = tenantMapper.queryByTenantCode(tenantCode);
+ if (tenant != null) {
+ result.put(Constants.DATA_LIST, tenant);
+ putMsg(result, Status.SUCCESS);
+ }
+ return result;
+ }
}
diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/QueueMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/QueueMapper.java
index 027bfd2..e486070 100644
--- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/QueueMapper.java
+++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/QueueMapper.java
@@ -53,4 +53,11 @@ public interface QueueMapper extends BaseMapper<Queue> {
* @return true if exist else return null
*/
Boolean existQueue(@Param("queue") String queue, @Param("queueName") String queueName);
+
+ /**
+ * query queue by queue name
+ * @param queueName queueName
+ * @return queue list
+ */
+ List<Queue> queryQueueName(@Param("queueName") String queueName);
}
diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/QueueMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/QueueMapper.xml
index 0d75c6e..cf381e4 100644
--- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/QueueMapper.xml
+++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/QueueMapper.xml
@@ -54,4 +54,13 @@
and queue_name =#{queueName}
</if>
</select>
+ <select id="queryQueueName" resultType="org.apache.dolphinscheduler.dao.entity.Queue">
+ select
+ <include refid="baseSql"/>
+ from t_ds_queue
+ where 1 = 1
+ <if test="queueName != null and queueName != ''">
+ and queue_name =#{queueName}
+ </if>
+ </select>
</mapper>
diff --git a/dolphinscheduler-dist/pom.xml b/dolphinscheduler-dist/pom.xml
index 63e4674..ee66995 100644
--- a/dolphinscheduler-dist/pom.xml
+++ b/dolphinscheduler-dist/pom.xml
@@ -51,6 +51,11 @@
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-alert-server</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-python</artifactId>
+ </dependency>
</dependencies>
<profiles>
diff --git a/dolphinscheduler-dist/src/main/assembly/dolphinscheduler-bin.xml b/dolphinscheduler-dist/src/main/assembly/dolphinscheduler-bin.xml
index bbc988b..8cc15fb 100644
--- a/dolphinscheduler-dist/src/main/assembly/dolphinscheduler-bin.xml
+++ b/dolphinscheduler-dist/src/main/assembly/dolphinscheduler-bin.xml
@@ -114,6 +114,15 @@
</fileSet>
<fileSet>
+ <directory>${basedir}/../dolphinscheduler-python/src/main/resources</directory>
+ <includes>
+ <include>**/*.yaml</include>
+ <include>**/*.xml</include>
+ </includes>
+ <outputDirectory>conf</outputDirectory>
+ </fileSet>
+
+ <fileSet>
<directory>${basedir}/../dolphinscheduler-dist/target/dolphinscheduler-dist-${project.version}</directory>
<includes>
<include>**/*.*</include>
diff --git a/dolphinscheduler-standalone-server/pom.xml b/dolphinscheduler-python/pom.xml
similarity index 64%
copy from dolphinscheduler-standalone-server/pom.xml
copy to dolphinscheduler-python/pom.xml
index 8382037..014f45a 100644
--- a/dolphinscheduler-standalone-server/pom.xml
+++ b/dolphinscheduler-python/pom.xml
@@ -15,40 +15,46 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
<parent>
- <artifactId>dolphinscheduler</artifactId>
<groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler</artifactId>
<version>2.0.2-SNAPSHOT</version>
</parent>
- <modelVersion>4.0.0</modelVersion>
-
- <artifactId>dolphinscheduler-standalone-server</artifactId>
+ <artifactId>dolphinscheduler-python</artifactId>
+ <name>${project.artifactId}</name>
+ <packaging>jar</packaging>
<dependencies>
- <dependency>
- <groupId>org.apache.dolphinscheduler</groupId>
- <artifactId>dolphinscheduler-server</artifactId>
- </dependency>
+ <!-- dolphinscheduler -->
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-api</artifactId>
</dependency>
+
+ <!--springboot-->
<dependency>
- <groupId>org.apache.curator</groupId>
- <artifactId>curator-test</artifactId>
- <version>${curator.test}</version>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
- <groupId>org.javassist</groupId>
- <artifactId>javassist</artifactId>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-tomcat</artifactId>
+ </exclusion>
+ <exclusion>
+ <artifactId>log4j-to-slf4j</artifactId>
+ <groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>
+
<dependency>
- <groupId>org.apache.dolphinscheduler</groupId>
- <artifactId>dolphinscheduler-alert-server</artifactId>
+ <groupId>net.sf.py4j</groupId>
+ <artifactId>py4j</artifactId>
</dependency>
- </dependencies>
+ </dependencies>
</project>
diff --git a/dolphinscheduler-python/pydolphinscheduler/.coveragerc b/dolphinscheduler-python/pydolphinscheduler/.coveragerc
new file mode 100644
index 0000000..524cb73
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/.coveragerc
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[run]
+command_line = -m pytest
+omit =
+ # Ignore all test cases in tests/
+ tests/*
+ # TODO. Temporary ignore java_gateway file, because we could not find good way to test it.
+ src/pydolphinscheduler/java_gateway.py
+
+[report]
+# Don’t report files that are 100% covered
+skip_covered = True
+show_missing = True
+precision = 2
+# Report will fail when coverage under 90.00%
+fail_under = 85
diff --git a/dolphinscheduler-python/pydolphinscheduler/.flake8 b/dolphinscheduler-python/pydolphinscheduler/.flake8
new file mode 100644
index 0000000..e676972
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/.flake8
@@ -0,0 +1,37 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[flake8]
+max-line-length = 110
+exclude =
+ .git,
+ __pycache__,
+ .pytest_cache,
+ *.egg-info,
+ docs/source/conf.py
+ old,
+ build,
+ dist,
+ htmlcov
+ignore =
+ # It's clear and not need to add docstring
+ D107, # D107: Don't require docstrings on __init__
+ D105, # D105: Missing docstring in magic method
+ # Conflict to Black
+ W503 # W503: Line breaks before binary operators
+per-file-ignores =
+ src/pydolphinscheduler/side/__init__.py:F401
diff --git a/dolphinscheduler-python/pydolphinscheduler/.isort.cfg b/dolphinscheduler-python/pydolphinscheduler/.isort.cfg
new file mode 100644
index 0000000..70fa2e0
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/.isort.cfg
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[settings]
+profile=black
diff --git a/dolphinscheduler-python/pydolphinscheduler/README.md b/dolphinscheduler-python/pydolphinscheduler/README.md
new file mode 100644
index 0000000..b06632c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/README.md
@@ -0,0 +1,173 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# pydolphinscheduler
+
+[![GitHub Build][ga-py-test]][ga]
+[![Code style: black][black-shield]][black-gh]
+[![Imports: isort][isort-shield]][isort-gh]
+
+pydolphinscheduler is python API for Apache DolphinScheduler, which allow you definition
+your workflow by python code, aka workflow-as-codes.
+
+## Quick Start
+
+> **_Notice:_** For now, due to pydolphinscheduler without release to any binary tarball or [PyPI][pypi], you
+> have to clone Apache DolphinScheduler code from GitHub to ensure quick start setup
+
+Here we show you how to install and run a simple example of pydolphinscheduler
+
+### Prepare
+
+```shell
+# Clone code from github
+git clone git@github.com:apache/dolphinscheduler.git
+
+# Install pydolphinscheduler from source
+cd dolphinscheduler-python/pydolphinscheduler
+pip install -e .
+```
+
+### Start Server And Run Example
+
+Before you run an example, you have to start backend server. You could follow [development setup][dev-setup]
+section "DolphinScheduler Standalone Quick Start" to set up developer environment. You have to start backend
+and frontend server in this step, which mean that you could view DolphinScheduler UI in your browser with URL
+http://localhost:12345/dolphinscheduler
+
+After backend server is being start, all requests from `pydolphinscheduler` would be sent to backend server.
+And for now we could run a simple example by:
+
+```shell
+cd dolphinscheduler-python/pydolphinscheduler
+python example/tutorial.py
+```
+
+> **_NOTICE:_** Since Apache DolphinScheduler's tenant is requests while running command, you might need to change
+> tenant value in `example/tutorial.py`. For now the value is `tenant_exists`, please change it to username exists
+> in you environment.
+
+After command execute, you could see a new project with single process definition named *tutorial* in the [UI][ui-project].
+
+Until now, we finish quick start by an example of pydolphinscheduler and run it. If you want to inspect or join
+pydolphinscheduler develop, you could take a look at [develop](#develop)
+
+## Develop
+
+pydolphinscheduler is python API for Apache DolphinScheduler, it just defines what workflow look like instead of
+store or execute it. We here use [py4j][py4j] to dynamically access Java Virtual Machine.
+
+### Setup Develop Environment
+
+We already clone the code in [quick start](#quick-start), so next step we have to open pydolphinscheduler project
+in you editor. We recommend you use [pycharm][pycharm] instead of [IntelliJ IDEA][idea] to open it. And you could
+just open directory `dolphinscheduler-python/pydolphinscheduler` instead of `dolphinscheduler-python`.
+
+Then you should add developer dependence to make sure you could run test and check code style locally
+
+```shell
+pip install -r requirements_dev.txt
+```
+
+### Brief Concept
+
+Apache DolphinScheduler is design to define workflow by UI, and pydolphinscheduler try to define it by code. When
+define by code, user usually do not care user, tenant, or queue exists or not. All user care about is created
+a new workflow by the code his/her definition. So we have some **side object** in `pydolphinscheduler/side`
+directory, their only check object exists or not, and create them if not exists.
+
+#### Process Definition
+
+pydolphinscheduler workflow object name, process definition is also same name as Java object(maybe would be change to
+other word for more simple).
+
+#### Tasks
+
+pydolphinscheduler tasks object, we use tasks to define exact job we want DolphinScheduler do for us. For now,
+we only support `shell` task to execute shell task. [This link][all-task] list all tasks support in DolphinScheduler
+and would be implemented in the further.
+
+### Code Style
+
+We use [isort][isort] to automatically keep Python imports alphabetically, and use [Black][black] for code
+formatter and [Flake8][flake8] for pep8 checker. If you use [pycharm][pycharm]or [IntelliJ IDEA][idea],
+maybe you could follow [Black-integration][black-editor] to configure them in your environment.
+
+Our Python API CI would automatically run code style checker and unittest when you submit pull request in
+GitHub, you could also run static check locally.
+
+```shell
+# We recommend you run isort and Black before Flake8, because Black could auto fix some code style issue
+# but Flake8 just hint when code style not match pep8
+
+# Run Isort
+isort .
+
+# Run Black
+black .
+
+# Run Flake8
+flake8
+```
+
+### Testing
+
+pydolphinscheduler using [pytest][pytest] to test our codebase. GitHub Action will run our test when you create
+pull request or commit to dev branch, with python version `3.6|3.7|3.8|3.9` and operating system `linux|macOS|windows`.
+
+To test locally, you could directly run pytest after set `PYTHONPATH`
+
+```shell
+PYTHONPATH=src/ pytest
+```
+
+We try to keep pydolphinscheduler usable through unit test coverage. 90% test coverage is our target, but for
+now, we require test coverage up to 85%, and each pull request leas than 85% would fail our CI step
+`Tests coverage`. We use [coverage][coverage] to check our test coverage, and you could check it locally by
+run command.
+
+```shell
+coverage run && coverage report
+```
+
+It would not only run unit test but also show each file coverage which cover rate less than 100%, and `TOTAL`
+line show you total coverage of you code. If your CI failed with coverage you could go and find some reason by
+this command output.
+
+<!-- content -->
+[pypi]: https://pypi.org/
+[dev-setup]: https://dolphinscheduler.apache.org/en-us/development/development-environment-setup.html
+[ui-project]: http://8.142.34.29:12345/dolphinscheduler/ui/#/projects/list
+[py4j]: https://www.py4j.org/index.html
+[pycharm]: https://www.jetbrains.com/pycharm
+[idea]: https://www.jetbrains.com/idea/
+[all-task]: https://dolphinscheduler.apache.org/en-us/docs/dev/user_doc/guide/task/shell.html
+[pytest]: https://docs.pytest.org/en/latest/
+[black]: https://black.readthedocs.io/en/stable/index.html
+[flake8]: https://flake8.pycqa.org/en/latest/index.html
+[black-editor]: https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea
+[coverage]: https://coverage.readthedocs.io/en/stable/
+[isort]: https://pycqa.github.io/isort/index.html
+<!-- badge -->
+[ga-py-test]: https://github.com/apache/dolphinscheduler/actions/workflows/py-ci.yml/badge.svg?branch=dev
+[ga]: https://github.com/apache/dolphinscheduler/actions
+[black-shield]: https://img.shields.io/badge/code%20style-black-000000.svg
+[black-gh]: https://github.com/psf/black
+[isort-shield]: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
+[isort-gh]: https://pycqa.github.io/isort/
diff --git a/dolphinscheduler-python/pydolphinscheduler/ROADMAP.md b/dolphinscheduler-python/pydolphinscheduler/ROADMAP.md
new file mode 100644
index 0000000..32ad5e2
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/ROADMAP.md
@@ -0,0 +1,34 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+## Roadmap
+
+### v0.0.3
+
+Add other features, tasks, parameters in DS, keep code coverage up to 90%
+
+### v0.0.2
+
+Add docs about how to use and develop package, code coverage up to 90%, add CI/CD
+for package
+
+### v0.0.1(current)
+
+Setup up POC, for defining DAG with python code, running DAG manually,
+releasing to pypi
\ No newline at end of file
diff --git a/dolphinscheduler-python/pydolphinscheduler/examples/bulk_create.py b/dolphinscheduler-python/pydolphinscheduler/examples/bulk_create.py
new file mode 100644
index 0000000..72bdb02
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/examples/bulk_create.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+This example show you how to create workflows in batch mode.
+
+After this example run, we will create 10 workflows named `workflow:<workflow_num>`, and with 3 tasks
+named `task:<task_num>-workflow:<workflow_num>` in each workflow. Task shape as below
+
+task:1-workflow:1 -> task:2-workflow:1 -> task:3-workflow:1
+
+Each workflow is linear since we set `IS_CHAIN=True`, you could change task to parallel by set it to `False`.
+"""
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.shell import Shell
+
+NUM_WORKFLOWS = 10
+NUM_TASKS = 5
+# Make sure your tenant exists in your operator system
+TENANT = "exists_tenant"
+# Whether task should dependent on pre one or not
+# False will create workflow with independent task, while True task will dependent on pre-task and dependence
+# link like `pre_task -> current_task -> next_task`, default True
+IS_CHAIN = True
+
+for wf in range(0, NUM_WORKFLOWS):
+ workflow_name = f"workflow:{wf}"
+
+ with ProcessDefinition(name=workflow_name, tenant=TENANT) as pd:
+ for t in range(0, NUM_TASKS):
+ task_name = f"task:{t}-{workflow_name}"
+ command = f"echo This is task {task_name}"
+ task = Shell(name=task_name, command=command)
+
+ if IS_CHAIN and t > 0:
+ pre_task_name = f"task:{t-1}-{workflow_name}"
+ pd.get_one_task_by_name(pre_task_name) >> task
+
+ # We just submit workflow and task definition without set schedule time or run it manually
+ pd.submit()
diff --git a/dolphinscheduler-python/pydolphinscheduler/examples/task_conditions_example.py b/dolphinscheduler-python/pydolphinscheduler/examples/task_conditions_example.py
new file mode 100644
index 0000000..a7723f3
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/examples/task_conditions_example.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+r"""
+A example workflow for task condition.
+
+This example will create five task in single workflow, with four shell task and one condition task. Task
+condition have one upstream which we declare explicit with syntax `parent >> condition`, and three downstream
+automatically set dependence by condition task by passing parameter `condition`. The graph of this workflow
+like:
+ --> condition_success_1
+ /
+parent -> conditions -> --> condition_success_2
+ \
+ --> condition_fail
+.
+"""
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.condition import FAILURE, SUCCESS, And, Conditions
+from pydolphinscheduler.tasks.shell import Shell
+
+with ProcessDefinition(name="task_conditions_example", tenant="tenant_exists") as pd:
+ parent = Shell(name="parent", command="echo parent")
+ condition_success_1 = Shell(
+ name="condition_success_1", command="echo condition_success_1"
+ )
+ condition_success_2 = Shell(
+ name="condition_success_2", command="echo condition_success_2"
+ )
+ condition_fail = Shell(name="condition_fail", command="echo condition_fail")
+ cond_operator = And(
+ And(
+ SUCCESS(condition_success_1, condition_success_2),
+ FAILURE(condition_fail),
+ ),
+ )
+
+ condition = Conditions(name="conditions", condition=cond_operator)
+ parent >> condition
+ pd.submit()
diff --git a/dolphinscheduler-python/pydolphinscheduler/examples/task_datax_example.py b/dolphinscheduler-python/pydolphinscheduler/examples/task_datax_example.py
new file mode 100644
index 0000000..c9ca80c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/examples/task_datax_example.py
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+A example workflow for task datax.
+
+This example will create a workflow named `task_datax`.
+`task_datax` is true workflow define and run task task_datax.
+You can create data sources `first_mysql` and `first_mysql` through UI.
+It creates a task to synchronize datax from the source database to the target database.
+"""
+
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.datax import CustomDataX, DataX
+
+# datax json template
+JSON_TEMPLATE = ""
+
+with ProcessDefinition(
+ name="task_datax",
+ tenant="tenant_exists",
+) as pd:
+ # This task synchronizes the data in `t_ds_project`
+ # of `first_mysql` database to `target_project` of `second_mysql` database.
+ task1 = DataX(
+ name="task_datax",
+ datasource_name="first_mysql",
+ datatarget_name="second_mysql",
+ sql="select id, name, code, description from source_table",
+ target_table="target_table",
+ )
+
+ # you can custom json_template of datax to sync data.
+ task2 = CustomDataX(name="task_custom_datax", json=JSON_TEMPLATE)
+ pd.run()
diff --git a/dolphinscheduler-python/pydolphinscheduler/examples/task_dependent_example.py b/dolphinscheduler-python/pydolphinscheduler/examples/task_dependent_example.py
new file mode 100644
index 0000000..68f2a57
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/examples/task_dependent_example.py
@@ -0,0 +1,72 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+r"""
+A example workflow for task dependent.
+
+This example will create two workflows named `task_dependent` and `task_dependent_external`.
+`task_dependent` is true workflow define and run task dependent, while `task_dependent_external`
+define outside workflow and task from dependent.
+
+After this script submit, we would get workflow as below:
+
+task_dependent_external:
+
+task_1
+task_2
+task_3
+
+task_dependent:
+
+task_dependent(this task dependent on task_dependent_external.task_1 and task_dependent_external.task_2).
+"""
+from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.dependent import And, Dependent, DependentItem, Or
+from pydolphinscheduler.tasks.shell import Shell
+
+with ProcessDefinition(
+ name="task_dependent_external",
+ tenant="tenant_exists",
+) as pd:
+ task_1 = Shell(name="task_1", command="echo task 1")
+ task_2 = Shell(name="task_2", command="echo task 2")
+ task_3 = Shell(name="task_3", command="echo task 3")
+ pd.submit()
+
+with ProcessDefinition(
+ name="task_dependent",
+ tenant="tenant_exists",
+) as pd:
+ task = Dependent(
+ name="task_dependent",
+ dependence=And(
+ Or(
+ DependentItem(
+ project_name=ProcessDefinitionDefault.PROJECT,
+ process_definition_name="task_dependent_external",
+ dependent_task_name="task_1",
+ ),
+ DependentItem(
+ project_name=ProcessDefinitionDefault.PROJECT,
+ process_definition_name="task_dependent_external",
+ dependent_task_name="task_2",
+ ),
+ )
+ ),
+ )
+ pd.submit()
diff --git a/dolphinscheduler-python/pydolphinscheduler/examples/task_switch_example.py b/dolphinscheduler-python/pydolphinscheduler/examples/task_switch_example.py
new file mode 100644
index 0000000..5ab2aa5
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/examples/task_switch_example.py
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+r"""
+A example workflow for task switch.
+
+This example will create four task in single workflow, with three shell task and one switch task. Task switch
+have one upstream which we declare explicit with syntax `parent >> switch`, and two downstream automatically
+set dependence by switch task by passing parameter `condition`. The graph of this workflow like:
+ --> switch_child_1
+ /
+parent -> switch ->
+ \
+ --> switch_child_2
+.
+"""
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.shell import Shell
+from pydolphinscheduler.tasks.switch import Branch, Default, Switch, SwitchCondition
+
+with ProcessDefinition(
+ name="task_dependent_external",
+ tenant="tenant_exists",
+) as pd:
+ parent = Shell(name="parent", command="echo parent")
+ switch_child_1 = Shell(name="switch_child_1", command="echo switch_child_1")
+ switch_child_2 = Shell(name="switch_child_2", command="echo switch_child_2")
+ switch_condition = SwitchCondition(
+ Branch(condition="${var} > 1", task=switch_child_1),
+ Default(task=switch_child_2),
+ )
+
+ switch = Switch(name="switch", condition=switch_condition)
+ parent >> switch
+ pd.submit()
diff --git a/dolphinscheduler-python/pydolphinscheduler/examples/tutorial.py b/dolphinscheduler-python/pydolphinscheduler/examples/tutorial.py
new file mode 100644
index 0000000..d58bd75
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/examples/tutorial.py
@@ -0,0 +1,52 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+r"""
+A tutorial example take you to experience pydolphinscheduler.
+
+After tutorial.py file submit to Apache DolphinScheduler server a DAG would be create,
+and workflow DAG graph as below:
+
+ --> task_child_one
+ / \
+task_parent --> --> task_union
+ \ /
+ --> task_child_two
+
+it will instantiate and run all the task it have.
+"""
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.shell import Shell
+
+with ProcessDefinition(
+ name="tutorial",
+ schedule="0 0 0 * * ? *",
+ start_time="2021-01-01",
+ tenant="tenant_exists",
+) as pd:
+ task_parent = Shell(name="task_parent", command="echo hello pydolphinscheduler")
+ task_child_one = Shell(name="task_child_one", command="echo 'child one'")
+ task_child_two = Shell(name="task_child_two", command="echo 'child two'")
+ task_union = Shell(name="task_union", command="echo union")
+
+ task_group = [task_child_one, task_child_two]
+ task_parent.set_downstream(task_group)
+
+ task_union << task_group
+
+ pd.run()
diff --git a/dolphinscheduler-python/pydolphinscheduler/pytest.ini b/dolphinscheduler-python/pydolphinscheduler/pytest.ini
new file mode 100644
index 0000000..f2c7ae6
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/pytest.ini
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[pytest]
+# Do not test test_java_gateway.py due to we can not mock java gateway for now
+addopts = --ignore=tests/test_java_gateway.py
+
+# add path here to skip pytest scan it
+norecursedirs =
+ tests/testing
diff --git a/dolphinscheduler-python/pydolphinscheduler/requirements.txt b/dolphinscheduler-python/pydolphinscheduler/requirements.txt
new file mode 100644
index 0000000..cdec3ca
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/requirements.txt
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+py4j~=0.10.9.2
\ No newline at end of file
diff --git a/dolphinscheduler-python/pydolphinscheduler/requirements_dev.txt b/dolphinscheduler-python/pydolphinscheduler/requirements_dev.txt
new file mode 100644
index 0000000..d31a56d
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/requirements_dev.txt
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# testting
+pytest~=6.2.5
+freezegun
+# Test coverage
+coverage
+# code linting and formatting
+flake8
+flake8-docstrings
+flake8-black
+isort
diff --git a/dolphinscheduler-python/pydolphinscheduler/setup.cfg b/dolphinscheduler-python/pydolphinscheduler/setup.cfg
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/setup.cfg
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/dolphinscheduler-python/pydolphinscheduler/setup.py b/dolphinscheduler-python/pydolphinscheduler/setup.py
new file mode 100644
index 0000000..4a6c045
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/setup.py
@@ -0,0 +1,94 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""The script for setting up pydolphinscheduler."""
+
+import sys
+from os.path import dirname, join
+
+from setuptools import find_packages, setup
+
+version = "0.0.1.dev0"
+
+if sys.version_info[0] < 3:
+ raise Exception(
+ "pydolphinscheduler does not support Python 2. Please upgrade to Python 3."
+ )
+
+
+def read(*names, **kwargs):
+ """Read file content from given file path."""
+ return open(
+ join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8")
+ ).read()
+
+
+setup(
+ name="pydolphinscheduler",
+ version=version,
+ license="Apache License 2.0",
+ description="Apache DolphinScheduler python SDK",
+ long_description=read("README.md"),
+ # Make sure pypi is expecting markdown
+ long_description_content_type="text/markdown",
+ author="Apache Software Foundation",
+ author_email="dev@dolphinscheduler.apache.org",
+ url="https://dolphinscheduler.apache.org/",
+ python_requires=">=3.6",
+ keywords=[
+ "dolphinscheduler",
+ "workflow",
+ "scheduler",
+ "taskflow",
+ ],
+ project_urls={
+ "Homepage": "https://dolphinscheduler.apache.org",
+ "Documentation": "https://dolphinscheduler.apache.org/en-us/docs/latest/user_doc/quick-start.html",
+ "Source": "https://github.com/apache/dolphinscheduler",
+ "Issue Tracker": "https://github.com/apache/dolphinscheduler/issues",
+ "Discussion": "https://github.com/apache/dolphinscheduler/discussions",
+ "Twitter": "https://twitter.com/dolphinschedule",
+ },
+ packages=find_packages(where="src"),
+ package_dir={"": "src"},
+ include_package_data=True,
+ classifiers=[
+ # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ "Development Status :: 1 - Planning",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: Unix",
+ "Operating System :: POSIX",
+ "Operating System :: Microsoft :: Windows",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Software Development :: User Interfaces",
+ ],
+ install_requires=[
+ # Core
+ "py4j~=0.10",
+ # Dev
+ "pytest~=6.2",
+ ],
+)
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/__init__.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/__init__.py
new file mode 100644
index 0000000..701b4cc
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init root of pydolphinscheduler."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py
new file mode 100644
index 0000000..84becbc
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Constants for pydolphinscheduler."""
+
+
+class ProcessDefinitionReleaseState:
+ """Constants for :class:`pydolphinscheduler.core.process_definition.ProcessDefinition` release state."""
+
+ ONLINE: str = "ONLINE"
+ OFFLINE: str = "OFFLINE"
+
+
+class ProcessDefinitionDefault:
+ """Constants default value for :class:`pydolphinscheduler.core.process_definition.ProcessDefinition`."""
+
+ PROJECT: str = "project-pydolphin"
+ TENANT: str = "tenant_pydolphin"
+ USER: str = "userPythonGateway"
+ # TODO simple set password same as username
+ USER_PWD: str = "userPythonGateway"
+ USER_EMAIL: str = "userPythonGateway@dolphinscheduler.com"
+ USER_PHONE: str = "11111111111"
+ USER_STATE: int = 1
+ QUEUE: str = "queuePythonGateway"
+ WORKER_GROUP: str = "default"
+ TIME_ZONE: str = "Asia/Shanghai"
+
+
+class TaskPriority(str):
+ """Constants for task priority."""
+
+ HIGHEST = "HIGHEST"
+ HIGH = "HIGH"
+ MEDIUM = "MEDIUM"
+ LOW = "LOW"
+ LOWEST = "LOWEST"
+
+
+class TaskFlag(str):
+ """Constants for task flag."""
+
+ YES = "YES"
+ NO = "NO"
+
+
+class TaskTimeoutFlag(str):
+ """Constants for task timeout flag."""
+
+ CLOSE = "CLOSE"
+
+
+class TaskType(str):
+ """Constants for task type, it will also show you which kind we support up to now."""
+
+ SHELL = "SHELL"
+ HTTP = "HTTP"
+ PYTHON = "PYTHON"
+ SQL = "SQL"
+ SUB_PROCESS = "SUB_PROCESS"
+ PROCEDURE = "PROCEDURE"
+ DATAX = "DATAX"
+ DEPENDENT = "DEPENDENT"
+ CONDITIONS = "CONDITIONS"
+ SWITCH = "SWITCH"
+
+
+class DefaultTaskCodeNum(str):
+ """Constants and default value for default task code number."""
+
+ DEFAULT = 1
+
+
+class JavaGatewayDefault(str):
+ """Constants and default value for java gateway."""
+
+ RESULT_MESSAGE_KEYWORD = "msg"
+ RESULT_MESSAGE_SUCCESS = "success"
+
+ RESULT_STATUS_KEYWORD = "status"
+ RESULT_STATUS_SUCCESS = "SUCCESS"
+
+ RESULT_DATA = "data"
+
+
+class Delimiter(str):
+ """Constants for delimiter."""
+
+ BAR = "-"
+ DASH = "/"
+ COLON = ":"
+ UNDERSCORE = "_"
+ DIRECTION = "->"
+
+
+class Time(str):
+ """Constants for date."""
+
+ FMT_STD_DATE = "%Y-%m-%d"
+ LEN_STD_DATE = 10
+
+ FMT_DASH_DATE = "%Y/%m/%d"
+
+ FMT_SHORT_DATE = "%Y%m%d"
+ LEN_SHORT_DATE = 8
+
+ FMT_STD_TIME = "%H:%M:%S"
+ FMT_NO_COLON_TIME = "%H%M%S"
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/__init__.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/__init__.py
new file mode 100644
index 0000000..3fbddf3
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init pydolphinscheduler.core package."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base.py
new file mode 100644
index 0000000..690351a
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base.py
@@ -0,0 +1,74 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler Base object."""
+
+from typing import Dict, Optional
+
+# from pydolphinscheduler.side.user import User
+from pydolphinscheduler.utils.string import attr2camel
+
+
+class Base:
+ """DolphinScheduler Base object."""
+
+ # Object key attribute, to test whether object equals and so on.
+ _KEY_ATTR: set = {"name", "description"}
+
+ # Object defines attribute, use when needs to communicate with Java gateway server.
+ _DEFINE_ATTR: set = set()
+
+ # Object default attribute, will add those attribute to `_DEFINE_ATTR` when init assign missing.
+ _DEFAULT_ATTR: Dict = {}
+
+ def __init__(self, name: str, description: Optional[str] = None):
+ self.name = name
+ self.description = description
+
+ def __repr__(self) -> str:
+ return f'<{type(self).__name__}: name="{self.name}">'
+
+ def __eq__(self, other):
+ return type(self) == type(other) and all(
+ getattr(self, a, None) == getattr(other, a, None) for a in self._KEY_ATTR
+ )
+
+ def get_define_custom(
+ self, camel_attr: bool = True, custom_attr: set = None
+ ) -> Dict:
+ """Get object definition attribute by given attr set."""
+ content = {}
+ for attr in custom_attr:
+ val = getattr(self, attr, None)
+ if camel_attr:
+ content[attr2camel(attr)] = val
+ else:
+ content[attr] = val
+ return content
+
+ def get_define(self, camel_attr: bool = True) -> Dict:
+ """Get object definition attribute communicate to Java gateway server.
+
+ use attribute `self._DEFINE_ATTR` to determine which attributes should including when
+ object tries to communicate with Java gateway server.
+ """
+ content = self.get_define_custom(camel_attr, self._DEFINE_ATTR)
+ update_default = {
+ k: self._DEFAULT_ATTR.get(k) for k in self._DEFAULT_ATTR if k not in content
+ }
+ content.update(update_default)
+ return content
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py
new file mode 100644
index 0000000..ed20d70
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Module for side object."""
+
+from typing import Optional
+
+from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core.base import Base
+
+
+class BaseSide(Base):
+ """Base class for side object, it declare base behavior for them."""
+
+ def __init__(self, name: str, description: Optional[str] = None):
+ super().__init__(name, description)
+
+ @classmethod
+ def create_if_not_exists(
+ cls,
+ # TODO comment for avoiding cycle import
+ # user: Optional[User] = ProcessDefinitionDefault.USER
+ user=ProcessDefinitionDefault.USER,
+ ):
+ """Create Base if not exists."""
+ raise NotImplementedError
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/database.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/database.py
new file mode 100644
index 0000000..d4addf9
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/database.py
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Module database."""
+
+import logging
+from typing import Dict
+
+from py4j.protocol import Py4JJavaError
+
+from pydolphinscheduler.java_gateway import launch_gateway
+
+
+class Database(dict):
+ """database object, get information about database.
+
+ You provider database_name contain connection information, it decisions which
+ database type and database instance would run task.
+ """
+
+ def __init__(self, database_name: str, type_key, database_key, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._database = {}
+ self.database_name = database_name
+ self[type_key] = self.database_type
+ self[database_key] = self.database_id
+
+ @property
+ def database_type(self) -> str:
+ """Get database type from java gateway, a wrapper for :func:`get_database_info`."""
+ return self.get_database_info(self.database_name).get("type")
+
+ @property
+ def database_id(self) -> str:
+ """Get database id from java gateway, a wrapper for :func:`get_database_info`."""
+ return self.get_database_info(self.database_name).get("id")
+
+ def get_database_info(self, name) -> Dict:
+ """Get database info from java gateway, contains database id, type, name."""
+ if self._database:
+ return self._database
+ else:
+ gateway = launch_gateway()
+ try:
+ self._database = gateway.entry_point.getDatasourceInfo(name)
+ # Handler database source do not exists error, for now we just terminate the process.
+ except Py4JJavaError:
+ logging.error("Datasource name `%s` do not exists.", name)
+ exit(1)
+ return self._database
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py
new file mode 100644
index 0000000..711cfb0
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py
@@ -0,0 +1,360 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Module process definition, core class for workflow define."""
+
+import json
+from datetime import datetime
+from typing import Any, Dict, List, Optional, Set
+
+from pydolphinscheduler.constants import (
+ ProcessDefinitionDefault,
+ ProcessDefinitionReleaseState,
+)
+from pydolphinscheduler.core.base import Base
+from pydolphinscheduler.exceptions import PyDSParamException, PyDSTaskNoFoundException
+from pydolphinscheduler.java_gateway import launch_gateway
+from pydolphinscheduler.side import Project, Tenant, User
+from pydolphinscheduler.utils.date import MAX_DATETIME, conv_from_str, conv_to_schedule
+
+
+class ProcessDefinitionContext:
+ """Class process definition context, use when task get process definition from context expression."""
+
+ _context_managed_process_definition: Optional["ProcessDefinition"] = None
+
+ @classmethod
+ def set(cls, pd: "ProcessDefinition") -> None:
+ """Set attribute self._context_managed_process_definition."""
+ cls._context_managed_process_definition = pd
+
+ @classmethod
+ def get(cls) -> Optional["ProcessDefinition"]:
+ """Get attribute self._context_managed_process_definition."""
+ return cls._context_managed_process_definition
+
+ @classmethod
+ def delete(cls) -> None:
+ """Delete attribute self._context_managed_process_definition."""
+ cls._context_managed_process_definition = None
+
+
+class ProcessDefinition(Base):
+ """process definition object, will define process definition attribute, task, relation.
+
+ TODO: maybe we should rename this class, currently use DS object name.
+ """
+
+ # key attribute for identify ProcessDefinition object
+ _KEY_ATTR = {
+ "name",
+ "project",
+ "tenant",
+ "release_state",
+ "param",
+ }
+
+ _DEFINE_ATTR = {
+ "name",
+ "description",
+ "_project",
+ "_tenant",
+ "worker_group",
+ "timeout",
+ "release_state",
+ "param",
+ "tasks",
+ "task_definition_json",
+ "task_relation_json",
+ }
+
+ def __init__(
+ self,
+ name: str,
+ description: Optional[str] = None,
+ schedule: Optional[str] = None,
+ start_time: Optional[str] = None,
+ end_time: Optional[str] = None,
+ timezone: Optional[str] = ProcessDefinitionDefault.TIME_ZONE,
+ user: Optional[str] = ProcessDefinitionDefault.USER,
+ project: Optional[str] = ProcessDefinitionDefault.PROJECT,
+ tenant: Optional[str] = ProcessDefinitionDefault.TENANT,
+ queue: Optional[str] = ProcessDefinitionDefault.QUEUE,
+ worker_group: Optional[str] = ProcessDefinitionDefault.WORKER_GROUP,
+ timeout: Optional[int] = 0,
+ release_state: Optional[str] = ProcessDefinitionReleaseState.ONLINE,
+ param: Optional[List] = None,
+ ):
+ super().__init__(name, description)
+ self.schedule = schedule
+ self._start_time = start_time
+ self._end_time = end_time
+ self.timezone = timezone
+ self._user = user
+ self._project = project
+ self._tenant = tenant
+ self._queue = queue
+ self.worker_group = worker_group
+ self.timeout = timeout
+ self.release_state = release_state
+ self.param = param
+ self.tasks: dict = {}
+ # TODO how to fix circle import
+ self._task_relations: set["TaskRelation"] = set() # noqa: F821
+ self._process_definition_code = None
+
+ def __enter__(self) -> "ProcessDefinition":
+ ProcessDefinitionContext.set(self)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ ProcessDefinitionContext.delete()
+
+ @property
+ def tenant(self) -> Tenant:
+ """Get attribute tenant."""
+ return Tenant(self._tenant)
+
+ @tenant.setter
+ def tenant(self, tenant: Tenant) -> None:
+ """Set attribute tenant."""
+ self._tenant = tenant.name
+
+ @property
+ def project(self) -> Project:
+ """Get attribute project."""
+ return Project(self._project)
+
+ @project.setter
+ def project(self, project: Project) -> None:
+ """Set attribute project."""
+ self._project = project.name
+
+ @property
+ def user(self) -> User:
+ """Get user object.
+
+ For now we just get from python side but not from java gateway side, so it may not correct.
+ """
+ return User(
+ self._user,
+ ProcessDefinitionDefault.USER_PWD,
+ ProcessDefinitionDefault.USER_EMAIL,
+ ProcessDefinitionDefault.USER_PHONE,
+ self._tenant,
+ self._queue,
+ ProcessDefinitionDefault.USER_STATE,
+ )
+
+ @staticmethod
+ def _parse_datetime(val: Any) -> Any:
+ if val is None or isinstance(val, datetime):
+ return val
+ elif isinstance(val, str):
+ return conv_from_str(val)
+ else:
+ raise PyDSParamException("Do not support value type %s for now", type(val))
+
+ @property
+ def start_time(self) -> Any:
+ """Get attribute start_time."""
+ return self._parse_datetime(self._start_time)
+
+ @start_time.setter
+ def start_time(self, val) -> None:
+ """Set attribute start_time."""
+ self._start_time = val
+
+ @property
+ def end_time(self) -> Any:
+ """Get attribute end_time."""
+ return self._parse_datetime(self._end_time)
+
+ @end_time.setter
+ def end_time(self, val) -> None:
+ """Set attribute end_time."""
+ self._end_time = val
+
+ @property
+ def task_definition_json(self) -> List[Dict]:
+ """Return all tasks definition in list of dict."""
+ if not self.tasks:
+ return [self.tasks]
+ else:
+ return [task.get_define() for task in self.tasks.values()]
+
+ @property
+ def task_relation_json(self) -> List[Dict]:
+ """Return all relation between tasks pair in list of dict."""
+ if not self.tasks:
+ return [self.tasks]
+ else:
+ self._handle_root_relation()
+ return [tr.get_define() for tr in self._task_relations]
+
+ @property
+ def schedule_json(self) -> Optional[Dict]:
+ """Get schedule parameter json object. This is requests from java gateway interface."""
+ if not self.schedule:
+ return None
+ else:
+ start_time = conv_to_schedule(
+ self.start_time if self.start_time else datetime.now()
+ )
+ end_time = conv_to_schedule(
+ self.end_time if self.end_time else MAX_DATETIME
+ )
+ return {
+ "startTime": start_time,
+ "endTime": end_time,
+ "crontab": self.schedule,
+ "timezoneId": self.timezone,
+ }
+
+ # TODO inti DAG's tasks are in the same location with default {x: 0, y: 0}
+ @property
+ def task_location(self) -> List[Dict]:
+ """Return all tasks location for all process definition.
+
+ For now, we only set all location with same x and y valued equal to 0. Because we do not
+ find a good way to set task locations. This is requests from java gateway interface.
+ """
+ if not self.tasks:
+ return [self.tasks]
+ else:
+ return [{"taskCode": task_code, "x": 0, "y": 0} for task_code in self.tasks]
+
+ @property
+ def task_list(self) -> List["Task"]: # noqa: F821
+ """Return list of tasks objects."""
+ return list(self.tasks.values())
+
+ def _handle_root_relation(self):
+ """Handle root task property :class:`pydolphinscheduler.core.task.TaskRelation`.
+
+ Root task in DAG do not have dominant upstream node, but we have to add an exactly default
+ upstream task with task_code equal to `0`. This is requests from java gateway interface.
+ """
+ from pydolphinscheduler.core.task import TaskRelation
+
+ post_relation_code = set()
+ for relation in self._task_relations:
+ post_relation_code.add(relation.post_task_code)
+ for task in self.task_list:
+ if task.code not in post_relation_code:
+ root_relation = TaskRelation(pre_task_code=0, post_task_code=task.code)
+ self._task_relations.add(root_relation)
+
+ def add_task(self, task: "Task") -> None: # noqa: F821
+ """Add a single task to process definition."""
+ self.tasks[task.code] = task
+ task._process_definition = self
+
+ def add_tasks(self, tasks: List["Task"]) -> None: # noqa: F821
+ """Add task sequence to process definition, it a wrapper of :func:`add_task`."""
+ for task in tasks:
+ self.add_task(task)
+
+ def get_task(self, code: str) -> "Task": # noqa: F821
+ """Get task object from process definition by given code."""
+ if code not in self.tasks:
+ raise PyDSTaskNoFoundException(
+ "Task with code %s can not found in process definition %",
+ (code, self.name),
+ )
+ return self.tasks[code]
+
+ # TODO which tying should return in this case
+ def get_tasks_by_name(self, name: str) -> Set["Task"]: # noqa: F821
+ """Get tasks object by given name, if will return all tasks with this name."""
+ find = set()
+ for task in self.tasks.values():
+ if task.name == name:
+ find.add(task)
+ return find
+
+ def get_one_task_by_name(self, name: str) -> "Task": # noqa: F821
+ """Get exact one task from process definition by given name.
+
+ Function always return one task even though this process definition have more than one task with
+ this name.
+ """
+ tasks = self.get_tasks_by_name(name)
+ if not tasks:
+ raise PyDSTaskNoFoundException(f"Can not find task with name {name}.")
+ return tasks.pop()
+
+ def run(self):
+ """Submit and Start ProcessDefinition instance.
+
+ Shortcut for function :func:`submit` and function :func:`start`. Only support manual start workflow
+ for now, and schedule run will coming soon.
+ :return:
+ """
+ self.submit()
+ self.start()
+
+ def _ensure_side_model_exists(self):
+ """Ensure process definition side model exists.
+
+ For now, side object including :class:`pydolphinscheduler.side.project.Project`,
+ :class:`pydolphinscheduler.side.tenant.Tenant`, :class:`pydolphinscheduler.side.user.User`.
+ If these model not exists, would create default value in
+ :class:`pydolphinscheduler.constants.ProcessDefinitionDefault`.
+ """
+ # TODO used metaclass for more pythonic
+ self.tenant.create_if_not_exists(self._queue)
+ # model User have to create after Tenant created
+ self.user.create_if_not_exists()
+ # Project model need User object exists
+ self.project.create_if_not_exists(self._user)
+
+ def submit(self) -> int:
+ """Submit ProcessDefinition instance to java gateway."""
+ self._ensure_side_model_exists()
+ gateway = launch_gateway()
+ self._process_definition_code = gateway.entry_point.createOrUpdateProcessDefinition(
+ self._user,
+ self._project,
+ self.name,
+ str(self.description) if self.description else "",
+ str(self.param) if self.param else None,
+ json.dumps(self.schedule_json) if self.schedule_json else None,
+ json.dumps(self.task_location),
+ self.timeout,
+ self.worker_group,
+ self._tenant,
+ # TODO add serialization function
+ json.dumps(self.task_relation_json),
+ json.dumps(self.task_definition_json),
+ )
+ return self._process_definition_code
+
+ def start(self) -> None:
+ """Create and start ProcessDefinition instance.
+
+ which post to `start-process-instance` to java gateway
+ """
+ gateway = launch_gateway()
+ gateway.entry_point.execProcessInstance(
+ self._user,
+ self._project,
+ self.name,
+ "",
+ self.worker_group,
+ 24 * 3600,
+ )
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py
new file mode 100644
index 0000000..8a90efc
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py
@@ -0,0 +1,268 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler Task and TaskRelation object."""
+
+import logging
+from typing import Dict, List, Optional, Sequence, Set, Tuple, Union
+
+from pydolphinscheduler.constants import (
+ Delimiter,
+ ProcessDefinitionDefault,
+ TaskFlag,
+ TaskPriority,
+ TaskTimeoutFlag,
+)
+from pydolphinscheduler.core.base import Base
+from pydolphinscheduler.core.process_definition import (
+ ProcessDefinition,
+ ProcessDefinitionContext,
+)
+from pydolphinscheduler.java_gateway import launch_gateway
+
+
+class TaskRelation(Base):
+ """TaskRelation object, describe the relation of exactly two tasks."""
+
+ # Add attr `_KEY_ATTR` to overwrite :func:`__eq__`, it is make set
+ # `Task.process_definition._task_relations` work correctly.
+ _KEY_ATTR = {
+ "pre_task_code",
+ "post_task_code",
+ }
+
+ _DEFINE_ATTR = {
+ "pre_task_code",
+ "post_task_code",
+ }
+
+ _DEFAULT_ATTR = {
+ "name": "",
+ "preTaskVersion": 1,
+ "postTaskVersion": 1,
+ "conditionType": 0,
+ "conditionParams": {},
+ }
+
+ def __init__(
+ self,
+ pre_task_code: int,
+ post_task_code: int,
+ name: Optional[str] = None,
+ ):
+ super().__init__(name)
+ self.pre_task_code = pre_task_code
+ self.post_task_code = post_task_code
+
+ def __hash__(self):
+ return hash(f"{self.pre_task_code} {Delimiter.DIRECTION} {self.post_task_code}")
+
+
+class Task(Base):
+ """Task object, parent class for all exactly task type."""
+
+ _DEFINE_ATTR = {
+ "name",
+ "code",
+ "version",
+ "task_type",
+ "task_params",
+ "description",
+ "flag",
+ "task_priority",
+ "worker_group",
+ "delay_time",
+ "fail_retry_times",
+ "fail_retry_interval",
+ "timeout_flag",
+ "timeout_notify_strategy",
+ "timeout",
+ }
+
+ _task_custom_attr: set = set()
+
+ DEFAULT_CONDITION_RESULT = {"successNode": [""], "failedNode": [""]}
+
+ def __init__(
+ self,
+ name: str,
+ task_type: str,
+ description: Optional[str] = None,
+ flag: Optional[str] = TaskFlag.YES,
+ task_priority: Optional[str] = TaskPriority.MEDIUM,
+ worker_group: Optional[str] = ProcessDefinitionDefault.WORKER_GROUP,
+ delay_time: Optional[int] = 0,
+ fail_retry_times: Optional[int] = 0,
+ fail_retry_interval: Optional[int] = 1,
+ timeout_flag: Optional[int] = TaskTimeoutFlag.CLOSE,
+ timeout_notify_strategy: Optional = None,
+ timeout: Optional[int] = 0,
+ process_definition: Optional[ProcessDefinition] = None,
+ local_params: Optional[List] = None,
+ resource_list: Optional[List] = None,
+ dependence: Optional[Dict] = None,
+ wait_start_timeout: Optional[Dict] = None,
+ condition_result: Optional[Dict] = None,
+ ):
+
+ super().__init__(name, description)
+ self.task_type = task_type
+ self.flag = flag
+ self.task_priority = task_priority
+ self.worker_group = worker_group
+ self.fail_retry_times = fail_retry_times
+ self.fail_retry_interval = fail_retry_interval
+ self.delay_time = delay_time
+ self.timeout_flag = timeout_flag
+ self.timeout_notify_strategy = timeout_notify_strategy
+ self.timeout = timeout
+ self._process_definition = None
+ self.process_definition: ProcessDefinition = (
+ process_definition or ProcessDefinitionContext.get()
+ )
+ self._upstream_task_codes: Set[int] = set()
+ self._downstream_task_codes: Set[int] = set()
+ self._task_relation: Set[TaskRelation] = set()
+ # move attribute code and version after _process_definition and process_definition declare
+ self.code, self.version = self.gen_code_and_version()
+ # Add task to process definition, maybe we could put into property process_definition latter
+ if (
+ self.process_definition is not None
+ and self.code not in self.process_definition.tasks
+ ):
+ self.process_definition.add_task(self)
+ else:
+ logging.warning(
+ "Task code %d already in process definition, prohibit re-add task.",
+ self.code,
+ )
+
+ # Attribute for task param
+ self.local_params = local_params or []
+ self.resource_list = resource_list or []
+ self.dependence = dependence or {}
+ self.wait_start_timeout = wait_start_timeout or {}
+ self.condition_result = condition_result or self.DEFAULT_CONDITION_RESULT
+
+ @property
+ def process_definition(self) -> Optional[ProcessDefinition]:
+ """Get attribute process_definition."""
+ return self._process_definition
+
+ @process_definition.setter
+ def process_definition(self, process_definition: Optional[ProcessDefinition]):
+ """Set attribute process_definition."""
+ self._process_definition = process_definition
+
+ @property
+ def task_params(self) -> Optional[Dict]:
+ """Get task parameter object.
+
+ Will get result to combine _task_custom_attr and custom_attr.
+ """
+ custom_attr = {
+ "local_params",
+ "resource_list",
+ "dependence",
+ "wait_start_timeout",
+ "condition_result",
+ }
+ custom_attr |= self._task_custom_attr
+ return self.get_define_custom(custom_attr=custom_attr)
+
+ def __hash__(self):
+ return hash(self.code)
+
+ def __lshift__(self, other: Union["Task", Sequence["Task"]]):
+ """Implement Task << Task."""
+ self.set_upstream(other)
+ return other
+
+ def __rshift__(self, other: Union["Task", Sequence["Task"]]):
+ """Implement Task >> Task."""
+ self.set_downstream(other)
+ return other
+
+ def __rrshift__(self, other: Union["Task", Sequence["Task"]]):
+ """Call for Task >> [Task] because list don't have __rshift__ operators."""
+ self.__lshift__(other)
+ return self
+
+ def __rlshift__(self, other: Union["Task", Sequence["Task"]]):
+ """Call for Task << [Task] because list don't have __lshift__ operators."""
+ self.__rshift__(other)
+ return self
+
+ def _set_deps(
+ self, tasks: Union["Task", Sequence["Task"]], upstream: bool = True
+ ) -> None:
+ """
+ Set parameter tasks dependent to current task.
+
+ it is a wrapper for :func:`set_upstream` and :func:`set_downstream`.
+ """
+ if not isinstance(tasks, Sequence):
+ tasks = [tasks]
+
+ for task in tasks:
+ if upstream:
+ self._upstream_task_codes.add(task.code)
+ task._downstream_task_codes.add(self.code)
+
+ if self._process_definition:
+ task_relation = TaskRelation(
+ pre_task_code=task.code,
+ post_task_code=self.code,
+ name=f"{task.name} {Delimiter.DIRECTION} {self.name}",
+ )
+ self.process_definition._task_relations.add(task_relation)
+ else:
+ self._downstream_task_codes.add(task.code)
+ task._upstream_task_codes.add(self.code)
+
+ if self._process_definition:
+ task_relation = TaskRelation(
+ pre_task_code=self.code,
+ post_task_code=task.code,
+ name=f"{self.name} {Delimiter.DIRECTION} {task.name}",
+ )
+ self.process_definition._task_relations.add(task_relation)
+
+ def set_upstream(self, tasks: Union["Task", Sequence["Task"]]) -> None:
+ """Set parameter tasks as upstream to current task."""
+ self._set_deps(tasks, upstream=True)
+
+ def set_downstream(self, tasks: Union["Task", Sequence["Task"]]) -> None:
+ """Set parameter tasks as downstream to current task."""
+ self._set_deps(tasks, upstream=False)
+
+ # TODO code should better generate in bulk mode when :ref: processDefinition run submit or start
+ def gen_code_and_version(self) -> Tuple:
+ """
+ Generate task code and version from java gateway.
+
+ If task name do not exists in process definition before, if will generate new code and version id
+ equal to 0 by java gateway, otherwise if will return the exists code and version.
+ """
+ # TODO get code from specific project process definition and task name
+ gateway = launch_gateway()
+ result = gateway.entry_point.getCodeAndVersion(
+ self.process_definition._project, self.name
+ )
+ # result = gateway.entry_point.genTaskCodeList(DefaultTaskCodeNum.DEFAULT)
+ # gateway_result_checker(result)
+ return result.get("code"), result.get("version")
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
new file mode 100644
index 0000000..745ef3e
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Exceptions for pydolphinscheduler."""
+
+
+class PyDSBaseException(Exception):
+ """Base exception for pydolphinscheduler."""
+
+ pass
+
+
+class PyDSParamException(PyDSBaseException):
+ """Exception for pydolphinscheduler parameter verify error."""
+
+ pass
+
+
+class PyDSTaskNoFoundException(PyDSBaseException):
+ """Exception for pydolphinscheduler workflow task no found error."""
+
+ pass
+
+
+class PyDSJavaGatewayException(PyDSBaseException):
+ """Exception for pydolphinscheduler Java gateway error."""
+
+ pass
+
+
+class PyDSProcessDefinitionNotAssignException(PyDSBaseException):
+ """Exception for pydolphinscheduler process definition not assign error."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py
new file mode 100644
index 0000000..d0b4c05
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Module java gateway, contain gateway behavior."""
+
+from typing import Any, Optional
+
+from py4j.java_collections import JavaMap
+from py4j.java_gateway import GatewayParameters, JavaGateway
+
+from pydolphinscheduler.constants import JavaGatewayDefault
+from pydolphinscheduler.exceptions import PyDSJavaGatewayException
+
+
+def launch_gateway() -> JavaGateway:
+ """Launch java gateway to pydolphinscheduler.
+
+ TODO Note that automatic conversion makes calling Java methods slightly less efficient because
+ in the worst case, Py4J needs to go through all registered converters for all parameters.
+ This is why automatic conversion is disabled by default.
+ """
+ gateway = JavaGateway(gateway_parameters=GatewayParameters(auto_convert=True))
+ return gateway
+
+
+def gateway_result_checker(
+ result: JavaMap,
+ msg_check: Optional[str] = JavaGatewayDefault.RESULT_MESSAGE_SUCCESS,
+) -> Any:
+ """Check weather java gateway result success or not."""
+ if (
+ result[JavaGatewayDefault.RESULT_STATUS_KEYWORD].toString()
+ != JavaGatewayDefault.RESULT_STATUS_SUCCESS
+ ):
+ raise PyDSJavaGatewayException("Failed when try to got result for java gateway")
+ if (
+ msg_check is not None
+ and result[JavaGatewayDefault.RESULT_MESSAGE_KEYWORD] != msg_check
+ ):
+ raise PyDSJavaGatewayException("Get result state not success.")
+ return result
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/__init__.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/__init__.py
new file mode 100644
index 0000000..de5188c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/__init__.py
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init Side package, Side package keep object related to DolphinScheduler but not in the Core part."""
+
+from pydolphinscheduler.side.project import Project
+from pydolphinscheduler.side.tenant import Tenant
+from pydolphinscheduler.side.user import User
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py
new file mode 100644
index 0000000..02382ca
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py
@@ -0,0 +1,42 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler Project object."""
+
+from typing import Optional
+
+from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core.base_side import BaseSide
+from pydolphinscheduler.java_gateway import launch_gateway
+
+
+class Project(BaseSide):
+ """DolphinScheduler Project object."""
+
+ def __init__(
+ self,
+ name: str = ProcessDefinitionDefault.PROJECT,
+ description: Optional[str] = None,
+ ):
+ super().__init__(name, description)
+
+ def create_if_not_exists(self, user=ProcessDefinitionDefault.USER) -> None:
+ """Create Project if not exists."""
+ gateway = launch_gateway()
+ gateway.entry_point.createProject(user, self.name, self.description)
+ # TODO recover result checker
+ # gateway_result_checker(result, None)
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py
new file mode 100644
index 0000000..9d6664e
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py
@@ -0,0 +1,42 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler User object."""
+
+from typing import Optional
+
+from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core.base_side import BaseSide
+from pydolphinscheduler.java_gateway import gateway_result_checker, launch_gateway
+
+
+class Queue(BaseSide):
+ """DolphinScheduler Queue object."""
+
+ def __init__(
+ self,
+ name: str = ProcessDefinitionDefault.QUEUE,
+ description: Optional[str] = "",
+ ):
+ super().__init__(name, description)
+
+ def create_if_not_exists(self, user=ProcessDefinitionDefault.USER) -> None:
+ """Create Queue if not exists."""
+ gateway = launch_gateway()
+ # Here we set Queue.name and Queue.queueName same as self.name
+ result = gateway.entry_point.createProject(user, self.name, self.name)
+ gateway_result_checker(result, None)
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py
new file mode 100644
index 0000000..508c033
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py
@@ -0,0 +1,45 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler Tenant object."""
+
+from typing import Optional
+
+from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core.base_side import BaseSide
+from pydolphinscheduler.java_gateway import launch_gateway
+
+
+class Tenant(BaseSide):
+ """DolphinScheduler Tenant object."""
+
+ def __init__(
+ self,
+ name: str = ProcessDefinitionDefault.TENANT,
+ queue: str = ProcessDefinitionDefault.QUEUE,
+ description: Optional[str] = None,
+ ):
+ super().__init__(name, description)
+ self.queue = queue
+
+ def create_if_not_exists(
+ self, queue_name: str, user=ProcessDefinitionDefault.USER
+ ) -> None:
+ """Create Tenant if not exists."""
+ gateway = launch_gateway()
+ gateway.entry_point.createTenant(self.name, self.description, queue_name)
+ # gateway_result_checker(result, None)
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/user.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/user.py
new file mode 100644
index 0000000..cd0145a
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/user.py
@@ -0,0 +1,70 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler User object."""
+
+from typing import Optional
+
+from pydolphinscheduler.core.base_side import BaseSide
+from pydolphinscheduler.java_gateway import launch_gateway
+
+
+class User(BaseSide):
+ """DolphinScheduler User object."""
+
+ _KEY_ATTR = {
+ "name",
+ "password",
+ "email",
+ "phone",
+ "tenant",
+ "queue",
+ "status",
+ }
+
+ def __init__(
+ self,
+ name: str,
+ password: str,
+ email: str,
+ phone: str,
+ tenant: str,
+ queue: Optional[str] = None,
+ status: Optional[int] = 1,
+ ):
+ super().__init__(name)
+ self.password = password
+ self.email = email
+ self.phone = phone
+ self.tenant = tenant
+ self.queue = queue
+ self.status = status
+
+ def create_if_not_exists(self, **kwargs):
+ """Create User if not exists."""
+ gateway = launch_gateway()
+ gateway.entry_point.createUser(
+ self.name,
+ self.password,
+ self.email,
+ self.phone,
+ self.tenant,
+ self.queue,
+ self.status,
+ )
+ # TODO recover result checker
+ # gateway_result_checker(result, None)
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/worker_group.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/worker_group.py
new file mode 100644
index 0000000..ed50ec6
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/worker_group.py
@@ -0,0 +1,30 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""DolphinScheduler Worker Group object."""
+
+from typing import Optional
+
+from pydolphinscheduler.core.base_side import BaseSide
+
+
+class WorkerGroup(BaseSide):
+ """DolphinScheduler Worker Group object."""
+
+ def __init__(self, name: str, address: str, description: Optional[str] = None):
+ super().__init__(name, description)
+ self.address = address
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/__init__.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/__init__.py
new file mode 100644
index 0000000..9eded99
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init pydolphinscheduler.tasks package."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/condition.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/condition.py
new file mode 100644
index 0000000..2278f49
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/condition.py
@@ -0,0 +1,185 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task Conditions."""
+
+from typing import Dict, List
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.base import Base
+from pydolphinscheduler.core.task import Task
+from pydolphinscheduler.exceptions import PyDSParamException
+
+
+class Status(Base):
+ """Base class of Condition task status.
+
+ It a parent class for :class:`SUCCESS` and :class:`FAILURE`. Provider status name
+ and :func:`get_define` to sub class.
+ """
+
+ def __init__(self, *tasks):
+ super().__init__(f"Condition.{self.status_name()}")
+ self.tasks = tasks
+
+ def __repr__(self) -> str:
+ return "depend_item_list"
+
+ @classmethod
+ def status_name(cls) -> str:
+ """Get name for Status or its sub class."""
+ return cls.__name__.upper()
+
+ def get_define(self, camel_attr: bool = True) -> List:
+ """Get status definition attribute communicate to Java gateway server."""
+ content = []
+ for task in self.tasks:
+ if not isinstance(task, Task):
+ raise PyDSParamException(
+ "%s only accept class Task or sub class Task, but get %s",
+ (self.status_name(), type(task)),
+ )
+ content.append({"depTaskCode": task.code, "status": self.status_name()})
+ return content
+
+
+class SUCCESS(Status):
+ """Class SUCCESS to task condition, sub class of :class:`Status`."""
+
+ def __init__(self, *tasks):
+ super().__init__(*tasks)
+
+
+class FAILURE(Status):
+ """Class FAILURE to task condition, sub class of :class:`Status`."""
+
+ def __init__(self, *tasks):
+ super().__init__(*tasks)
+
+
+class ConditionOperator(Base):
+ """Set ConditionTask or ConditionOperator with specific operator."""
+
+ _DEFINE_ATTR = {
+ "relation",
+ }
+
+ def __init__(self, *args):
+ super().__init__(self.__class__.__name__)
+ self.args = args
+
+ def __repr__(self) -> str:
+ return "depend_task_list"
+
+ @classmethod
+ def operator_name(cls) -> str:
+ """Get operator name in different class."""
+ return cls.__name__.upper()
+
+ @property
+ def relation(self) -> str:
+ """Get operator name in different class, for function :func:`get_define`."""
+ return self.operator_name()
+
+ def set_define_attr(self) -> str:
+ """Set attribute to function :func:`get_define`.
+
+ It is a wrapper for both `And` and `Or` operator.
+ """
+ result = []
+ attr = None
+ for condition in self.args:
+ if isinstance(condition, (Status, ConditionOperator)):
+ if attr is None:
+ attr = repr(condition)
+ elif repr(condition) != attr:
+ raise PyDSParamException(
+ "Condition %s operator parameter only support same type.",
+ self.relation,
+ )
+ else:
+ raise PyDSParamException(
+ "Condition %s operator parameter support ConditionTask and ConditionOperator but got %s.",
+ (self.relation, type(condition)),
+ )
+ if attr == "depend_item_list":
+ result.extend(condition.get_define())
+ else:
+ result.append(condition.get_define())
+ setattr(self, attr, result)
+ return attr
+
+ def get_define(self, camel_attr=True) -> Dict:
+ """Overwrite Base.get_define to get task Condition specific get define."""
+ attr = self.set_define_attr()
+ dependent_define_attr = self._DEFINE_ATTR.union({attr})
+ return super().get_define_custom(
+ camel_attr=True, custom_attr=dependent_define_attr
+ )
+
+
+class And(ConditionOperator):
+ """Operator And for task condition.
+
+ It could accept both :class:`Task` and children of :class:`ConditionOperator`,
+ and set AND condition to those args.
+ """
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+
+class Or(ConditionOperator):
+ """Operator Or for task condition.
+
+ It could accept both :class:`Task` and children of :class:`ConditionOperator`,
+ and set OR condition to those args.
+ """
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+
+class Conditions(Task):
+ """Task condition object, declare behavior for condition task to dolphinscheduler."""
+
+ def __init__(self, name: str, condition: ConditionOperator, *args, **kwargs):
+ super().__init__(name, TaskType.CONDITIONS, *args, **kwargs)
+ self.condition = condition
+ # Set condition tasks as current task downstream
+ self._set_dep()
+
+ def _set_dep(self) -> None:
+ """Set downstream according to parameter `condition`."""
+ downstream = []
+ for cond in self.condition.args:
+ if isinstance(cond, ConditionOperator):
+ for status in cond.args:
+ downstream.extend(list(status.tasks))
+ self.set_downstream(downstream)
+
+ @property
+ def task_params(self, camel_attr: bool = True, custom_attr: set = None) -> Dict:
+ """Override Task.task_params for Condition task.
+
+ Condition task have some specials attribute `dependence`, and in most of the task
+ this attribute is None and use empty dict `{}` as default value. We do not use class
+ attribute `_task_custom_attr` due to avoid attribute cover.
+ """
+ params = super().task_params
+ params["dependence"] = self.condition.get_define()
+ return params
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/datax.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/datax.py
new file mode 100644
index 0000000..f881a67
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/datax.py
@@ -0,0 +1,121 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task datax."""
+
+from typing import Dict, List, Optional
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.database import Database
+from pydolphinscheduler.core.task import Task
+
+
+class CustomDataX(Task):
+ """Task CustomDatax object, declare behavior for custom DataX task to dolphinscheduler.
+
+ You provider json template for DataX, it can synchronize data according to the template you provided.
+ """
+
+ CUSTOM_CONFIG = 1
+
+ _task_custom_attr = {"custom_config", "json", "xms", "xmx"}
+
+ def __init__(
+ self,
+ name: str,
+ json: str,
+ xms: Optional[int] = 1,
+ xmx: Optional[int] = 1,
+ *args,
+ **kwargs
+ ):
+ super().__init__(name, TaskType.DATAX, *args, **kwargs)
+ self.custom_config = self.CUSTOM_CONFIG
+ self.json = json
+ self.xms = xms
+ self.xmx = xmx
+
+
+class DataX(Task):
+ """Task DataX object, declare behavior for DataX task to dolphinscheduler.
+
+ It should run database datax job in multiply sql link engine, such as:
+ - MySQL
+ - Oracle
+ - Postgresql
+ - SQLServer
+ You provider datasource_name and datatarget_name contain connection information, it decisions which
+ database type and database instance would synchronous data.
+ """
+
+ CUSTOM_CONFIG = 0
+
+ _task_custom_attr = {
+ "custom_config",
+ "sql",
+ "target_table",
+ "job_speed_byte",
+ "job_speed_record",
+ "pre_statements",
+ "post_statements",
+ "xms",
+ "xmx",
+ }
+
+ def __init__(
+ self,
+ name: str,
+ datasource_name: str,
+ datatarget_name: str,
+ sql: str,
+ target_table: str,
+ job_speed_byte: Optional[int] = 0,
+ job_speed_record: Optional[int] = 1000,
+ pre_statements: Optional[List[str]] = None,
+ post_statements: Optional[List[str]] = None,
+ xms: Optional[int] = 1,
+ xmx: Optional[int] = 1,
+ *args,
+ **kwargs
+ ):
+ super().__init__(name, TaskType.DATAX, *args, **kwargs)
+ self.sql = sql
+ self.custom_config = self.CUSTOM_CONFIG
+ self.datasource_name = datasource_name
+ self.datatarget_name = datatarget_name
+ self.target_table = target_table
+ self.job_speed_byte = job_speed_byte
+ self.job_speed_record = job_speed_record
+ self.pre_statements = pre_statements or []
+ self.post_statements = post_statements or []
+ self.xms = xms
+ self.xmx = xmx
+
+ @property
+ def task_params(self, camel_attr: bool = True, custom_attr: set = None) -> Dict:
+ """Override Task.task_params for datax task.
+
+ datax task have some specials attribute for task_params, and is odd if we
+ directly set as python property, so we Override Task.task_params here.
+ """
+ params = super().task_params
+ datasource = Database(self.datasource_name, "dsType", "dataSource")
+ params.update(datasource)
+
+ datatarget = Database(self.datatarget_name, "dtType", "dataTarget")
+ params.update(datatarget)
+ return params
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/dependent.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/dependent.py
new file mode 100644
index 0000000..7cff24c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/dependent.py
@@ -0,0 +1,274 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task dependent."""
+
+from typing import Dict, Optional, Tuple
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.base import Base
+from pydolphinscheduler.core.task import Task
+from pydolphinscheduler.exceptions import PyDSJavaGatewayException, PyDSParamException
+from pydolphinscheduler.java_gateway import launch_gateway
+
+DEPENDENT_ALL_TASK_IN_WORKFLOW = "0"
+
+
+class DependentDate(str):
+ """Constant of Dependent date value.
+
+ These values set according to Java server side, if you want to add and change it,
+ please change Java server side first.
+ """
+
+ # TODO Maybe we should add parent level to DependentDate for easy to use, such as
+ # DependentDate.MONTH.THIS_MONTH
+
+ # Hour
+ CURRENT_HOUR = "currentHour"
+ LAST_ONE_HOUR = "last1Hour"
+ LAST_TWO_HOURS = "last2Hours"
+ LAST_THREE_HOURS = "last3Hours"
+ LAST_TWENTY_FOUR_HOURS = "last24Hours"
+
+ # Day
+ TODAY = "today"
+ LAST_ONE_DAYS = "last1Days"
+ LAST_TWO_DAYS = "last2Days"
+ LAST_THREE_DAYS = "last3Days"
+ LAST_SEVEN_DAYS = "last7Days"
+
+ # Week
+ THIS_WEEK = "thisWeek"
+ LAST_WEEK = "lastWeek"
+ LAST_MONDAY = "lastMonday"
+ LAST_TUESDAY = "lastTuesday"
+ LAST_WEDNESDAY = "lastWednesday"
+ LAST_THURSDAY = "lastThursday"
+ LAST_FRIDAY = "lastFriday"
+ LAST_SATURDAY = "lastSaturday"
+ LAST_SUNDAY = "lastSunday"
+
+ # Month
+ THIS_MONTH = "thisMonth"
+ LAST_MONTH = "lastMonth"
+ LAST_MONTH_BEGIN = "lastMonthBegin"
+ LAST_MONTH_END = "lastMonthEnd"
+
+
+class DependentItem(Base):
+ """Dependent item object, minimal unit for task dependent.
+
+ It declare which project, process_definition, task are dependent to this task.
+ """
+
+ _DEFINE_ATTR = {
+ "project_code",
+ "definition_code",
+ "dep_task_code",
+ "cycle",
+ "date_value",
+ }
+
+ # TODO maybe we should conside overwrite operator `and` and `or` for DependentItem to
+ # support more easy way to set relation
+ def __init__(
+ self,
+ project_name: str,
+ process_definition_name: str,
+ dependent_task_name: Optional[str] = DEPENDENT_ALL_TASK_IN_WORKFLOW,
+ dependent_date: Optional[DependentDate] = DependentDate.TODAY,
+ ):
+ obj_name = f"{project_name}.{process_definition_name}.{dependent_task_name}.{dependent_date}"
+ super().__init__(obj_name)
+ self.project_name = project_name
+ self.process_definition_name = process_definition_name
+ self.dependent_task_name = dependent_task_name
+ if dependent_date is None:
+ raise PyDSParamException(
+ "Parameter dependent_date must provider by got None."
+ )
+ else:
+ self.dependent_date = dependent_date
+ self._code = {}
+
+ def __repr__(self) -> str:
+ return "depend_item_list"
+
+ @property
+ def project_code(self) -> str:
+ """Get dependent project code."""
+ return self.get_code_from_gateway().get("projectCode")
+
+ @property
+ def definition_code(self) -> str:
+ """Get dependent definition code."""
+ return self.get_code_from_gateway().get("processDefinitionCode")
+
+ @property
+ def dep_task_code(self) -> str:
+ """Get dependent tasks code list."""
+ if self.is_all_task:
+ return DEPENDENT_ALL_TASK_IN_WORKFLOW
+ else:
+ return self.get_code_from_gateway().get("taskDefinitionCode")
+
+ # TODO Maybe we should get cycle from dependent date class.
+ @property
+ def cycle(self) -> str:
+ """Get dependent cycle."""
+ if "Hour" in self.dependent_date:
+ return "hour"
+ elif self.dependent_date == "today" or "Days" in self.dependent_date:
+ return "day"
+ elif "Month" in self.dependent_date:
+ return "month"
+ else:
+ return "week"
+
+ @property
+ def date_value(self) -> str:
+ """Get dependent date."""
+ return self.dependent_date
+
+ @property
+ def is_all_task(self) -> bool:
+ """Check whether dependent all tasks or not."""
+ return self.dependent_task_name == DEPENDENT_ALL_TASK_IN_WORKFLOW
+
+ @property
+ def code_parameter(self) -> Tuple:
+ """Get name info parameter to query code."""
+ param = (
+ self.project_name,
+ self.process_definition_name,
+ self.dependent_task_name if not self.is_all_task else None,
+ )
+ return param
+
+ def get_code_from_gateway(self) -> Dict:
+ """Get project, definition, task code from given parameter."""
+ if self._code:
+ return self._code
+ else:
+ gateway = launch_gateway()
+ try:
+ self._code = gateway.entry_point.getDependentInfo(*self.code_parameter)
+ return self._code
+ except Exception:
+ raise PyDSJavaGatewayException("Function get_code_from_gateway error.")
+
+
+class DependentOperator(Base):
+ """Set DependentItem or dependItemList with specific operator."""
+
+ _DEFINE_ATTR = {
+ "relation",
+ }
+
+ def __init__(self, *args):
+ super().__init__(self.__class__.__name__)
+ self.args = args
+
+ def __repr__(self) -> str:
+ return "depend_task_list"
+
+ @classmethod
+ def operator_name(cls) -> str:
+ """Get operator name in different class."""
+ return cls.__name__.upper()
+
+ @property
+ def relation(self) -> str:
+ """Get operator name in different class, for function :func:`get_define`."""
+ return self.operator_name()
+
+ def set_define_attr(self) -> str:
+ """Set attribute to function :func:`get_define`.
+
+ It is a wrapper for both `And` and `Or` operator.
+ """
+ result = []
+ attr = None
+ for dependent in self.args:
+ if isinstance(dependent, (DependentItem, DependentOperator)):
+ if attr is None:
+ attr = repr(dependent)
+ elif repr(dependent) != attr:
+ raise PyDSParamException(
+ "Dependent %s operator parameter only support same type.",
+ self.relation,
+ )
+ else:
+ raise PyDSParamException(
+ "Dependent %s operator parameter support DependentItem and "
+ "DependentOperator but got %s.",
+ (self.relation, type(dependent)),
+ )
+ result.append(dependent.get_define())
+ setattr(self, attr, result)
+ return attr
+
+ def get_define(self, camel_attr=True) -> Dict:
+ """Overwrite Base.get_define to get task dependent specific get define."""
+ attr = self.set_define_attr()
+ dependent_define_attr = self._DEFINE_ATTR.union({attr})
+ return super().get_define_custom(
+ camel_attr=True, custom_attr=dependent_define_attr
+ )
+
+
+class And(DependentOperator):
+ """Operator And for task dependent.
+
+ It could accept both :class:`DependentItem` and children of :class:`DependentOperator`,
+ and set AND condition to those args.
+ """
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+
+class Or(DependentOperator):
+ """Operator Or for task dependent.
+
+ It could accept both :class:`DependentItem` and children of :class:`DependentOperator`,
+ and set OR condition to those args.
+ """
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+
+class Dependent(Task):
+ """Task dependent object, declare behavior for dependent task to dolphinscheduler."""
+
+ def __init__(self, name: str, dependence: DependentOperator, *args, **kwargs):
+ super().__init__(name, TaskType.DEPENDENT, *args, **kwargs)
+ self.dependence = dependence
+
+ @property
+ def task_params(self, camel_attr: bool = True, custom_attr: set = None) -> Dict:
+ """Override Task.task_params for dependent task.
+
+ Dependent task have some specials attribute `dependence`, and in most of the task
+ this attribute is None and use empty dict `{}` as default value. We do not use class
+ attribute `_task_custom_attr` due to avoid attribute cover.
+ """
+ params = super().task_params
+ params["dependence"] = self.dependence.get_define()
+ return params
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/http.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/http.py
new file mode 100644
index 0000000..781333d
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/http.py
@@ -0,0 +1,101 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task shell."""
+
+from typing import Optional
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.task import Task
+from pydolphinscheduler.exceptions import PyDSParamException
+
+
+class HttpMethod:
+ """Constant of HTTP method."""
+
+ GET = "GET"
+ POST = "POST"
+ HEAD = "HEAD"
+ PUT = "PUT"
+ DELETE = "DELETE"
+
+
+class HttpCheckCondition:
+ """Constant of HTTP check condition.
+
+ For now it contain four value:
+ - STATUS_CODE_DEFAULT: when http response code equal to 200, mark as success.
+ - STATUS_CODE_CUSTOM: when http response code equal to the code user define, mark as success.
+ - BODY_CONTAINS: when http response body contain text user define, mark as success.
+ - BODY_NOT_CONTAINS: when http response body do not contain text user define, mark as success.
+ """
+
+ STATUS_CODE_DEFAULT = "STATUS_CODE_DEFAULT"
+ STATUS_CODE_CUSTOM = "STATUS_CODE_CUSTOM"
+ BODY_CONTAINS = "BODY_CONTAINS"
+ BODY_NOT_CONTAINS = "BODY_NOT_CONTAINS"
+
+
+class Http(Task):
+ """Task HTTP object, declare behavior for HTTP task to dolphinscheduler."""
+
+ _task_custom_attr = {
+ "url",
+ "http_method",
+ "http_params",
+ "http_check_condition",
+ "condition",
+ "connect_timeout",
+ "socket_timeout",
+ }
+
+ def __init__(
+ self,
+ name: str,
+ url: str,
+ http_method: Optional[str] = HttpMethod.GET,
+ http_params: Optional[str] = None,
+ http_check_condition: Optional[str] = HttpCheckCondition.STATUS_CODE_DEFAULT,
+ condition: Optional[str] = None,
+ connect_timeout: Optional[int] = 60000,
+ socket_timeout: Optional[int] = 60000,
+ *args,
+ **kwargs
+ ):
+ super().__init__(name, TaskType.HTTP, *args, **kwargs)
+ self.url = url
+ if not hasattr(HttpMethod, http_method):
+ raise PyDSParamException(
+ "Parameter http_method %s not support.", http_method
+ )
+ self.http_method = http_method
+ self.http_params = http_params or []
+ if not hasattr(HttpCheckCondition, http_check_condition):
+ raise PyDSParamException(
+ "Parameter http_check_condition %s not support.", http_check_condition
+ )
+ self.http_check_condition = http_check_condition
+ if (
+ http_check_condition != HttpCheckCondition.STATUS_CODE_DEFAULT
+ and condition is None
+ ):
+ raise PyDSParamException(
+ "Parameter condition must provider if http_check_condition not equal to STATUS_CODE_DEFAULT"
+ )
+ self.condition = condition
+ self.connect_timeout = connect_timeout
+ self.socket_timeout = socket_timeout
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/procedure.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/procedure.py
new file mode 100644
index 0000000..6383e07
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/procedure.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task procedure."""
+
+from typing import Dict
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.database import Database
+from pydolphinscheduler.core.task import Task
+
+
+class Procedure(Task):
+ """Task Procedure object, declare behavior for Procedure task to dolphinscheduler.
+
+ It should run database procedure job in multiply sql lik engine, such as:
+ - ClickHouse
+ - DB2
+ - HIVE
+ - MySQL
+ - Oracle
+ - Postgresql
+ - Presto
+ - SQLServer
+ You provider datasource_name contain connection information, it decisions which
+ database type and database instance would run this sql.
+ """
+
+ _task_custom_attr = {"method"}
+
+ def __init__(self, name: str, datasource_name: str, method: str, *args, **kwargs):
+ super().__init__(name, TaskType.PROCEDURE, *args, **kwargs)
+ self.datasource_name = datasource_name
+ self.method = method
+
+ @property
+ def task_params(self, camel_attr: bool = True, custom_attr: set = None) -> Dict:
+ """Override Task.task_params for produce task.
+
+ produce task have some specials attribute for task_params, and is odd if we
+ directly set as python property, so we Override Task.task_params here.
+ """
+ params = super().task_params
+ datasource = Database(self.datasource_name, "type", "datasource")
+ params.update(datasource)
+ return params
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/python.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/python.py
new file mode 100644
index 0000000..7950480
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/python.py
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task Python."""
+
+import inspect
+import types
+from typing import Any
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.task import Task
+from pydolphinscheduler.exceptions import PyDSParamException
+
+
+class Python(Task):
+ """Task Python object, declare behavior for Python task to dolphinscheduler."""
+
+ _task_custom_attr = {
+ "raw_script",
+ }
+
+ def __init__(self, name: str, code: Any, *args, **kwargs):
+ super().__init__(name, TaskType.PYTHON, *args, **kwargs)
+ self._code = code
+
+ @property
+ def raw_script(self) -> str:
+ """Get python task define attribute `raw_script`."""
+ if isinstance(self._code, str):
+ return self._code
+ elif isinstance(self._code, types.FunctionType):
+ py_function = inspect.getsource(self._code)
+ return py_function
+ else:
+ raise PyDSParamException(
+ "Parameter code do not support % for now.", type(self._code)
+ )
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/shell.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/shell.py
new file mode 100644
index 0000000..94ad2f4
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/shell.py
@@ -0,0 +1,38 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task shell."""
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.task import Task
+
+
+class Shell(Task):
+ """Task shell object, declare behavior for shell task to dolphinscheduler.
+
+ TODO maybe we could use instance name to replace attribute `name`
+ which is simplify as `task_shell = Shell(command = "echo 1")` and
+ task.name assign to `task_shell`
+ """
+
+ _task_custom_attr = {
+ "raw_script",
+ }
+
+ def __init__(self, name: str, command: str, *args, **kwargs):
+ super().__init__(name, TaskType.SHELL, *args, **kwargs)
+ self.raw_script = command
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/sql.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/sql.py
new file mode 100644
index 0000000..b5be3e4
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/sql.py
@@ -0,0 +1,99 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task sql."""
+
+import re
+from typing import Dict, Optional
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.database import Database
+from pydolphinscheduler.core.task import Task
+
+
+class SqlType:
+ """SQL type, for now it just contain `SELECT` and `NO_SELECT`."""
+
+ SELECT = 0
+ NOT_SELECT = 1
+
+
+class Sql(Task):
+ """Task SQL object, declare behavior for SQL task to dolphinscheduler.
+
+ It should run sql job in multiply sql lik engine, such as:
+ - ClickHouse
+ - DB2
+ - HIVE
+ - MySQL
+ - Oracle
+ - Postgresql
+ - Presto
+ - SQLServer
+ You provider datasource_name contain connection information, it decisions which
+ database type and database instance would run this sql.
+ """
+
+ _task_custom_attr = {
+ "sql",
+ "sql_type",
+ "pre_statements",
+ "post_statements",
+ "display_rows",
+ }
+
+ def __init__(
+ self,
+ name: str,
+ datasource_name: str,
+ sql: str,
+ pre_statements: Optional[str] = None,
+ post_statements: Optional[str] = None,
+ display_rows: Optional[int] = 10,
+ *args,
+ **kwargs
+ ):
+ super().__init__(name, TaskType.SQL, *args, **kwargs)
+ self.sql = sql
+ self.datasource_name = datasource_name
+ self.pre_statements = pre_statements or []
+ self.post_statements = post_statements or []
+ self.display_rows = display_rows
+
+ @property
+ def sql_type(self) -> int:
+ """Judgement sql type, use regexp to check which type of the sql is."""
+ pattern_select_str = (
+ "^(?!(.* |)insert |(.* |)delete |(.* |)drop |(.* |)update |(.* |)alter ).*"
+ )
+ pattern_select = re.compile(pattern_select_str, re.IGNORECASE)
+ if pattern_select.match(self.sql) is None:
+ return SqlType.NOT_SELECT
+ else:
+ return SqlType.SELECT
+
+ @property
+ def task_params(self, camel_attr: bool = True, custom_attr: set = None) -> Dict:
+ """Override Task.task_params for sql task.
+
+ sql task have some specials attribute for task_params, and is odd if we
+ directly set as python property, so we Override Task.task_params here.
+ """
+ params = super().task_params
+ datasource = Database(self.datasource_name, "type", "datasource")
+ params.update(datasource)
+ return params
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/sub_process.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/sub_process.py
new file mode 100644
index 0000000..8ba6b4c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/sub_process.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task sub_process."""
+
+from typing import Dict
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.task import Task
+from pydolphinscheduler.exceptions import PyDSProcessDefinitionNotAssignException
+from pydolphinscheduler.java_gateway import launch_gateway
+
+
+class SubProcess(Task):
+ """Task SubProcess object, declare behavior for SubProcess task to dolphinscheduler."""
+
+ _task_custom_attr = {"process_definition_code"}
+
+ def __init__(self, name: str, process_definition_name: str, *args, **kwargs):
+ super().__init__(name, TaskType.SUB_PROCESS, *args, **kwargs)
+ self.process_definition_name = process_definition_name
+
+ @property
+ def process_definition_code(self) -> str:
+ """Get process definition code, a wrapper for :func:`get_process_definition_info`."""
+ return self.get_process_definition_info(self.process_definition_name).get(
+ "code"
+ )
+
+ def get_process_definition_info(self, process_definition_name: str) -> Dict:
+ """Get process definition info from java gateway, contains process definition id, name, code."""
+ if not self.process_definition:
+ raise PyDSProcessDefinitionNotAssignException(
+ "ProcessDefinition must be provider for task SubProcess."
+ )
+ gateway = launch_gateway()
+ return gateway.entry_point.getProcessDefinitionInfo(
+ self.process_definition.user.name,
+ self.process_definition.project.name,
+ process_definition_name,
+ )
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/switch.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/switch.py
new file mode 100644
index 0000000..28032f8
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/tasks/switch.py
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Task Switch."""
+
+from typing import Dict, Optional
+
+from pydolphinscheduler.constants import TaskType
+from pydolphinscheduler.core.base import Base
+from pydolphinscheduler.core.task import Task
+from pydolphinscheduler.exceptions import PyDSParamException
+
+
+class SwitchBranch(Base):
+ """Base class of ConditionBranch of task switch.
+
+ It a parent class for :class:`Branch` and :class:`Default`.
+ """
+
+ _DEFINE_ATTR = {
+ "next_node",
+ }
+
+ def __init__(self, task: Task, exp: Optional[str] = None):
+ super().__init__(f"Switch.{self.__class__.__name__.upper()}")
+ self.task = task
+ self.exp = exp
+
+ @property
+ def next_node(self) -> str:
+ """Get task switch property next_node, it return task code when init class switch."""
+ return self.task.code
+
+ @property
+ def condition(self) -> Optional[str]:
+ """Get task switch property condition."""
+ return self.exp
+
+ def get_define(self, camel_attr: bool = True) -> Dict:
+ """Get :class:`ConditionBranch` definition attribute communicate to Java gateway server."""
+ if self.condition:
+ self._DEFINE_ATTR.add("condition")
+ return super().get_define()
+
+
+class Branch(SwitchBranch):
+ """Common condition branch for switch task.
+
+ If any condition in :class:`Branch` match, would set this :class:`Branch`'s task as downstream of task
+ switch. If all condition branch do not match would set :class:`Default`'s task as task switch downstream.
+ """
+
+ def __init__(self, condition: str, task: Task):
+ super().__init__(task, condition)
+
+
+class Default(SwitchBranch):
+ """Class default branch for switch task.
+
+ If all condition of :class:`Branch` do not match, task switch would run the tasks in :class:`Default`
+ and set :class:`Default`'s task as switch downstream. Please notice that each switch condition
+ could only have one single :class:`Default`.
+ """
+
+ def __init__(self, task: Task):
+ super().__init__(task)
+
+
+class SwitchCondition(Base):
+ """Set switch condition of given parameter."""
+
+ _DEFINE_ATTR = {
+ "depend_task_list",
+ }
+
+ def __init__(self, *args):
+ super().__init__(self.__class__.__name__)
+ self.args = args
+
+ def set_define_attr(self) -> None:
+ """Set attribute to function :func:`get_define`.
+
+ It is a wrapper for both `And` and `Or` operator.
+ """
+ result = []
+ num_branch_default = 0
+ for condition in self.args:
+ if isinstance(condition, SwitchBranch):
+ if num_branch_default < 1:
+ if isinstance(condition, Default):
+ self._DEFINE_ATTR.add("next_node")
+ setattr(self, "next_node", condition.next_node)
+ num_branch_default += 1
+ elif isinstance(condition, Branch):
+ result.append(condition.get_define())
+ else:
+ raise PyDSParamException(
+ "Task Switch's parameter only support exactly one default branch."
+ )
+ else:
+ raise PyDSParamException(
+ "Task Switch's parameter only support SwitchBranch but got %s.",
+ type(condition),
+ )
+ # Handle switch default branch, default value is `""` if not provide.
+ if num_branch_default == 0:
+ self._DEFINE_ATTR.add("next_node")
+ setattr(self, "next_node", "")
+ setattr(self, "depend_task_list", result)
+
+ def get_define(self, camel_attr=True) -> Dict:
+ """Overwrite Base.get_define to get task Condition specific get define."""
+ self.set_define_attr()
+ return super().get_define()
+
+
+class Switch(Task):
+ """Task switch object, declare behavior for switch task to dolphinscheduler."""
+
+ def __init__(self, name: str, condition: SwitchCondition, *args, **kwargs):
+ super().__init__(name, TaskType.SWITCH, *args, **kwargs)
+ self.condition = condition
+ # Set condition tasks as current task downstream
+ self._set_dep()
+
+ def _set_dep(self) -> None:
+ """Set downstream according to parameter `condition`."""
+ downstream = []
+ for condition in self.condition.args:
+ if isinstance(condition, SwitchBranch):
+ downstream.append(condition.task)
+ self.set_downstream(downstream)
+
+ @property
+ def task_params(self, camel_attr: bool = True, custom_attr: set = None) -> Dict:
+ """Override Task.task_params for switch task.
+
+ switch task have some specials attribute `switch`, and in most of the task
+ this attribute is None and use empty dict `{}` as default value. We do not use class
+ attribute `_task_custom_attr` due to avoid attribute cover.
+ """
+ params = super().task_params
+ params["switchResult"] = self.condition.get_define()
+ return params
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/__init__.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/__init__.py
new file mode 100644
index 0000000..f8d3fbf
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init utils package."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/date.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/date.py
new file mode 100644
index 0000000..18cf93e
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/date.py
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Date util function collections."""
+
+from datetime import datetime
+
+from pydolphinscheduler.constants import Delimiter, Time
+
+LEN_SUPPORT_DATETIME = (
+ 15,
+ 19,
+)
+
+FMT_SHORT = f"{Time.FMT_SHORT_DATE} {Time.FMT_NO_COLON_TIME}"
+FMT_DASH = f"{Time.FMT_DASH_DATE} {Time.FMT_STD_TIME}"
+FMT_STD = f"{Time.FMT_STD_DATE} {Time.FMT_STD_TIME}"
+
+MAX_DATETIME = datetime(9999, 12, 31, 23, 59, 59)
+
+
+def conv_to_schedule(src: datetime) -> str:
+ """Convert given datetime to schedule date string."""
+ return datetime.strftime(src, FMT_STD)
+
+
+def conv_from_str(src: str) -> datetime:
+ """Convert given string to datetime.
+
+ This function give an ability to convert string to datetime, and for now it could handle
+ format like:
+ - %Y-%m-%d
+ - %Y/%m/%d
+ - %Y%m%d
+ - %Y-%m-%d %H:%M:%S
+ - %Y/%m/%d %H:%M:%S
+ - %Y%m%d %H%M%S
+ If pattern not like above be given will raise NotImplementedError.
+ """
+ len_ = len(src)
+ if len_ == Time.LEN_SHORT_DATE:
+ return datetime.strptime(src, Time.FMT_SHORT_DATE)
+ elif len_ == Time.LEN_STD_DATE:
+ if Delimiter.BAR in src:
+ return datetime.strptime(src, Time.FMT_STD_DATE)
+ elif Delimiter.DASH in src:
+ return datetime.strptime(src, Time.FMT_DASH_DATE)
+ else:
+ raise NotImplementedError(
+ "%s could not be convert to datetime for now.", src
+ )
+ elif len_ in LEN_SUPPORT_DATETIME:
+ if Delimiter.BAR in src and Delimiter.COLON in src:
+ return datetime.strptime(src, FMT_STD)
+ elif Delimiter.DASH in src and Delimiter.COLON in src:
+ return datetime.strptime(src, FMT_DASH)
+ elif (
+ Delimiter.DASH not in src
+ and Delimiter.BAR not in src
+ and Delimiter.COLON not in src
+ ):
+ return datetime.strptime(src, FMT_SHORT)
+ else:
+ raise NotImplementedError(
+ "%s could not be convert to datetime for now.", src
+ )
+ else:
+ raise NotImplementedError("%s could not be convert to datetime for now.", src)
diff --git a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/string.py b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/string.py
new file mode 100644
index 0000000..e7e781c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/string.py
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""String util function collections."""
+
+from pydolphinscheduler.constants import Delimiter
+
+
+def attr2camel(attr: str, include_private=True):
+ """Covert class attribute name to camel case."""
+ if include_private:
+ attr = attr.lstrip(Delimiter.UNDERSCORE)
+ return snake2camel(attr)
+
+
+def snake2camel(snake: str):
+ """Covert snake case to camel case."""
+ components = snake.split(Delimiter.UNDERSCORE)
+ return components[0] + "".join(x.title() for x in components[1:])
+
+
+def class_name2camel(class_name: str):
+ """Covert class name string to camel case."""
+ class_name = class_name.lstrip(Delimiter.UNDERSCORE)
+ return class_name[0].lower() + snake2camel(class_name[1:])
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/__init__.py b/dolphinscheduler-python/pydolphinscheduler/tests/__init__.py
new file mode 100644
index 0000000..5ce1f82
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init tests package."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/core/__init__.py b/dolphinscheduler-python/pydolphinscheduler/tests/core/__init__.py
new file mode 100644
index 0000000..62ce0ea
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/core/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init core package tests."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_database.py b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_database.py
new file mode 100644
index 0000000..1286a4a
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_database.py
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Database."""
+
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.core.database import Database
+
+TEST_DATABASE_DATASOURCE_NAME = "test_datasource"
+TEST_DATABASE_TYPE_KEY = "type"
+TEST_DATABASE_KEY = "datasource"
+
+
+@pytest.mark.parametrize(
+ "expect",
+ [
+ {
+ TEST_DATABASE_TYPE_KEY: "mock_type",
+ TEST_DATABASE_KEY: 1,
+ }
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "mock_type"}),
+)
+def test_get_datasource_detail(mock_datasource, mock_code_version, expect):
+ """Test :func:`get_database_type` and :func:`get_database_id` can return expect value."""
+ database_info = Database(
+ TEST_DATABASE_DATASOURCE_NAME, TEST_DATABASE_TYPE_KEY, TEST_DATABASE_KEY
+ )
+ assert expect == database_info
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py
new file mode 100644
index 0000000..8491878
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py
@@ -0,0 +1,342 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test process definition."""
+
+from datetime import datetime
+from typing import Any
+
+import pytest
+from freezegun import freeze_time
+
+from pydolphinscheduler.constants import (
+ ProcessDefinitionDefault,
+ ProcessDefinitionReleaseState,
+)
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.exceptions import PyDSParamException
+from pydolphinscheduler.side import Project, Tenant, User
+from pydolphinscheduler.utils.date import conv_to_schedule
+from tests.testing.task import Task
+
+TEST_PROCESS_DEFINITION_NAME = "simple-test-process-definition"
+
+
+@pytest.mark.parametrize("func", ["run", "submit", "start"])
+def test_process_definition_key_attr(func):
+ """Test process definition have specific functions or attributes."""
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ assert hasattr(
+ pd, func
+ ), f"ProcessDefinition instance don't have attribute `{func}`"
+
+
+@pytest.mark.parametrize(
+ "name,value",
+ [
+ ("timezone", ProcessDefinitionDefault.TIME_ZONE),
+ ("project", Project(ProcessDefinitionDefault.PROJECT)),
+ ("tenant", Tenant(ProcessDefinitionDefault.TENANT)),
+ (
+ "user",
+ User(
+ ProcessDefinitionDefault.USER,
+ ProcessDefinitionDefault.USER_PWD,
+ ProcessDefinitionDefault.USER_EMAIL,
+ ProcessDefinitionDefault.USER_PHONE,
+ ProcessDefinitionDefault.TENANT,
+ ProcessDefinitionDefault.QUEUE,
+ ProcessDefinitionDefault.USER_STATE,
+ ),
+ ),
+ ("worker_group", ProcessDefinitionDefault.WORKER_GROUP),
+ ("release_state", ProcessDefinitionReleaseState.ONLINE),
+ ],
+)
+def test_process_definition_default_value(name, value):
+ """Test process definition default attributes."""
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ assert getattr(pd, name) == value, (
+ f"ProcessDefinition instance attribute `{name}` not with "
+ f"except default value `{getattr(pd, name)}`"
+ )
+
+
+@pytest.mark.parametrize(
+ "name,cls,expect",
+ [
+ ("name", str, "name"),
+ ("description", str, "description"),
+ ("schedule", str, "schedule"),
+ ("timezone", str, "timezone"),
+ ("worker_group", str, "worker_group"),
+ ("timeout", int, 1),
+ ("release_state", str, "OFFLINE"),
+ ("param", dict, {"key": "value"}),
+ ],
+)
+def test_set_attr(name, cls, expect):
+ """Test process definition set attributes which get with same type."""
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ setattr(pd, name, expect)
+ assert (
+ getattr(pd, name) == expect
+ ), f"ProcessDefinition set attribute `{name}` do not work expect"
+
+
+@pytest.mark.parametrize(
+ "set_attr,set_val,get_attr,get_val",
+ [
+ ("_project", "project", "project", Project("project")),
+ ("_tenant", "tenant", "tenant", Tenant("tenant")),
+ ("_start_time", "2021-01-01", "start_time", datetime(2021, 1, 1)),
+ ("_end_time", "2021-01-01", "end_time", datetime(2021, 1, 1)),
+ ],
+)
+def test_set_attr_return_special_object(set_attr, set_val, get_attr, get_val):
+ """Test process definition set attributes which get with different type."""
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ setattr(pd, set_attr, set_val)
+ assert get_val == getattr(
+ pd, get_attr
+ ), f"Set attribute {set_attr} can not get back with {get_val}."
+
+
+@pytest.mark.parametrize(
+ "val,expect",
+ [
+ (datetime(2021, 1, 1), datetime(2021, 1, 1)),
+ (None, None),
+ ("2021-01-01", datetime(2021, 1, 1)),
+ ("2021-01-01 01:01:01", datetime(2021, 1, 1, 1, 1, 1)),
+ ],
+)
+def test__parse_datetime(val, expect):
+ """Test process definition function _parse_datetime.
+
+ Only two datetime test cases here because we have more test cases in tests/utils/test_date.py file.
+ """
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ assert expect == pd._parse_datetime(
+ val
+ ), f"Function _parse_datetime with unexpect value by {val}."
+
+
+@pytest.mark.parametrize(
+ "val",
+ [
+ 20210101,
+ (2021, 1, 1),
+ {"year": "2021", "month": "1", "day": 1},
+ ],
+)
+def test__parse_datetime_not_support_type(val: Any):
+ """Test process definition function _parse_datetime not support type error."""
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ with pytest.raises(PyDSParamException, match="Do not support value type.*?"):
+ pd._parse_datetime(val)
+
+
+def test_process_definition_get_define_without_task():
+ """Test process definition function get_define without task."""
+ expect = {
+ "name": TEST_PROCESS_DEFINITION_NAME,
+ "description": None,
+ "project": ProcessDefinitionDefault.PROJECT,
+ "tenant": ProcessDefinitionDefault.TENANT,
+ "workerGroup": ProcessDefinitionDefault.WORKER_GROUP,
+ "timeout": 0,
+ "releaseState": ProcessDefinitionReleaseState.ONLINE,
+ "param": None,
+ "tasks": {},
+ "taskDefinitionJson": [{}],
+ "taskRelationJson": [{}],
+ }
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ assert pd.get_define() == expect
+
+
+def test_process_definition_simple_context_manager():
+ """Test simple create workflow in process definition context manager mode."""
+ expect_tasks_num = 5
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
+ for i in range(expect_tasks_num):
+ curr_task = Task(name=f"task-{i}", task_type=f"type-{i}")
+ # Set deps task i as i-1 parent
+ if i > 0:
+ pre_task = pd.get_one_task_by_name(f"task-{i - 1}")
+ curr_task.set_upstream(pre_task)
+ assert len(pd.tasks) == expect_tasks_num
+
+ # Test if task process_definition same as origin one
+ task: Task = pd.get_one_task_by_name("task-0")
+ assert pd is task.process_definition
+
+ # Test if all tasks with expect deps
+ for i in range(expect_tasks_num):
+ task: Task = pd.get_one_task_by_name(f"task-{i}")
+ if i == 0:
+ assert task._upstream_task_codes == set()
+ assert task._downstream_task_codes == {
+ pd.get_one_task_by_name("task-1").code
+ }
+ elif i == expect_tasks_num - 1:
+ assert task._upstream_task_codes == {
+ pd.get_one_task_by_name(f"task-{i - 1}").code
+ }
+ assert task._downstream_task_codes == set()
+ else:
+ assert task._upstream_task_codes == {
+ pd.get_one_task_by_name(f"task-{i - 1}").code
+ }
+ assert task._downstream_task_codes == {
+ pd.get_one_task_by_name(f"task-{i + 1}").code
+ }
+
+
+def test_process_definition_simple_separate():
+ """Test process definition simple create workflow in separate mode.
+
+ This test just test basic information, cause most of test case is duplicate to
+ test_process_definition_simple_context_manager.
+ """
+ expect_tasks_num = 5
+ pd = ProcessDefinition(TEST_PROCESS_DEFINITION_NAME)
+ for i in range(expect_tasks_num):
+ curr_task = Task(
+ name=f"task-{i}",
+ task_type=f"type-{i}",
+ process_definition=pd,
+ )
+ # Set deps task i as i-1 parent
+ if i > 0:
+ pre_task = pd.get_one_task_by_name(f"task-{i - 1}")
+ curr_task.set_upstream(pre_task)
+ assert len(pd.tasks) == expect_tasks_num
+ assert all(["task-" in task.name for task in pd.task_list])
+
+
+@pytest.mark.parametrize(
+ "user_attrs",
+ [
+ {"tenant": "tenant_specific"},
+ {"queue": "queue_specific"},
+ {"tenant": "tenant_specific", "queue": "queue_specific"},
+ ],
+)
+def test_set_process_definition_user_attr(user_attrs):
+ """Test user with correct attributes if we specific assigned to process definition object."""
+ default_value = {
+ "tenant": ProcessDefinitionDefault.TENANT,
+ "queue": ProcessDefinitionDefault.QUEUE,
+ }
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME, **user_attrs) as pd:
+ user = pd.user
+ for attr in default_value:
+ # Get assigned attribute if we specific, else get default value
+ except_attr = (
+ user_attrs[attr] if attr in user_attrs else default_value[attr]
+ )
+ # Get actually attribute of user object
+ actual_attr = getattr(user, attr)
+ assert (
+ except_attr == actual_attr
+ ), f"Except attribute is {except_attr} but get {actual_attr}"
+
+
+def test_schedule_json_none_schedule():
+ """Test function schedule_json with None as schedule."""
+ with ProcessDefinition(
+ TEST_PROCESS_DEFINITION_NAME,
+ schedule=None,
+ ) as pd:
+ assert pd.schedule_json is None
+
+
+# We freeze time here, because we test start_time with None, and if will get datetime.datetime.now. If we do
+# not freeze time, it will cause flaky test here.
+@freeze_time("2021-01-01")
+@pytest.mark.parametrize(
+ "start_time,end_time,expect_date",
+ [
+ (
+ "20210101",
+ "20210201",
+ {"start_time": "2021-01-01 00:00:00", "end_time": "2021-02-01 00:00:00"},
+ ),
+ (
+ "2021-01-01",
+ "2021-02-01",
+ {"start_time": "2021-01-01 00:00:00", "end_time": "2021-02-01 00:00:00"},
+ ),
+ (
+ "2021/01/01",
+ "2021/02/01",
+ {"start_time": "2021-01-01 00:00:00", "end_time": "2021-02-01 00:00:00"},
+ ),
+ # Test mix pattern
+ (
+ "2021/01/01 01:01:01",
+ "2021-02-02 02:02:02",
+ {"start_time": "2021-01-01 01:01:01", "end_time": "2021-02-02 02:02:02"},
+ ),
+ (
+ "2021/01/01 01:01:01",
+ "20210202 020202",
+ {"start_time": "2021-01-01 01:01:01", "end_time": "2021-02-02 02:02:02"},
+ ),
+ (
+ "20210101 010101",
+ "2021-02-02 02:02:02",
+ {"start_time": "2021-01-01 01:01:01", "end_time": "2021-02-02 02:02:02"},
+ ),
+ # Test None value
+ (
+ "2021/01/01 01:02:03",
+ None,
+ {"start_time": "2021-01-01 01:02:03", "end_time": "9999-12-31 23:59:59"},
+ ),
+ (
+ None,
+ None,
+ {
+ "start_time": conv_to_schedule(datetime(2021, 1, 1)),
+ "end_time": "9999-12-31 23:59:59",
+ },
+ ),
+ ],
+)
+def test_schedule_json_start_and_end_time(start_time, end_time, expect_date):
+ """Test function schedule_json about handle start_time and end_time.
+
+ Only two datetime test cases here because we have more test cases in tests/utils/test_date.py file.
+ """
+ schedule = "0 0 0 * * ? *"
+ expect = {
+ "crontab": schedule,
+ "startTime": expect_date["start_time"],
+ "endTime": expect_date["end_time"],
+ "timezoneId": ProcessDefinitionDefault.TIME_ZONE,
+ }
+ with ProcessDefinition(
+ TEST_PROCESS_DEFINITION_NAME,
+ schedule=schedule,
+ start_time=start_time,
+ end_time=end_time,
+ timezone=ProcessDefinitionDefault.TIME_ZONE,
+ ) as pd:
+ assert pd.schedule_json == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_task.py b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_task.py
new file mode 100644
index 0000000..6af731b
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_task.py
@@ -0,0 +1,224 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task class function."""
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.core.task import Task, TaskRelation
+from tests.testing.task import Task as testTask
+
+TEST_TASK_RELATION_SET = set()
+TEST_TASK_RELATION_SIZE = 0
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ dict(),
+ {
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ ),
+ (
+ {
+ "local_params": ["foo", "bar"],
+ "resource_list": ["foo", "bar"],
+ "dependence": {"foo", "bar"},
+ "wait_start_timeout": {"foo", "bar"},
+ "condition_result": {"foo": ["bar"]},
+ },
+ {
+ "localParams": ["foo", "bar"],
+ "resourceList": ["foo", "bar"],
+ "dependence": {"foo", "bar"},
+ "waitStartTimeout": {"foo", "bar"},
+ "conditionResult": {"foo": ["bar"]},
+ },
+ ),
+ ],
+)
+def test_property_task_params(attr, expect):
+ """Test class task property."""
+ task = testTask(
+ "test-property-task-params",
+ "test-task",
+ **attr,
+ )
+ assert expect == task.task_params
+
+
+@pytest.mark.parametrize(
+ "pre_code, post_code, expect",
+ [
+ (123, 456, hash("123 -> 456")),
+ (12345678, 987654321, hash("12345678 -> 987654321")),
+ ],
+)
+def test_task_relation_hash_func(pre_code, post_code, expect):
+ """Test TaskRelation magic function :func:`__hash__`."""
+ task_param = TaskRelation(pre_task_code=pre_code, post_task_code=post_code)
+ assert hash(task_param) == expect
+
+
+@pytest.mark.parametrize(
+ "pre_code, post_code, size_add",
+ [
+ (123, 456, 1),
+ (123, 456, 0),
+ (456, 456, 1),
+ (123, 123, 1),
+ (456, 123, 1),
+ (0, 456, 1),
+ (123, 0, 1),
+ ],
+)
+def test_task_relation_add_to_set(pre_code, post_code, size_add):
+ """Test TaskRelation with different pre_code and post_code add to set behavior.
+
+ Here we use global variable to keep set of :class:`TaskRelation` instance and the number we expect
+ of the size when we add a new task relation to exists set.
+ """
+ task_relation = TaskRelation(pre_task_code=pre_code, post_task_code=post_code)
+ TEST_TASK_RELATION_SET.add(task_relation)
+ # hint python interpreter use global variable instead of local's
+ global TEST_TASK_RELATION_SIZE
+ TEST_TASK_RELATION_SIZE += size_add
+ assert len(TEST_TASK_RELATION_SET) == TEST_TASK_RELATION_SIZE
+
+
+def test_task_relation_to_dict():
+ """Test TaskRelation object function to_dict."""
+ pre_task_code = 123
+ post_task_code = 456
+ expect = {
+ "name": "",
+ "preTaskCode": pre_task_code,
+ "postTaskCode": post_task_code,
+ "preTaskVersion": 1,
+ "postTaskVersion": 1,
+ "conditionType": 0,
+ "conditionParams": {},
+ }
+ task_relation = TaskRelation(
+ pre_task_code=pre_task_code, post_task_code=post_task_code
+ )
+ assert task_relation.get_define() == expect
+
+
+def test_task_get_define():
+ """Test Task object function get_define."""
+ code = 123
+ version = 1
+ name = "test_task_get_define"
+ task_type = "test_task_get_define_type"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": version,
+ "description": None,
+ "delayTime": 0,
+ "taskType": task_type,
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ task = Task(name=name, task_type=task_type)
+ assert task.get_define() == expect
+
+
+@pytest.mark.parametrize("shift", ["<<", ">>"])
+def test_two_tasks_shift(shift: str):
+ """Test bit operator between tasks.
+
+ Here we test both `>>` and `<<` bit operator.
+ """
+ upstream = testTask(name="upstream", task_type=shift)
+ downstream = testTask(name="downstream", task_type=shift)
+ if shift == "<<":
+ downstream << upstream
+ elif shift == ">>":
+ upstream >> downstream
+ else:
+ assert False, f"Unexpect bit operator type {shift}."
+ assert (
+ 1 == len(upstream._downstream_task_codes)
+ and downstream.code in upstream._downstream_task_codes
+ ), "Task downstream task attributes error, downstream codes size or specific code failed."
+ assert (
+ 1 == len(downstream._upstream_task_codes)
+ and upstream.code in downstream._upstream_task_codes
+ ), "Task upstream task attributes error, upstream codes size or upstream code failed."
+
+
+@pytest.mark.parametrize(
+ "dep_expr, flag",
+ [
+ ("task << tasks", "upstream"),
+ ("tasks << task", "downstream"),
+ ("task >> tasks", "downstream"),
+ ("tasks >> task", "upstream"),
+ ],
+)
+def test_tasks_list_shift(dep_expr: str, flag: str):
+ """Test bit operator between task and sequence of tasks.
+
+ Here we test both `>>` and `<<` bit operator.
+ """
+ reverse_dict = {
+ "upstream": "downstream",
+ "downstream": "upstream",
+ }
+ task_type = "dep_task_and_tasks"
+ task = testTask(name="upstream", task_type=task_type)
+ tasks = [
+ testTask(name="downstream1", task_type=task_type),
+ testTask(name="downstream2", task_type=task_type),
+ ]
+
+ # Use build-in function eval to simply test case and reduce duplicate code
+ eval(dep_expr)
+ direction_attr = f"_{flag}_task_codes"
+ reverse_direction_attr = f"_{reverse_dict[flag]}_task_codes"
+ assert 2 == len(getattr(task, direction_attr))
+ assert [t.code in getattr(task, direction_attr) for t in tasks]
+
+ assert all([1 == len(getattr(t, reverse_direction_attr)) for t in tasks])
+ assert all([task.code in getattr(t, reverse_direction_attr) for t in tasks])
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/__init__.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/__init__.py
new file mode 100644
index 0000000..095e301
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init tasks package tests."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_condition.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_condition.py
new file mode 100644
index 0000000..5647d5b
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_condition.py
@@ -0,0 +1,439 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task dependent."""
+from typing import List, Tuple
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.exceptions import PyDSParamException
+from pydolphinscheduler.tasks.condition import (
+ FAILURE,
+ SUCCESS,
+ And,
+ ConditionOperator,
+ Conditions,
+ Or,
+ Status,
+)
+from tests.testing.task import Task
+
+TEST_NAME = "test-name"
+TEST_PROJECT = "test-project"
+TEST_PROCESS_DEFINITION = "test-process-definition"
+TEST_TYPE = "test-type"
+TEST_PROJECT_CODE, TEST_DEFINITION_CODE, TEST_TASK_CODE = 12345, 123456, 1234567
+
+TEST_OPERATOR_LIST = ("AND", "OR")
+
+
+@pytest.mark.parametrize(
+ "obj, expect",
+ [
+ (Status, "STATUS"),
+ (SUCCESS, "SUCCESS"),
+ (FAILURE, "FAILURE"),
+ ],
+)
+def test_class_status_status_name(obj: Status, expect: str):
+ """Test class status and sub class property status_name."""
+ assert obj.status_name() == expect
+
+
+@pytest.mark.parametrize(
+ "obj, tasks",
+ [
+ (Status, (1, 2, 3)),
+ (SUCCESS, (1.1, 2.2, 3.3)),
+ (FAILURE, (ConditionOperator(1), ConditionOperator(2), ConditionOperator(3))),
+ ],
+)
+def test_class_status_depend_item_list_no_expect_type(obj: Status, tasks: Tuple):
+ """Test class status and sub class raise error when assign not support type."""
+ with pytest.raises(
+ PyDSParamException, match=".*?only accept class Task or sub class Task, but get"
+ ):
+ obj(*tasks).get_define()
+
+
+@pytest.mark.parametrize(
+ "obj, tasks",
+ [
+ (Status, [Task(str(i), TEST_TYPE) for i in range(1)]),
+ (Status, [Task(str(i), TEST_TYPE) for i in range(2)]),
+ (Status, [Task(str(i), TEST_TYPE) for i in range(3)]),
+ (SUCCESS, [Task(str(i), TEST_TYPE) for i in range(1)]),
+ (SUCCESS, [Task(str(i), TEST_TYPE) for i in range(2)]),
+ (SUCCESS, [Task(str(i), TEST_TYPE) for i in range(3)]),
+ (FAILURE, [Task(str(i), TEST_TYPE) for i in range(1)]),
+ (FAILURE, [Task(str(i), TEST_TYPE) for i in range(2)]),
+ (FAILURE, [Task(str(i), TEST_TYPE) for i in range(3)]),
+ ],
+)
+def test_class_status_depend_item_list(obj: Status, tasks: Tuple):
+ """Test class status and sub class function :func:`depend_item_list`."""
+ status = obj.status_name()
+ expect = [
+ {
+ "depTaskCode": i.code,
+ "status": status,
+ }
+ for i in tasks
+ ]
+ assert obj(*tasks).get_define() == expect
+
+
+@pytest.mark.parametrize(
+ "obj, expect",
+ [
+ (ConditionOperator, "CONDITIONOPERATOR"),
+ (And, "AND"),
+ (Or, "OR"),
+ ],
+)
+def test_condition_operator_operator_name(obj: ConditionOperator, expect: str):
+ """Test class ConditionOperator and sub class class function :func:`operator_name`."""
+ assert obj.operator_name() == expect
+
+
+@pytest.mark.parametrize(
+ "obj, expect",
+ [
+ (ConditionOperator, "CONDITIONOPERATOR"),
+ (And, "AND"),
+ (Or, "OR"),
+ ],
+)
+def test_condition_operator_relation(obj: ConditionOperator, expect: str):
+ """Test class ConditionOperator and sub class class property `relation`."""
+ assert obj(1).relation == expect
+
+
+@pytest.mark.parametrize(
+ "obj, status_or_operator, match",
+ [
+ (
+ ConditionOperator,
+ [Status(Task("1", TEST_TYPE)), 1],
+ ".*?operator parameter support ConditionTask and ConditionOperator.*?",
+ ),
+ (
+ ConditionOperator,
+ [
+ Status(Task("1", TEST_TYPE)),
+ 1.0,
+ ],
+ ".*?operator parameter support ConditionTask and ConditionOperator.*?",
+ ),
+ (
+ ConditionOperator,
+ [
+ Status(Task("1", TEST_TYPE)),
+ ConditionOperator(And(Status(Task("1", TEST_TYPE)))),
+ ],
+ ".*?operator parameter only support same type.",
+ ),
+ (
+ ConditionOperator,
+ [
+ ConditionOperator(And(Status(Task("1", TEST_TYPE)))),
+ Status(Task("1", TEST_TYPE)),
+ ],
+ ".*?operator parameter only support same type.",
+ ),
+ ],
+)
+def test_condition_operator_set_define_attr_not_support_type(
+ obj, status_or_operator, match
+):
+ """Test class ConditionOperator parameter error, including parameter not same or type not support."""
+ with pytest.raises(PyDSParamException, match=match):
+ op = obj(*status_or_operator)
+ op.set_define_attr()
+
+
+@pytest.mark.parametrize(
+ "obj, task_num",
+ [
+ (ConditionOperator, 1),
+ (ConditionOperator, 2),
+ (ConditionOperator, 3),
+ (And, 1),
+ (And, 2),
+ (And, 3),
+ (Or, 1),
+ (Or, 2),
+ (Or, 3),
+ ],
+)
+def test_condition_operator_set_define_attr_status(
+ obj: ConditionOperator, task_num: int
+):
+ """Test :func:`set_define_attr` with one or more class status."""
+ attr = "depend_item_list"
+
+ tasks = [Task(str(i), TEST_TYPE) for i in range(task_num)]
+ status = Status(*tasks)
+
+ expect = [
+ {"depTaskCode": task.code, "status": status.status_name()} for task in tasks
+ ]
+
+ co = obj(status)
+ co.set_define_attr()
+ assert getattr(co, attr) == expect
+
+
+@pytest.mark.parametrize(
+ "obj, status",
+ [
+ (ConditionOperator, (SUCCESS, SUCCESS)),
+ (ConditionOperator, (FAILURE, FAILURE)),
+ (ConditionOperator, (SUCCESS, FAILURE)),
+ (ConditionOperator, (FAILURE, SUCCESS)),
+ (And, (SUCCESS, SUCCESS)),
+ (And, (FAILURE, FAILURE)),
+ (And, (SUCCESS, FAILURE)),
+ (And, (FAILURE, SUCCESS)),
+ (Or, (SUCCESS, SUCCESS)),
+ (Or, (FAILURE, FAILURE)),
+ (Or, (SUCCESS, FAILURE)),
+ (Or, (FAILURE, SUCCESS)),
+ ],
+)
+def test_condition_operator_set_define_attr_mix_status(
+ obj: ConditionOperator, status: List[Status]
+):
+ """Test :func:`set_define_attr` with one or more mixed status."""
+ attr = "depend_item_list"
+
+ task = Task("test-operator", TEST_TYPE)
+ status_list = []
+ expect = []
+ for sta in status:
+ status_list.append(sta(task))
+ expect.append({"depTaskCode": task.code, "status": sta.status_name()})
+
+ co = obj(*status_list)
+ co.set_define_attr()
+ assert getattr(co, attr) == expect
+
+
+@pytest.mark.parametrize(
+ "obj, task_num",
+ [
+ (ConditionOperator, 1),
+ (ConditionOperator, 2),
+ (ConditionOperator, 3),
+ (And, 1),
+ (And, 2),
+ (And, 3),
+ (Or, 1),
+ (Or, 2),
+ (Or, 3),
+ ],
+)
+def test_condition_operator_set_define_attr_operator(
+ obj: ConditionOperator, task_num: int
+):
+ """Test :func:`set_define_attr` with one or more class condition operator."""
+ attr = "depend_task_list"
+
+ task = Task("test-operator", TEST_TYPE)
+ status = Status(task)
+
+ expect = [
+ {
+ "relation": obj.operator_name(),
+ "dependItemList": [
+ {
+ "depTaskCode": task.code,
+ "status": status.status_name(),
+ }
+ ],
+ }
+ for _ in range(task_num)
+ ]
+
+ co = obj(*[obj(status) for _ in range(task_num)])
+ co.set_define_attr()
+ assert getattr(co, attr) == expect
+
+
+@pytest.mark.parametrize(
+ "cond, sub_cond",
+ [
+ (ConditionOperator, (And, Or)),
+ (ConditionOperator, (Or, And)),
+ (And, (And, Or)),
+ (And, (Or, And)),
+ (Or, (And, Or)),
+ (Or, (Or, And)),
+ ],
+)
+def test_condition_operator_set_define_attr_mix_operator(
+ cond: ConditionOperator, sub_cond: Tuple[ConditionOperator]
+):
+ """Test :func:`set_define_attr` with one or more class mix condition operator."""
+ attr = "depend_task_list"
+
+ task = Task("test-operator", TEST_TYPE)
+
+ expect = []
+ sub_condition = []
+ for cond in sub_cond:
+ status = Status(task)
+ sub_condition.append(cond(status))
+ expect.append(
+ {
+ "relation": cond.operator_name(),
+ "dependItemList": [
+ {
+ "depTaskCode": task.code,
+ "status": status.status_name(),
+ }
+ ],
+ }
+ )
+ co = cond(*sub_condition)
+ co.set_define_attr()
+ assert getattr(co, attr) == expect
+
+
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(12345, 1),
+)
+@patch(
+ "pydolphinscheduler.tasks.condition.Conditions.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_dependent_get_define(mock_condition_code_version, mock_task_code_version):
+ """Test task condition :func:`get_define`."""
+ common_task = Task(name="common_task", task_type="test_task_condition")
+ cond_operator = And(
+ And(
+ SUCCESS(common_task, common_task),
+ FAILURE(common_task, common_task),
+ ),
+ Or(
+ SUCCESS(common_task, common_task),
+ FAILURE(common_task, common_task),
+ ),
+ )
+
+ name = "test_condition_get_define"
+ expect = {
+ "code": 123,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "CONDITIONS",
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "dependence": {
+ "relation": "AND",
+ "dependTaskList": [
+ {
+ "relation": "AND",
+ "dependItemList": [
+ {"depTaskCode": common_task.code, "status": "SUCCESS"},
+ {"depTaskCode": common_task.code, "status": "SUCCESS"},
+ {"depTaskCode": common_task.code, "status": "FAILURE"},
+ {"depTaskCode": common_task.code, "status": "FAILURE"},
+ ],
+ },
+ {
+ "relation": "OR",
+ "dependItemList": [
+ {"depTaskCode": common_task.code, "status": "SUCCESS"},
+ {"depTaskCode": common_task.code, "status": "SUCCESS"},
+ {"depTaskCode": common_task.code, "status": "FAILURE"},
+ {"depTaskCode": common_task.code, "status": "FAILURE"},
+ ],
+ },
+ ],
+ },
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+
+ task = Conditions(name, condition=cond_operator)
+ assert task.get_define() == expect
+
+
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_condition_set_dep_workflow(mock_task_code_version):
+ """Test task condition set dependence in workflow level."""
+ with ProcessDefinition(name="test-condition-set-dep-workflow") as pd:
+ parent = Task(name="parent", task_type=TEST_TYPE)
+ condition_success_1 = Task(name="condition_success_1", task_type=TEST_TYPE)
+ condition_success_2 = Task(name="condition_success_2", task_type=TEST_TYPE)
+ condition_fail = Task(name="condition_fail", task_type=TEST_TYPE)
+ cond_operator = And(
+ And(
+ SUCCESS(condition_success_1, condition_success_2),
+ FAILURE(condition_fail),
+ ),
+ )
+
+ condition = Conditions(name=TEST_NAME, condition=cond_operator)
+ parent >> condition
+ # General tasks test
+ assert len(pd.tasks) == 5
+ assert sorted(pd.task_list, key=lambda t: t.name) == sorted(
+ [
+ parent,
+ condition,
+ condition_success_1,
+ condition_success_2,
+ condition_fail,
+ ],
+ key=lambda t: t.name,
+ )
+ # Task dep test
+ assert parent._downstream_task_codes == {condition.code}
+ assert condition._upstream_task_codes == {parent.code}
+
+ # Condition task dep after ProcessDefinition function get_define called
+ assert condition._downstream_task_codes == {
+ condition_success_1.code,
+ condition_success_2.code,
+ condition_fail.code,
+ }
+ assert all(
+ [
+ child._upstream_task_codes == {condition.code}
+ for child in [condition_success_1, condition_success_2, condition_fail]
+ ]
+ )
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_datax.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_datax.py
new file mode 100644
index 0000000..7fa4569
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_datax.py
@@ -0,0 +1,124 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task DataX."""
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.tasks.datax import CustomDataX, DataX
+
+
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "MYSQL"}),
+)
+def test_datax_get_define(mock_datasource):
+ """Test task datax function get_define."""
+ code = 123
+ version = 1
+ name = "test_datax_get_define"
+ command = "select name from test_source_table_name"
+ datasource_name = "test_datasource"
+ datatarget_name = "test_datatarget"
+ target_table = "test_target_table_name"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "DATAX",
+ "taskParams": {
+ "customConfig": 0,
+ "dsType": "MYSQL",
+ "dataSource": 1,
+ "dtType": "MYSQL",
+ "dataTarget": 1,
+ "sql": command,
+ "targetTable": target_table,
+ "jobSpeedByte": 0,
+ "jobSpeedRecord": 1000,
+ "xms": 1,
+ "xmx": 1,
+ "preStatements": [],
+ "postStatements": [],
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ task = DataX(name, datasource_name, datatarget_name, command, target_table)
+ assert task.get_define() == expect
+
+
+@pytest.mark.parametrize("json_template", ["json_template"])
+def test_custom_datax_get_define(json_template):
+ """Test task custom datax function get_define."""
+ code = 123
+ version = 1
+ name = "test_custom_datax_get_define"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "DATAX",
+ "taskParams": {
+ "customConfig": 1,
+ "json": json_template,
+ "xms": 1,
+ "xmx": 1,
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ task = CustomDataX(name, json_template)
+ print(task.get_define())
+ print(expect)
+ assert task.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_dependent.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_dependent.py
new file mode 100644
index 0000000..f16e291
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_dependent.py
@@ -0,0 +1,793 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task dependent."""
+import itertools
+from typing import Dict, List, Optional, Tuple, Union
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.exceptions import PyDSParamException
+from pydolphinscheduler.tasks.dependent import (
+ And,
+ Dependent,
+ DependentDate,
+ DependentItem,
+ DependentOperator,
+ Or,
+)
+
+TEST_PROJECT = "test-project"
+TEST_PROCESS_DEFINITION = "test-process-definition"
+TEST_TASK = "test-task"
+TEST_PROJECT_CODE, TEST_DEFINITION_CODE, TEST_TASK_CODE = 12345, 123456, 1234567
+
+TEST_OPERATOR_LIST = ("AND", "OR")
+
+
+@pytest.mark.parametrize(
+ "dep_date, dep_cycle",
+ [
+ # hour
+ (DependentDate.CURRENT_HOUR, "hour"),
+ (DependentDate.LAST_ONE_HOUR, "hour"),
+ (DependentDate.LAST_TWO_HOURS, "hour"),
+ (DependentDate.LAST_THREE_HOURS, "hour"),
+ (DependentDate.LAST_TWENTY_FOUR_HOURS, "hour"),
+ # day
+ (DependentDate.TODAY, "day"),
+ (DependentDate.LAST_ONE_DAYS, "day"),
+ (DependentDate.LAST_TWO_DAYS, "day"),
+ (DependentDate.LAST_THREE_DAYS, "day"),
+ (DependentDate.LAST_SEVEN_DAYS, "day"),
+ # week
+ (DependentDate.THIS_WEEK, "week"),
+ (DependentDate.LAST_WEEK, "week"),
+ (DependentDate.LAST_MONDAY, "week"),
+ (DependentDate.LAST_TUESDAY, "week"),
+ (DependentDate.LAST_WEDNESDAY, "week"),
+ (DependentDate.LAST_THURSDAY, "week"),
+ (DependentDate.LAST_FRIDAY, "week"),
+ (DependentDate.LAST_SATURDAY, "week"),
+ (DependentDate.LAST_SUNDAY, "week"),
+ # month
+ (DependentDate.THIS_MONTH, "month"),
+ (DependentDate.LAST_MONTH, "month"),
+ (DependentDate.LAST_MONTH_BEGIN, "month"),
+ (DependentDate.LAST_MONTH_END, "month"),
+ ],
+)
+@patch(
+ "pydolphinscheduler.tasks.dependent.DependentItem.get_code_from_gateway",
+ return_value={
+ "projectCode": TEST_PROJECT_CODE,
+ "processDefinitionCode": TEST_DEFINITION_CODE,
+ "taskDefinitionCode": TEST_TASK_CODE,
+ },
+)
+def test_dependent_item_get_define(mock_task_info, dep_date, dep_cycle):
+ """Test dependent.DependentItem get define.
+
+ Here we have test some cases as below.
+ ```py
+ {
+ "projectCode": "project code",
+ "definitionCode": "definition code",
+ "depTaskCode": "dep task code",
+ "cycle": "day",
+ "dateValue": "today"
+ }
+ ```
+ """
+ attr = {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": dep_date,
+ }
+ expect = {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": dep_cycle,
+ "dateValue": dep_date,
+ }
+ task = DependentItem(**attr)
+ assert expect == task.get_define()
+
+
+def test_dependent_item_date_error():
+ """Test error when pass None to dependent_date."""
+ with pytest.raises(
+ PyDSParamException, match="Parameter dependent_date must provider.*?"
+ ):
+ DependentItem(
+ project_name=TEST_PROJECT,
+ process_definition_name=TEST_PROCESS_DEFINITION,
+ dependent_date=None,
+ )
+
+
+@pytest.mark.parametrize(
+ "task_name, result",
+ [
+ ({"dependent_task_name": TEST_TASK}, TEST_TASK),
+ ({}, None),
+ ],
+)
+def test_dependent_item_code_parameter(task_name: dict, result: Optional[str]):
+ """Test dependent item property code_parameter."""
+ dependent_item = DependentItem(
+ project_name=TEST_PROJECT,
+ process_definition_name=TEST_PROCESS_DEFINITION,
+ **task_name,
+ )
+ expect = (TEST_PROJECT, TEST_PROCESS_DEFINITION, result)
+ assert dependent_item.code_parameter == expect
+
+
+@pytest.mark.parametrize(
+ "arg_list",
+ [
+ [1, 2],
+ [
+ DependentItem(
+ project_name=TEST_PROJECT,
+ process_definition_name=TEST_PROCESS_DEFINITION,
+ ),
+ 1,
+ ],
+ [
+ And(
+ DependentItem(
+ project_name=TEST_PROJECT,
+ process_definition_name=TEST_PROCESS_DEFINITION,
+ )
+ ),
+ 1,
+ ],
+ [
+ DependentItem(
+ project_name=TEST_PROJECT,
+ process_definition_name=TEST_PROCESS_DEFINITION,
+ ),
+ And(
+ DependentItem(
+ project_name=TEST_PROJECT,
+ process_definition_name=TEST_PROCESS_DEFINITION,
+ )
+ ),
+ ],
+ ],
+)
+@patch(
+ "pydolphinscheduler.tasks.dependent.DependentItem.get_code_from_gateway",
+ return_value={
+ "projectCode": TEST_PROJECT_CODE,
+ "processDefinitionCode": TEST_DEFINITION_CODE,
+ "taskDefinitionCode": TEST_TASK_CODE,
+ },
+)
+def test_dependent_operator_set_define_error(mock_code, arg_list):
+ """Test dependent operator function :func:`set_define` with not support type."""
+ dep_op = DependentOperator(*arg_list)
+ with pytest.raises(PyDSParamException, match="Dependent .*? operator.*?"):
+ dep_op.set_define_attr()
+
+
+@pytest.mark.parametrize(
+ # Test dependent operator, Test dependent item parameters, expect operator define
+ "operators, kwargs, expect",
+ [
+ # Test dependent operator (And | Or) with single dependent item
+ (
+ (And, Or),
+ (
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ ),
+ [
+ {
+ "relation": op,
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ ],
+ }
+ for op in TEST_OPERATOR_LIST
+ ],
+ ),
+ # Test dependent operator (And | Or) with two dependent item
+ (
+ (And, Or),
+ (
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_WEEK,
+ },
+ ),
+ [
+ {
+ "relation": op,
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "week",
+ "dateValue": DependentDate.LAST_WEEK,
+ },
+ ],
+ }
+ for op in TEST_OPERATOR_LIST
+ ],
+ ),
+ # Test dependent operator (And | Or) with multiply dependent item
+ (
+ (And, Or),
+ (
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_WEEK,
+ },
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_ONE_DAYS,
+ },
+ ),
+ [
+ {
+ "relation": op,
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "week",
+ "dateValue": DependentDate.LAST_WEEK,
+ },
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "day",
+ "dateValue": DependentDate.LAST_ONE_DAYS,
+ },
+ ],
+ }
+ for op in TEST_OPERATOR_LIST
+ ],
+ ),
+ ],
+)
+@patch(
+ "pydolphinscheduler.tasks.dependent.DependentItem.get_code_from_gateway",
+ return_value={
+ "projectCode": TEST_PROJECT_CODE,
+ "processDefinitionCode": TEST_DEFINITION_CODE,
+ "taskDefinitionCode": TEST_TASK_CODE,
+ },
+)
+def test_operator_dependent_item(
+ mock_code_info,
+ operators: Tuple[DependentOperator],
+ kwargs: Tuple[dict],
+ expect: List[Dict],
+):
+ """Test DependentOperator(DependentItem) function get_define.
+
+ Here we have test some cases as below, including single dependentItem and multiply dependentItem.
+ ```py
+ {
+ "relation": "AND",
+ "dependItemList": [
+ {
+ "projectCode": "project code",
+ "definitionCode": "definition code",
+ "depTaskCode": "dep task code",
+ "cycle": "day",
+ "dateValue": "today"
+ },
+ ...
+ ]
+ }
+ ```
+ """
+ for idx, operator in enumerate(operators):
+ # Use variable to keep one or more dependent item to test dependent operator behavior
+ dependent_item_list = []
+ for kwarg in kwargs:
+ dependent_item = DependentItem(**kwarg)
+ dependent_item_list.append(dependent_item)
+ op = operator(*dependent_item_list)
+ assert expect[idx] == op.get_define()
+
+
+@pytest.mark.parametrize(
+ # Test dependent operator, Test dependent item parameters, expect operator define
+ "operators, args, expect",
+ [
+ # Test dependent operator (And | Or) with single dependent task list
+ (
+ (And, Or),
+ (
+ (And, Or),
+ (
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ ),
+ ),
+ [
+ {
+ "relation": par_op,
+ "dependTaskList": [
+ {
+ "relation": chr_op,
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ ],
+ }
+ ],
+ }
+ for (par_op, chr_op) in itertools.product(
+ TEST_OPERATOR_LIST, TEST_OPERATOR_LIST
+ )
+ ],
+ ),
+ # Test dependent operator (And | Or) with two dependent task list
+ (
+ (And, Or),
+ (
+ (And, Or),
+ (
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_WEEK,
+ },
+ ),
+ ),
+ [
+ {
+ "relation": par_op,
+ "dependTaskList": [
+ {
+ "relation": chr_op,
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "week",
+ "dateValue": DependentDate.LAST_WEEK,
+ },
+ ],
+ }
+ ],
+ }
+ for (par_op, chr_op) in itertools.product(
+ TEST_OPERATOR_LIST, TEST_OPERATOR_LIST
+ )
+ ],
+ ),
+ # Test dependent operator (And | Or) with multiply dependent task list
+ (
+ (And, Or),
+ (
+ (And, Or),
+ (
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_WEEK,
+ },
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_ONE_DAYS,
+ },
+ ),
+ ),
+ [
+ {
+ "relation": par_op,
+ "dependTaskList": [
+ {
+ "relation": chr_op,
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "week",
+ "dateValue": DependentDate.LAST_WEEK,
+ },
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "day",
+ "dateValue": DependentDate.LAST_ONE_DAYS,
+ },
+ ],
+ }
+ ],
+ }
+ for (par_op, chr_op) in itertools.product(
+ TEST_OPERATOR_LIST, TEST_OPERATOR_LIST
+ )
+ ],
+ ),
+ ],
+)
+@patch(
+ "pydolphinscheduler.tasks.dependent.DependentItem.get_code_from_gateway",
+ return_value={
+ "projectCode": TEST_PROJECT_CODE,
+ "processDefinitionCode": TEST_DEFINITION_CODE,
+ "taskDefinitionCode": TEST_TASK_CODE,
+ },
+)
+def test_operator_dependent_task_list_multi_dependent_item(
+ mock_code_info,
+ operators: Tuple[DependentOperator],
+ args: Tuple[Union[Tuple, dict]],
+ expect: List[Dict],
+):
+ """Test DependentOperator(DependentOperator(DependentItem)) single operator function get_define.
+
+ Here we have test some cases as below. This test case only test single DependTaskList with one or
+ multiply dependItemList.
+ ```py
+ {
+ "relation": "OR",
+ "dependTaskList": [
+ {
+ "relation": "AND",
+ "dependItemList": [
+ {
+ "projectCode": "project code",
+ "definitionCode": "definition code",
+ "depTaskCode": "dep task code",
+ "cycle": "day",
+ "dateValue": "today"
+ },
+ ...
+ ]
+ },
+ ]
+ }
+ ```
+ """
+ # variable expect_idx record idx should be use to get specific expect
+ expect_idx = 0
+
+ for op_idx, operator in enumerate(operators):
+ dependent_operator = args[0]
+ dependent_item_kwargs = args[1]
+
+ for dop_idx, dpt_op in enumerate(dependent_operator):
+ dependent_item_list = []
+ for dpt_kwargs in dependent_item_kwargs:
+ dpti = DependentItem(**dpt_kwargs)
+ dependent_item_list.append(dpti)
+ child_dep_op = dpt_op(*dependent_item_list)
+ op = operator(child_dep_op)
+ assert expect[expect_idx] == op.get_define()
+ expect_idx += 1
+
+
+def get_dep_task_list(*operator):
+ """Return dependent task list from given operators list."""
+ result = []
+ for op in operator:
+ result.append(
+ {
+ "relation": op.operator_name(),
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "month",
+ "dateValue": DependentDate.LAST_MONTH_END,
+ },
+ ],
+ }
+ )
+ return result
+
+
+@pytest.mark.parametrize(
+ # Test dependent operator, Test dependent item parameters, expect operator define
+ "operators, args, expect",
+ [
+ # Test dependent operator (And | Or) with two dependent task list
+ (
+ (And, Or),
+ (
+ ((And, And), (And, Or), (Or, And), (Or, Or)),
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ ),
+ [
+ {
+ "relation": parent_op.operator_name(),
+ "dependTaskList": get_dep_task_list(*child_ops),
+ }
+ for parent_op in (And, Or)
+ for child_ops in ((And, And), (And, Or), (Or, And), (Or, Or))
+ ],
+ ),
+ # Test dependent operator (And | Or) with multiple dependent task list
+ (
+ (And, Or),
+ (
+ ((And, And, And), (And, And, And, And), (And, And, And, And, And)),
+ {
+ "project_name": TEST_PROJECT,
+ "process_definition_name": TEST_PROCESS_DEFINITION,
+ "dependent_task_name": TEST_TASK,
+ "dependent_date": DependentDate.LAST_MONTH_END,
+ },
+ ),
+ [
+ {
+ "relation": parent_op.operator_name(),
+ "dependTaskList": get_dep_task_list(*child_ops),
+ }
+ for parent_op in (And, Or)
+ for child_ops in (
+ (And, And, And),
+ (And, And, And, And),
+ (And, And, And, And, And),
+ )
+ ],
+ ),
+ ],
+)
+@patch(
+ "pydolphinscheduler.tasks.dependent.DependentItem.get_code_from_gateway",
+ return_value={
+ "projectCode": TEST_PROJECT_CODE,
+ "processDefinitionCode": TEST_DEFINITION_CODE,
+ "taskDefinitionCode": TEST_TASK_CODE,
+ },
+)
+def test_operator_dependent_task_list_multi_dependent_list(
+ mock_code_info,
+ operators: Tuple[DependentOperator],
+ args: Tuple[Union[Tuple, dict]],
+ expect: List[Dict],
+):
+ """Test DependentOperator(DependentOperator(DependentItem)) multiply operator function get_define.
+
+ Here we have test some cases as below. This test case only test single DependTaskList with one or
+ multiply dependTaskList.
+ ```py
+ {
+ "relation": "OR",
+ "dependTaskList": [
+ {
+ "relation": "AND",
+ "dependItemList": [
+ {
+ "projectCode": "project code",
+ "definitionCode": "definition code",
+ "depTaskCode": "dep task code",
+ "cycle": "day",
+ "dateValue": "today"
+ }
+ ]
+ },
+ ...
+ ]
+ }
+ ```
+ """
+ # variable expect_idx record idx should be use to get specific expect
+ expect_idx = 0
+ for op_idx, operator in enumerate(operators):
+ dependent_operator = args[0]
+ dependent_item_kwargs = args[1]
+
+ for dop_idx, dpt_ops in enumerate(dependent_operator):
+ dependent_task_list = [
+ dpt_op(DependentItem(**dependent_item_kwargs)) for dpt_op in dpt_ops
+ ]
+ op = operator(*dependent_task_list)
+ assert (
+ expect[expect_idx] == op.get_define()
+ ), f"Failed with operator syntax {operator}.{dpt_ops}"
+ expect_idx += 1
+
+
+@patch(
+ "pydolphinscheduler.tasks.dependent.DependentItem.get_code_from_gateway",
+ return_value={
+ "projectCode": TEST_PROJECT_CODE,
+ "processDefinitionCode": TEST_DEFINITION_CODE,
+ "taskDefinitionCode": TEST_TASK_CODE,
+ },
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_dependent_get_define(mock_code_version, mock_dep_code):
+ """Test task dependent function get_define."""
+ project_name = "test-dep-project"
+ process_definition_name = "test-dep-definition"
+ dependent_task_name = "test-dep-task"
+ dep_operator = And(
+ Or(
+ # test dependence with add tasks
+ DependentItem(
+ project_name=project_name,
+ process_definition_name=process_definition_name,
+ )
+ ),
+ And(
+ # test dependence with specific task
+ DependentItem(
+ project_name=project_name,
+ process_definition_name=process_definition_name,
+ dependent_task_name=dependent_task_name,
+ )
+ ),
+ )
+
+ name = "test_dependent_get_define"
+ expect = {
+ "code": 123,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "DEPENDENT",
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "dependence": {
+ "relation": "AND",
+ "dependTaskList": [
+ {
+ "relation": "OR",
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": "0",
+ "cycle": "day",
+ "dateValue": "today",
+ }
+ ],
+ },
+ {
+ "relation": "AND",
+ "dependItemList": [
+ {
+ "projectCode": TEST_PROJECT_CODE,
+ "definitionCode": TEST_DEFINITION_CODE,
+ "depTaskCode": TEST_TASK_CODE,
+ "cycle": "day",
+ "dateValue": "today",
+ }
+ ],
+ },
+ ],
+ },
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+
+ task = Dependent(name, dependence=dep_operator)
+ assert task.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_http.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_http.py
new file mode 100644
index 0000000..060cdec
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_http.py
@@ -0,0 +1,144 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task HTTP."""
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.exceptions import PyDSParamException
+from pydolphinscheduler.tasks.http import Http, HttpCheckCondition, HttpMethod
+
+
+@pytest.mark.parametrize(
+ "class_name, attrs",
+ [
+ (HttpMethod, ("GET", "POST", "HEAD", "PUT", "DELETE")),
+ (
+ HttpCheckCondition,
+ (
+ "STATUS_CODE_DEFAULT",
+ "STATUS_CODE_CUSTOM",
+ "BODY_CONTAINS",
+ "BODY_NOT_CONTAINS",
+ ),
+ ),
+ ],
+)
+def test_attr_exists(class_name, attrs):
+ """Test weather class HttpMethod and HttpCheckCondition contain specific attribute."""
+ assert all(hasattr(class_name, attr) for attr in attrs)
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ {"url": "https://www.apache.org"},
+ {
+ "url": "https://www.apache.org",
+ "httpMethod": "GET",
+ "httpParams": [],
+ "httpCheckCondition": "STATUS_CODE_DEFAULT",
+ "condition": None,
+ "connectTimeout": 60000,
+ "socketTimeout": 60000,
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ )
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_property_task_params(mock_code_version, attr, expect):
+ """Test task http property."""
+ task = Http("test-http-task-params", **attr)
+ assert expect == task.task_params
+
+
+@pytest.mark.parametrize(
+ "param",
+ [
+ {"http_method": "http_method"},
+ {"http_check_condition": "http_check_condition"},
+ {"http_check_condition": HttpCheckCondition.STATUS_CODE_CUSTOM},
+ {
+ "http_check_condition": HttpCheckCondition.STATUS_CODE_CUSTOM,
+ "condition": None,
+ },
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_http_task_param_not_support_param(mock_code, param):
+ """Test HttpTaskParams not support parameter."""
+ url = "https://www.apache.org"
+ with pytest.raises(PyDSParamException, match="Parameter .*?"):
+ Http("test-no-supprot-param", url, **param)
+
+
+def test_http_get_define():
+ """Test task HTTP function get_define."""
+ code = 123
+ version = 1
+ name = "test_http_get_define"
+ url = "https://www.apache.org"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "HTTP",
+ "taskParams": {
+ "localParams": [],
+ "httpParams": [],
+ "url": url,
+ "httpMethod": "GET",
+ "httpCheckCondition": "STATUS_CODE_DEFAULT",
+ "condition": None,
+ "connectTimeout": 60000,
+ "socketTimeout": 60000,
+ "dependence": {},
+ "resourceList": [],
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ http = Http(name, url)
+ assert http.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_procedure.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_procedure.py
new file mode 100644
index 0000000..1782593
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_procedure.py
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task Procedure."""
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.tasks.procedure import Procedure
+
+TEST_PROCEDURE_SQL = (
+ 'create procedure HelloWorld() selece "hello world"; call HelloWorld();'
+)
+TEST_PROCEDURE_DATASOURCE_NAME = "test_datasource"
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ {
+ "name": "test-procedure-task-params",
+ "datasource_name": TEST_PROCEDURE_DATASOURCE_NAME,
+ "method": TEST_PROCEDURE_SQL,
+ },
+ {
+ "method": TEST_PROCEDURE_SQL,
+ "type": "MYSQL",
+ "datasource": 1,
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ )
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "MYSQL"}),
+)
+def test_property_task_params(mock_datasource, mock_code_version, attr, expect):
+ """Test task sql task property."""
+ task = Procedure(**attr)
+ assert expect == task.task_params
+
+
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "MYSQL"}),
+)
+def test_sql_get_define(mock_datasource, mock_code_version):
+ """Test task procedure function get_define."""
+ name = "test_procedure_get_define"
+ expect = {
+ "code": 123,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "PROCEDURE",
+ "taskParams": {
+ "type": "MYSQL",
+ "datasource": 1,
+ "method": TEST_PROCEDURE_SQL,
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ task = Procedure(name, TEST_PROCEDURE_DATASOURCE_NAME, TEST_PROCEDURE_SQL)
+ assert task.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_python.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_python.py
new file mode 100644
index 0000000..dbcd298
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_python.py
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task python."""
+
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.exceptions import PyDSParamException
+from pydolphinscheduler.tasks.python import Python
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ {"code": "print(1)"},
+ {
+ "rawScript": "print(1)",
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ )
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_property_task_params(mock_code_version, attr, expect):
+ """Test task python property."""
+ task = Python("test-python-task-params", **attr)
+ assert expect == task.task_params
+
+
+@pytest.mark.parametrize(
+ "script_code",
+ [
+ 123,
+ ("print", "hello world"),
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_python_task_not_support_code(mock_code, script_code):
+ """Test python task parameters."""
+ name = "not_support_code_type"
+ with pytest.raises(PyDSParamException, match="Parameter code do not support .*?"):
+ task = Python(name, script_code)
+ task.raw_script
+
+
+def foo(): # noqa: D103
+ print("hello world.")
+
+
+@pytest.mark.parametrize(
+ "name, script_code, raw",
+ [
+ ("string_define", 'print("hello world.")', 'print("hello world.")'),
+ (
+ "function_define",
+ foo,
+ 'def foo(): # noqa: D103\n print("hello world.")\n',
+ ),
+ ],
+)
+def test_python_get_define(name, script_code, raw):
+ """Test task python function get_define."""
+ code = 123
+ version = 1
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "PYTHON",
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "rawScript": raw,
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ shell = Python(name, script_code)
+ assert shell.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_shell.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_shell.py
new file mode 100644
index 0000000..e42f6dc
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_shell.py
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task shell."""
+
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.tasks.shell import Shell
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ {"command": "test script"},
+ {
+ "rawScript": "test script",
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ )
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_property_task_params(mock_code_version, attr, expect):
+ """Test task shell task property."""
+ task = Shell("test-shell-task-params", **attr)
+ assert expect == task.task_params
+
+
+def test_shell_get_define():
+ """Test task shell function get_define."""
+ code = 123
+ version = 1
+ name = "test_shell_get_define"
+ command = "echo test shell"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "SHELL",
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "rawScript": command,
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ shell = Shell(name, command)
+ assert shell.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_sql.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_sql.py
new file mode 100644
index 0000000..3f8209c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_sql.py
@@ -0,0 +1,149 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task Sql."""
+
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.tasks.sql import Sql, SqlType
+
+
+@pytest.mark.parametrize(
+ "sql, sql_type",
+ [
+ ("select 1", SqlType.SELECT),
+ (" select 1", SqlType.SELECT),
+ (" select 1 ", SqlType.SELECT),
+ (" select 'insert' ", SqlType.SELECT),
+ (" select 'insert ' ", SqlType.SELECT),
+ ("with tmp as (select 1) select * from tmp ", SqlType.SELECT),
+ ("insert into table_name(col1, col2) value (val1, val2)", SqlType.NOT_SELECT),
+ (
+ "insert into table_name(select, col2) value ('select', val2)",
+ SqlType.NOT_SELECT,
+ ),
+ ("update table_name SET col1=val1 where col1=val2", SqlType.NOT_SELECT),
+ ("update table_name SET col1='select' where col1=val2", SqlType.NOT_SELECT),
+ ("delete from table_name where id < 10", SqlType.NOT_SELECT),
+ ("delete from table_name where id < 10", SqlType.NOT_SELECT),
+ ("alter table table_name add column col1 int", SqlType.NOT_SELECT),
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "mock_type"}),
+)
+def test_get_sql_type(mock_datasource, mock_code_version, sql, sql_type):
+ """Test property sql_type could return correct type."""
+ name = "test_get_sql_type"
+ datasource_name = "test_datasource"
+ task = Sql(name, datasource_name, sql)
+ assert (
+ sql_type == task.sql_type
+ ), f"Sql {sql} expect sql type is {sql_type} but got {task.sql_type}"
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ {"datasource_name": "datasource_name", "sql": "select 1"},
+ {
+ "sql": "select 1",
+ "type": "MYSQL",
+ "datasource": 1,
+ "sqlType": SqlType.SELECT,
+ "preStatements": [],
+ "postStatements": [],
+ "displayRows": 10,
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ )
+ ],
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "MYSQL"}),
+)
+def test_property_task_params(mock_datasource, mock_code_version, attr, expect):
+ """Test task sql task property."""
+ task = Sql("test-sql-task-params", **attr)
+ assert expect == task.task_params
+
+
+@patch(
+ "pydolphinscheduler.core.database.Database.get_database_info",
+ return_value=({"id": 1, "type": "MYSQL"}),
+)
+def test_sql_get_define(mock_datasource):
+ """Test task sql function get_define."""
+ code = 123
+ version = 1
+ name = "test_sql_get_define"
+ command = "select 1"
+ datasource_name = "test_datasource"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "SQL",
+ "taskParams": {
+ "type": "MYSQL",
+ "datasource": 1,
+ "sql": command,
+ "sqlType": SqlType.SELECT,
+ "displayRows": 10,
+ "preStatements": [],
+ "postStatements": [],
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ task = Sql(name, datasource_name, command)
+ assert task.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_sub_process.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_sub_process.py
new file mode 100644
index 0000000..7f471a1
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_sub_process.py
@@ -0,0 +1,114 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task sub_process."""
+
+
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.tasks.sub_process import SubProcess
+
+TEST_SUB_PROCESS_DEFINITION_NAME = "sub-test-process-definition"
+TEST_SUB_PROCESS_DEFINITION_CODE = "3643589832320"
+TEST_PROCESS_DEFINITION_NAME = "simple-test-process-definition"
+
+
+@pytest.mark.parametrize(
+ "attr, expect",
+ [
+ (
+ {"process_definition_name": TEST_SUB_PROCESS_DEFINITION_NAME},
+ {
+ "processDefinitionCode": TEST_SUB_PROCESS_DEFINITION_CODE,
+ "localParams": [],
+ "resourceList": [],
+ "dependence": {},
+ "waitStartTimeout": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ },
+ )
+ ],
+)
+@patch(
+ "pydolphinscheduler.tasks.sub_process.SubProcess.get_process_definition_info",
+ return_value=(
+ {
+ "id": 1,
+ "name": TEST_SUB_PROCESS_DEFINITION_NAME,
+ "code": TEST_SUB_PROCESS_DEFINITION_CODE,
+ }
+ ),
+)
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_property_task_params(mock_code_version, mock_pd_info, attr, expect):
+ """Test task sub process property."""
+ task = SubProcess("test-sub-process-task-params", **attr)
+ assert expect == task.task_params
+
+
+@patch(
+ "pydolphinscheduler.tasks.sub_process.SubProcess.get_process_definition_info",
+ return_value=(
+ {
+ "id": 1,
+ "name": TEST_SUB_PROCESS_DEFINITION_NAME,
+ "code": TEST_SUB_PROCESS_DEFINITION_CODE,
+ }
+ ),
+)
+def test_sub_process_get_define(mock_process_definition):
+ """Test task sub_process function get_define."""
+ code = 123
+ version = 1
+ name = "test_sub_process_get_define"
+ expect = {
+ "code": code,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "SUB_PROCESS",
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "processDefinitionCode": TEST_SUB_PROCESS_DEFINITION_CODE,
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+ with patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(code, version),
+ ):
+ with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME):
+ sub_process = SubProcess(name, TEST_SUB_PROCESS_DEFINITION_NAME)
+ assert sub_process.get_define() == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_switch.py b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_switch.py
new file mode 100644
index 0000000..1f6ff5b
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/tasks/test_switch.py
@@ -0,0 +1,300 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test Task switch."""
+
+from typing import Optional, Tuple
+from unittest.mock import patch
+
+import pytest
+
+from pydolphinscheduler.core.process_definition import ProcessDefinition
+from pydolphinscheduler.exceptions import PyDSParamException
+from pydolphinscheduler.tasks.switch import (
+ Branch,
+ Default,
+ Switch,
+ SwitchBranch,
+ SwitchCondition,
+)
+from tests.testing.task import Task
+
+TEST_NAME = "test-task"
+TEST_TYPE = "test-type"
+
+
+def task_switch_arg_wrapper(obj, task: Task, exp: Optional[str] = None) -> SwitchBranch:
+ """Wrap task switch and its subclass."""
+ if obj is Default:
+ return obj(task)
+ elif obj is Branch:
+ return obj(exp, task)
+ else:
+ return obj(task, exp)
+
+
+@pytest.mark.parametrize(
+ "obj",
+ [
+ SwitchBranch,
+ Branch,
+ Default,
+ ],
+)
+def test_switch_branch_attr_next_node(obj: SwitchBranch):
+ """Test get attribute from class switch branch."""
+ task = Task(name=TEST_NAME, task_type=TEST_TYPE)
+ switch_branch = task_switch_arg_wrapper(obj, task=task, exp="unittest")
+ assert switch_branch.next_node == task.code
+
+
+@pytest.mark.parametrize(
+ "obj",
+ [
+ SwitchBranch,
+ Default,
+ ],
+)
+def test_switch_branch_get_define_without_condition(obj: SwitchBranch):
+ """Test function :func:`get_define` with None value of attribute condition from class switch branch."""
+ task = Task(name=TEST_NAME, task_type=TEST_TYPE)
+ expect = {"nextNode": task.code}
+ switch_branch = task_switch_arg_wrapper(obj, task=task)
+ assert switch_branch.get_define() == expect
+
+
+@pytest.mark.parametrize(
+ "obj",
+ [
+ SwitchBranch,
+ Branch,
+ ],
+)
+def test_switch_branch_get_define_condition(obj: SwitchBranch):
+ """Test function :func:`get_define` with specific attribute condition from class switch branch."""
+ task = Task(name=TEST_NAME, task_type=TEST_TYPE)
+ exp = "${var} == 1"
+ expect = {
+ "nextNode": task.code,
+ "condition": exp,
+ }
+ switch_branch = task_switch_arg_wrapper(obj, task=task, exp=exp)
+ assert switch_branch.get_define() == expect
+
+
+@pytest.mark.parametrize(
+ "args, msg",
+ [
+ (
+ (1,),
+ ".*?parameter only support SwitchBranch but got.*?",
+ ),
+ (
+ (Default(Task(TEST_NAME, TEST_TYPE)), 2),
+ ".*?parameter only support SwitchBranch but got.*?",
+ ),
+ (
+ (Default(Task(TEST_NAME, TEST_TYPE)), Default(Task(TEST_NAME, TEST_TYPE))),
+ ".*?parameter only support exactly one default branch",
+ ),
+ (
+ (
+ Branch(condition="unittest", task=Task(TEST_NAME, TEST_TYPE)),
+ Default(Task(TEST_NAME, TEST_TYPE)),
+ Default(Task(TEST_NAME, TEST_TYPE)),
+ ),
+ ".*?parameter only support exactly one default branch",
+ ),
+ ],
+)
+def test_switch_condition_set_define_attr_error(args: Tuple, msg: str):
+ """Test error case on :class:`SwitchCondition`."""
+ switch_condition = SwitchCondition(*args)
+ with pytest.raises(PyDSParamException, match=msg):
+ switch_condition.set_define_attr()
+
+
+def test_switch_condition_set_define_attr_default():
+ """Test set :class:`Default` to attribute on :class:`SwitchCondition`."""
+ task = Task(TEST_NAME, TEST_TYPE)
+ switch_condition = SwitchCondition(Default(task))
+ switch_condition.set_define_attr()
+ assert getattr(switch_condition, "next_node") == task.code
+ assert getattr(switch_condition, "depend_task_list") == []
+
+
+def test_switch_condition_set_define_attr_branch():
+ """Test set :class:`Branch` to attribute on :class:`SwitchCondition`."""
+ task = Task(TEST_NAME, TEST_TYPE)
+ switch_condition = SwitchCondition(
+ Branch("unittest1", task), Branch("unittest2", task)
+ )
+ expect = [
+ {"condition": "unittest1", "nextNode": task.code},
+ {"condition": "unittest2", "nextNode": task.code},
+ ]
+
+ switch_condition.set_define_attr()
+ assert getattr(switch_condition, "next_node") == ""
+ assert getattr(switch_condition, "depend_task_list") == expect
+
+
+def test_switch_condition_set_define_attr_mix_branch_and_default():
+ """Test set bot :class:`Branch` and :class:`Default` to attribute on :class:`SwitchCondition`."""
+ task = Task(TEST_NAME, TEST_TYPE)
+ switch_condition = SwitchCondition(
+ Branch("unittest1", task), Branch("unittest2", task), Default(task)
+ )
+ expect = [
+ {"condition": "unittest1", "nextNode": task.code},
+ {"condition": "unittest2", "nextNode": task.code},
+ ]
+
+ switch_condition.set_define_attr()
+ assert getattr(switch_condition, "next_node") == task.code
+ assert getattr(switch_condition, "depend_task_list") == expect
+
+
+def test_switch_condition_get_define_default():
+ """Test function :func:`get_define` with :class:`Default` in :class:`SwitchCondition`."""
+ task = Task(TEST_NAME, TEST_TYPE)
+ switch_condition = SwitchCondition(Default(task))
+ expect = {
+ "dependTaskList": [],
+ "nextNode": task.code,
+ }
+ assert switch_condition.get_define() == expect
+
+
+def test_switch_condition_get_define_branch():
+ """Test function :func:`get_define` with :class:`Branch` in :class:`SwitchCondition`."""
+ task = Task(TEST_NAME, TEST_TYPE)
+ switch_condition = SwitchCondition(
+ Branch("unittest1", task), Branch("unittest2", task)
+ )
+ expect = {
+ "dependTaskList": [
+ {"condition": "unittest1", "nextNode": task.code},
+ {"condition": "unittest2", "nextNode": task.code},
+ ],
+ "nextNode": "",
+ }
+ assert switch_condition.get_define() == expect
+
+
+def test_switch_condition_get_define_mix_branch_and_default():
+ """Test function :func:`get_define` with both :class:`Branch` and :class:`Default`."""
+ task = Task(TEST_NAME, TEST_TYPE)
+ switch_condition = SwitchCondition(
+ Branch("unittest1", task), Branch("unittest2", task), Default(task)
+ )
+ expect = {
+ "dependTaskList": [
+ {"condition": "unittest1", "nextNode": task.code},
+ {"condition": "unittest2", "nextNode": task.code},
+ ],
+ "nextNode": task.code,
+ }
+ assert switch_condition.get_define() == expect
+
+
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_switch_get_define(mock_task_code_version):
+ """Test task switch :func:`get_define`."""
+ task = Task(name=TEST_NAME, task_type=TEST_TYPE)
+ switch_condition = SwitchCondition(
+ Branch(condition="${var1} > 1", task=task),
+ Branch(condition="${var1} <= 1", task=task),
+ Default(task),
+ )
+
+ name = "test_switch_get_define"
+ expect = {
+ "code": 123,
+ "name": name,
+ "version": 1,
+ "description": None,
+ "delayTime": 0,
+ "taskType": "SWITCH",
+ "taskParams": {
+ "resourceList": [],
+ "localParams": [],
+ "dependence": {},
+ "conditionResult": {"successNode": [""], "failedNode": [""]},
+ "waitStartTimeout": {},
+ "switchResult": {
+ "dependTaskList": [
+ {"condition": "${var1} > 1", "nextNode": task.code},
+ {"condition": "${var1} <= 1", "nextNode": task.code},
+ ],
+ "nextNode": task.code,
+ },
+ },
+ "flag": "YES",
+ "taskPriority": "MEDIUM",
+ "workerGroup": "default",
+ "failRetryTimes": 0,
+ "failRetryInterval": 1,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": None,
+ "timeout": 0,
+ }
+
+ task = Switch(name, condition=switch_condition)
+ assert task.get_define() == expect
+
+
+@patch(
+ "pydolphinscheduler.core.task.Task.gen_code_and_version",
+ return_value=(123, 1),
+)
+def test_switch_set_dep_workflow(mock_task_code_version):
+ """Test task switch set dependence in workflow level."""
+ with ProcessDefinition(name="test-switch-set-dep-workflow") as pd:
+ parent = Task(name="parent", task_type=TEST_TYPE)
+ switch_child_1 = Task(name="switch_child_1", task_type=TEST_TYPE)
+ switch_child_2 = Task(name="switch_child_2", task_type=TEST_TYPE)
+ switch_condition = SwitchCondition(
+ Branch(condition="${var} > 1", task=switch_child_1),
+ Default(task=switch_child_2),
+ )
+
+ switch = Switch(name=TEST_NAME, condition=switch_condition)
+ parent >> switch
+ # General tasks test
+ assert len(pd.tasks) == 4
+ assert sorted(pd.task_list, key=lambda t: t.name) == sorted(
+ [parent, switch, switch_child_1, switch_child_2], key=lambda t: t.name
+ )
+ # Task dep test
+ assert parent._downstream_task_codes == {switch.code}
+ assert switch._upstream_task_codes == {parent.code}
+
+ # Switch task dep after ProcessDefinition function get_define called
+ assert switch._downstream_task_codes == {
+ switch_child_1.code,
+ switch_child_2.code,
+ }
+ assert all(
+ [
+ child._upstream_task_codes == {switch.code}
+ for child in [switch_child_1, switch_child_2]
+ ]
+ )
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/test_java_gateway.py b/dolphinscheduler-python/pydolphinscheduler/tests/test_java_gateway.py
new file mode 100644
index 0000000..3c8831e
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/test_java_gateway.py
@@ -0,0 +1,52 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test pydolphinscheduler java gateway."""
+
+
+from py4j.java_gateway import JavaGateway, java_import
+
+
+def test_gateway_connect():
+ """Test weather client could connect java gate way or not."""
+ gateway = JavaGateway()
+ app = gateway.entry_point
+ assert app.ping() == "PONG"
+
+
+def test_jvm_simple():
+ """Test use JVM build-in object and operator from java gateway."""
+ gateway = JavaGateway()
+ smaller = gateway.jvm.java.lang.Integer.MIN_VALUE
+ bigger = gateway.jvm.java.lang.Integer.MAX_VALUE
+ assert bigger > smaller
+
+
+def test_python_client_java_import_single():
+ """Test import single class from java gateway."""
+ gateway = JavaGateway()
+ java_import(gateway.jvm, "org.apache.dolphinscheduler.common.utils.FileUtils")
+ assert hasattr(gateway.jvm, "FileUtils")
+
+
+def test_python_client_java_import_package():
+ """Test import package contain multiple class from java gateway."""
+ gateway = JavaGateway()
+ java_import(gateway.jvm, "org.apache.dolphinscheduler.common.utils.*")
+ # test if jvm view have some common utils
+ for util in ("FileUtils", "OSUtils", "DateUtils"):
+ assert hasattr(gateway.jvm, util)
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/testing/__init__.py b/dolphinscheduler-python/pydolphinscheduler/tests/testing/__init__.py
new file mode 100644
index 0000000..c8caf5b
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init testing package, it provider easy way for pydolphinscheduler test."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/testing/task.py b/dolphinscheduler-python/pydolphinscheduler/tests/testing/task.py
new file mode 100644
index 0000000..e0affc9
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/task.py
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Mock class Task for other test."""
+
+import uuid
+
+from pydolphinscheduler.core.task import Task as SourceTask
+
+
+class Task(SourceTask):
+ """Mock class :class:`pydolphinscheduler.core.task.Task` for unittest."""
+
+ DEFAULT_VERSION = 1
+
+ def gen_code_and_version(self):
+ """Mock java gateway code and version, convenience method for unittest."""
+ return uuid.uuid1().time, self.DEFAULT_VERSION
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/utils/__init__.py b/dolphinscheduler-python/pydolphinscheduler/tests/utils/__init__.py
new file mode 100644
index 0000000..119f825
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/utils/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Init tests for utils package."""
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_date.py b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_date.py
new file mode 100644
index 0000000..b9f8ce5
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_date.py
@@ -0,0 +1,78 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test utils.date module."""
+
+from datetime import datetime
+
+import pytest
+
+from pydolphinscheduler.utils.date import FMT_STD, conv_from_str, conv_to_schedule
+
+curr_date = datetime.now()
+
+
+@pytest.mark.parametrize(
+ "src,expect",
+ [
+ (curr_date, curr_date.strftime(FMT_STD)),
+ (datetime(2021, 1, 1), "2021-01-01 00:00:00"),
+ (datetime(2021, 1, 1, 1), "2021-01-01 01:00:00"),
+ (datetime(2021, 1, 1, 1, 1), "2021-01-01 01:01:00"),
+ (datetime(2021, 1, 1, 1, 1, 1), "2021-01-01 01:01:01"),
+ (datetime(2021, 1, 1, 1, 1, 1, 1), "2021-01-01 01:01:01"),
+ ],
+)
+def test_conv_to_schedule(src: datetime, expect: str) -> None:
+ """Test function conv_to_schedule."""
+ assert expect == conv_to_schedule(src)
+
+
+@pytest.mark.parametrize(
+ "src,expect",
+ [
+ ("2021-01-01", datetime(2021, 1, 1)),
+ ("2021/01/01", datetime(2021, 1, 1)),
+ ("20210101", datetime(2021, 1, 1)),
+ ("2021-01-01 01:01:01", datetime(2021, 1, 1, 1, 1, 1)),
+ ("2021/01/01 01:01:01", datetime(2021, 1, 1, 1, 1, 1)),
+ ("20210101 010101", datetime(2021, 1, 1, 1, 1, 1)),
+ ],
+)
+def test_conv_from_str_success(src: str, expect: datetime) -> None:
+ """Test function conv_from_str success case."""
+ assert expect == conv_from_str(
+ src
+ ), f"Function conv_from_str convert {src} not expect to {expect}."
+
+
+@pytest.mark.parametrize(
+ "src",
+ [
+ "2021-01-01 010101",
+ "2021:01:01",
+ "202111",
+ "20210101010101",
+ "2021:01:01 01:01:01",
+ ],
+)
+def test_conv_from_str_not_impl(src: str) -> None:
+ """Test function conv_from_str fail case."""
+ with pytest.raises(
+ NotImplementedError, match=".*? could not be convert to datetime for now."
+ ):
+ conv_from_str(src)
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_string.py b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_string.py
new file mode 100644
index 0000000..2ccd206
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_string.py
@@ -0,0 +1,87 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Test utils.string module."""
+
+import pytest
+
+from pydolphinscheduler.utils.string import attr2camel, class_name2camel, snake2camel
+
+
+@pytest.mark.parametrize(
+ "snake, expect",
+ [
+ ("snake_case", "snakeCase"),
+ ("snake_123case", "snake123Case"),
+ ("snake_c_a_s_e", "snakeCASE"),
+ ("snake__case", "snakeCase"),
+ ("snake_case_case", "snakeCaseCase"),
+ ("_snake_case", "SnakeCase"),
+ ("__snake_case", "SnakeCase"),
+ ("Snake_case", "SnakeCase"),
+ ],
+)
+def test_snake2camel(snake: str, expect: str):
+ """Test function snake2camel, this is a base function for utils.string."""
+ assert expect == snake2camel(
+ snake
+ ), f"Test case {snake} do no return expect result {expect}."
+
+
+@pytest.mark.parametrize(
+ "attr, expects",
+ [
+ # source attribute, (true expect, false expect),
+ ("snake_case", ("snakeCase", "snakeCase")),
+ ("snake_123case", ("snake123Case", "snake123Case")),
+ ("snake_c_a_s_e", ("snakeCASE", "snakeCASE")),
+ ("snake__case", ("snakeCase", "snakeCase")),
+ ("snake_case_case", ("snakeCaseCase", "snakeCaseCase")),
+ ("_snake_case", ("snakeCase", "SnakeCase")),
+ ("__snake_case", ("snakeCase", "SnakeCase")),
+ ("Snake_case", ("SnakeCase", "SnakeCase")),
+ ],
+)
+def test_attr2camel(attr: str, expects: tuple):
+ """Test function attr2camel."""
+ for idx, expect in enumerate(expects):
+ include_private = idx % 2 == 0
+ assert expect == attr2camel(
+ attr, include_private
+ ), f"Test case {attr} do no return expect result {expect} when include_private is {include_private}."
+
+
+@pytest.mark.parametrize(
+ "class_name, expect",
+ [
+ ("snake_case", "snakeCase"),
+ ("snake_123case", "snake123Case"),
+ ("snake_c_a_s_e", "snakeCASE"),
+ ("snake__case", "snakeCase"),
+ ("snake_case_case", "snakeCaseCase"),
+ ("_snake_case", "snakeCase"),
+ ("_Snake_case", "snakeCase"),
+ ("__snake_case", "snakeCase"),
+ ("__Snake_case", "snakeCase"),
+ ("Snake_case", "snakeCase"),
+ ],
+)
+def test_class_name2camel(class_name: str, expect: str):
+ """Test function class_name2camel."""
+ assert expect == class_name2camel(
+ class_name
+ ), f"Test case {class_name} do no return expect result {expect}."
diff --git a/dolphinscheduler-python/src/main/java/org/apache/dolphinscheduler/server/PythonGatewayServer.java b/dolphinscheduler-python/src/main/java/org/apache/dolphinscheduler/server/PythonGatewayServer.java
index 09d9773..79087cf 100644
--- a/dolphinscheduler-python/src/main/java/org/apache/dolphinscheduler/server/PythonGatewayServer.java
+++ b/dolphinscheduler-python/src/main/java/org/apache/dolphinscheduler/server/PythonGatewayServer.java
@@ -30,13 +30,13 @@ import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.Constants;
import org.apache.dolphinscheduler.common.enums.FailureStrategy;
import org.apache.dolphinscheduler.common.enums.Priority;
-import org.apache.dolphinscheduler.common.enums.ProcessExecutionTypeEnum;
import org.apache.dolphinscheduler.common.enums.ReleaseState;
import org.apache.dolphinscheduler.common.enums.RunMode;
import org.apache.dolphinscheduler.common.enums.TaskDependType;
import org.apache.dolphinscheduler.common.enums.UserType;
import org.apache.dolphinscheduler.common.enums.WarningType;
import org.apache.dolphinscheduler.common.utils.CodeGenerateUtils;
+import org.apache.dolphinscheduler.dao.entity.DataSource;
import org.apache.dolphinscheduler.dao.entity.ProcessDefinition;
import org.apache.dolphinscheduler.dao.entity.Project;
import org.apache.dolphinscheduler.dao.entity.Queue;
@@ -44,6 +44,7 @@ import org.apache.dolphinscheduler.dao.entity.Schedule;
import org.apache.dolphinscheduler.dao.entity.TaskDefinition;
import org.apache.dolphinscheduler.dao.entity.Tenant;
import org.apache.dolphinscheduler.dao.entity.User;
+import org.apache.dolphinscheduler.dao.mapper.DataSourceMapper;
import org.apache.dolphinscheduler.dao.mapper.ProcessDefinitionMapper;
import org.apache.dolphinscheduler.dao.mapper.ProjectMapper;
import org.apache.dolphinscheduler.dao.mapper.ScheduleMapper;
@@ -60,24 +61,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.FilterType;
import py4j.GatewayServer;
-@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = {
- @ComponentScan.Filter(type = FilterType.REGEX, pattern = {
- "org.apache.dolphinscheduler.server.master.*",
- "org.apache.dolphinscheduler.server.worker.*",
- "org.apache.dolphinscheduler.server.monitor.*",
- "org.apache.dolphinscheduler.server.log.*",
- "org.apache.dolphinscheduler.alert.*"
- })
-})
+@SpringBootApplication
+@ComponentScan(value = "org.apache.dolphinscheduler")
public class PythonGatewayServer extends SpringBootServletInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(PythonGatewayServer.class);
-
+
private static final WarningType DEFAULT_WARNING_TYPE = WarningType.NONE;
private static final int DEFAULT_WARNING_GROUP_ID = 0;
private static final FailureStrategy DEFAULT_FAILURE_STRATEGY = FailureStrategy.CONTINUE;
@@ -124,6 +118,9 @@ public class PythonGatewayServer extends SpringBootServletInitializer {
@Autowired
private ScheduleMapper scheduleMapper;
+ @Autowired
+ private DataSourceMapper dataSourceMapper;
+
// TODO replace this user to build in admin user if we make sure build in one could not be change
private final User dummyAdminUser = new User() {
{
@@ -202,34 +199,26 @@ public class PythonGatewayServer extends SpringBootServletInitializer {
String workerGroup,
String tenantCode,
String taskRelationJson,
- String taskDefinitionJson,
- ProcessExecutionTypeEnum executionType) {
+ String taskDefinitionJson) {
User user = usersService.queryUser(userName);
Project project = (Project) projectService.queryByName(user, projectName).get(Constants.DATA_LIST);
long projectCode = project.getCode();
- Map<String, Object> verifyProcessDefinitionExists = processDefinitionService.verifyProcessDefinitionName(user, projectCode, name);
- Status verifyStatus = (Status) verifyProcessDefinitionExists.get(Constants.STATUS);
-
+ ProcessDefinition processDefinition = getProcessDefinition(user, projectCode, name);
long processDefinitionCode;
// create or update process definition
- if (verifyStatus == Status.PROCESS_DEFINITION_NAME_EXIST) {
- ProcessDefinition processDefinition = processDefinitionMapper.queryByDefineName(projectCode, name);
+ if (processDefinition != null) {
processDefinitionCode = processDefinition.getCode();
// make sure process definition offline which could edit
processDefinitionService.releaseProcessDefinition(user, projectCode, processDefinitionCode, ReleaseState.OFFLINE);
Map<String, Object> result = processDefinitionService.updateProcessDefinition(user, projectCode, name, processDefinitionCode, description, globalParams,
- locations, timeout, tenantCode, taskRelationJson, taskDefinitionJson,executionType);
- } else if (verifyStatus == Status.SUCCESS) {
+ locations, timeout, tenantCode, taskRelationJson, taskDefinitionJson);
+ } else {
Map<String, Object> result = processDefinitionService.createProcessDefinition(user, projectCode, name, description, globalParams,
- locations, timeout, tenantCode, taskRelationJson, taskDefinitionJson,executionType);
- ProcessDefinition processDefinition = (ProcessDefinition) result.get(Constants.DATA_LIST);
+ locations, timeout, tenantCode, taskRelationJson, taskDefinitionJson);
+ processDefinition = (ProcessDefinition) result.get(Constants.DATA_LIST);
processDefinitionCode = processDefinition.getCode();
- } else {
- String msg = "Verify process definition exists status is invalid, neither SUCCESS or PROCESS_DEFINITION_NAME_EXIST.";
- LOGGER.error(msg);
- throw new RuntimeException(msg);
}
-
+
// Fresh process definition schedule
if (schedule != null) {
createOrUpdateSchedule(user, projectCode, processDefinitionCode, schedule, workerGroup);
@@ -239,6 +228,28 @@ public class PythonGatewayServer extends SpringBootServletInitializer {
}
/**
+ * get process definition
+ * @param user user who create or update schedule
+ * @param projectCode project which process definition belongs to
+ * @param processDefinitionName process definition name
+ */
+ private ProcessDefinition getProcessDefinition(User user, long projectCode, String processDefinitionName) {
+ Map<String, Object> verifyProcessDefinitionExists = processDefinitionService.verifyProcessDefinitionName(user, projectCode, processDefinitionName);
+ Status verifyStatus = (Status) verifyProcessDefinitionExists.get(Constants.STATUS);
+
+ ProcessDefinition processDefinition = null;
+ if (verifyStatus == Status.PROCESS_DEFINITION_NAME_EXIST) {
+ processDefinition = processDefinitionMapper.queryByDefineName(projectCode, processDefinitionName);
+ } else if (verifyStatus != Status.SUCCESS) {
+ String msg = "Verify process definition exists status is invalid, neither SUCCESS or PROCESS_DEFINITION_NAME_EXIST.";
+ LOGGER.error(msg);
+ throw new RuntimeException(msg);
+ }
+
+ return processDefinition;
+ }
+
+ /**
* create or update process definition schedule.
* It would always use latest schedule define in workflow-as-code, and set schedule online when
* it's not null
@@ -360,11 +371,103 @@ public class PythonGatewayServer extends SpringBootServletInitializer {
}
}
+ /**
+ * Get datasource by given datasource name. It return map contain datasource id, type, name.
+ * Useful in Python API create sql task which need datasource information.
+ *
+ * @param datasourceName user who create or update schedule
+ */
+ public Map<String, Object> getDatasourceInfo(String datasourceName) {
+ Map<String, Object> result = new HashMap<>();
+ List<DataSource> dataSourceList = dataSourceMapper.queryDataSourceByName(datasourceName);
+ if (dataSourceList.size() > 1) {
+ String msg = String.format("Get more than one datasource by name %s", datasourceName);
+ logger.error(msg);
+ throw new IllegalArgumentException(msg);
+ } else if (dataSourceList.size() == 0) {
+ String msg = String.format("Can not find any datasource by name %s", datasourceName);
+ logger.error(msg);
+ throw new IllegalArgumentException(msg);
+ } else {
+ DataSource dataSource = dataSourceList.get(0);
+ result.put("id", dataSource.getId());
+ result.put("type", dataSource.getType().name());
+ result.put("name", dataSource.getName());
+ }
+ return result;
+ }
+
+ /**
+ * Get processDefinition by given processDefinitionName name. It return map contain processDefinition id, name, code.
+ * Useful in Python API create subProcess task which need processDefinition information.
+ *
+ * @param userName user who create or update schedule
+ * @param projectName project name which process definition belongs to
+ * @param processDefinitionName process definition name
+ */
+ public Map<String, Object> getProcessDefinitionInfo(String userName, String projectName, String processDefinitionName) {
+ Map<String, Object> result = new HashMap<>();
+
+ User user = usersService.queryUser(userName);
+ Project project = (Project) projectService.queryByName(user, projectName).get(Constants.DATA_LIST);
+ long projectCode = project.getCode();
+ ProcessDefinition processDefinition = getProcessDefinition(user, projectCode, processDefinitionName);
+ // get process definition info
+ if (processDefinition != null) {
+ // make sure process definition online
+ processDefinitionService.releaseProcessDefinition(user, projectCode, processDefinition.getCode(), ReleaseState.ONLINE);
+ result.put("id", processDefinition.getId());
+ result.put("name", processDefinition.getName());
+ result.put("code", processDefinition.getCode());
+ } else {
+ String msg = String.format("Can not find valid process definition by name %s", processDefinitionName);
+ logger.error(msg);
+ throw new IllegalArgumentException(msg);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get project, process definition, task code.
+ * Useful in Python API create dependent task which need processDefinition information.
+ *
+ * @param projectName project name which process definition belongs to
+ * @param processDefinitionName process definition name
+ * @param taskName task name
+ */
+ public Map<String, Object> getDependentInfo(String projectName, String processDefinitionName, String taskName) {
+ Map<String, Object> result = new HashMap<>();
+
+ Project project = projectMapper.queryByName(projectName);
+ if (project == null) {
+ String msg = String.format("Can not find valid project by name %s", projectName);
+ logger.error(msg);
+ throw new IllegalArgumentException(msg);
+ }
+ long projectCode = project.getCode();
+ result.put("projectCode", projectCode);
+
+ ProcessDefinition processDefinition = processDefinitionMapper.queryByDefineName(projectCode, processDefinitionName);
+ if (processDefinition == null) {
+ String msg = String.format("Can not find valid process definition by name %s", processDefinitionName);
+ logger.error(msg);
+ throw new IllegalArgumentException(msg);
+ }
+ result.put("processDefinitionCode", processDefinition.getCode());
+
+ if (taskName != null) {
+ TaskDefinition taskDefinition = taskDefinitionMapper.queryByName(projectCode, taskName);
+ result.put("taskDefinitionCode", taskDefinition.getCode());
+ }
+ return result;
+ }
+
@PostConstruct
public void run() {
GatewayServer server = new GatewayServer(this);
GatewayServer.turnLoggingOn();
- // Start server to accept python client RPC
+ // Start server to accept python client socket
server.start();
}
diff --git a/dolphinscheduler-server/src/main/resources/config/install_config.conf b/dolphinscheduler-server/src/main/resources/config/install_config.conf
index 88248ea..b3ed2ab 100755
--- a/dolphinscheduler-server/src/main/resources/config/install_config.conf
+++ b/dolphinscheduler-server/src/main/resources/config/install_config.conf
@@ -48,6 +48,11 @@ alertServer="ds3"
# Example for hostname: apiServers="ds1", Example for IP: apiServers="192.168.8.1"
apiServers="ds1"
+# A comma separated list of machine hostname or IP would be installed Python gateway server, it
+# must be a subset of configuration `ips`.
+# Example for hostname: pythonGatewayServers="ds1", Example for IP: pythonGatewayServers="192.168.8.1"
+pythonGatewayServers="ds1"
+
# The directory to install DolphinScheduler for all machine we config above. It will automatically be created by `install.sh` script if not exists.
# Do not set this configuration same as the current path (pwd)
installPath="/data1_1T/dolphinscheduler"
diff --git a/dolphinscheduler-standalone-server/pom.xml b/dolphinscheduler-standalone-server/pom.xml
index 8382037..104a66c 100644
--- a/dolphinscheduler-standalone-server/pom.xml
+++ b/dolphinscheduler-standalone-server/pom.xml
@@ -35,6 +35,10 @@
<artifactId>dolphinscheduler-api</artifactId>
</dependency>
<dependency>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-python</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-test</artifactId>
<version>${curator.test}</version>
diff --git a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java
index 2ace183..05204b2 100644
--- a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java
+++ b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java
@@ -40,6 +40,6 @@ public class StandaloneServer {
MasterServer.class,
WorkerServer.class,
AlertServer.class
- ).profiles("master", "worker", "api", "alert", "h2", "standalone").run(args);
+ ).profiles("master", "worker", "api", "alert", "python-gateway", "h2", "standalone").run(args);
}
}
diff --git a/pom.xml b/pom.xml
index 3142f87..6de49db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1142,5 +1142,6 @@
<module>dolphinscheduler-service</module>
<module>dolphinscheduler-standalone-server</module>
<module>dolphinscheduler-datasource-plugin</module>
+ <module>dolphinscheduler-python</module>
</modules>
</project>
diff --git a/script/dolphinscheduler-daemon.sh b/script/dolphinscheduler-daemon.sh
index 19f8373..b8a1c16 100755
--- a/script/dolphinscheduler-daemon.sh
+++ b/script/dolphinscheduler-daemon.sh
@@ -16,7 +16,7 @@
# limitations under the License.
#
-usage="Usage: dolphinscheduler-daemon.sh (start|stop|status) <api-server|master-server|worker-server|alert-server|standalone-server> "
+usage="Usage: dolphinscheduler-daemon.sh (start|stop|status) <api-server|master-server|worker-server|alert-server|standalone-server|python-gateway-server> "
# if no args specified, show usage
if [ $# -le 1 ]; then
@@ -96,6 +96,9 @@ elif [ "$command" = "logger-server" ]; then
elif [ "$command" = "standalone-server" ]; then
CLASS=org.apache.dolphinscheduler.server.StandaloneServer
export SPRING_PROFILES_ACTIVE="${SPRING_PROFILES_ACTIVE},standalone,${DATABASE_TYPE}"
+elif [ "$command" = "python-gateway-server" ]; then
+ CLASS=org.apache.dolphinscheduler.server.PythonGatewayServer
+ export SPRING_PROFILES_ACTIVE="${SPRING_PROFILES_ACTIVE},python-gateway,${DATABASE_TYPE}"
else
echo "Error: No command named '$command' was found."
exit 1
diff --git a/script/start-all.sh b/script/start-all.sh
index b024d6c..b83a545 100755
--- a/script/start-all.sh
+++ b/script/start-all.sh
@@ -59,6 +59,13 @@ do
ssh -p $sshPort $apiServer "cd $installPath/; sh bin/dolphinscheduler-daemon.sh start api-server;"
done
+pythonGatewayServersHost=(${pythonGatewayServers//,/ })
+for pythonGatewayServer in ${pythonGatewayServersHost[@]}
+do
+ echo "$pythonGatewayServer worker server is starting"
+ ssh -p $sshPort $pythonGatewayServer "cd $installPath/; sh bin/dolphinscheduler-daemon.sh start python-gateway-server;"
+done
+
# query server status
echo "query server status"
cd $installPath/; sh bin/status-all.sh
diff --git a/script/status-all.sh b/script/status-all.sh
index 24cdb75..a58f671 100755
--- a/script/status-all.sh
+++ b/script/status-all.sh
@@ -78,3 +78,11 @@ do
apiState=`ssh -p $sshPort $apiServer "cd $installPath/; sh bin/dolphinscheduler-daemon.sh status api-server;"`
echo "$apiServer $apiState"
done
+
+# 5.python gateway server check state
+pythonGatewayServersHost=(${pythonGatewayServers//,/ })
+for pythonGateway in ${pythonGatewayServersHost[@]}
+do
+ pythonGatewayState=`ssh -p $sshPort $pythonGateway "cd $installPath/; sh bin/dolphinscheduler-daemon.sh status python-gateway-server;"`
+ echo "$pythonGateway $pythonGatewayState"
+done
diff --git a/script/stop-all.sh b/script/stop-all.sh
index 884f1d9..77017a0 100755
--- a/script/stop-all.sh
+++ b/script/stop-all.sh
@@ -58,6 +58,13 @@ do
ssh -p $sshPort $apiServer "cd $installPath/; sh bin/dolphinscheduler-daemon.sh stop api-server;"
done
+pythonGatewayServersHost=(${pythonGatewayServers//,/ })
+for pythonGatewayServer in ${pythonGatewayServersHost[@]}
+do
+ echo "$pythonGatewayServer python gateway server is stopping"
+ ssh -p $sshPort $pythonGatewayServer "cd $installPath/; sh bin/dolphinscheduler-daemon.sh stop python-gateway-server;"
+done
+
# query server status
echo "query server status"
cd $installPath/; sh bin/status-all.sh
diff --git a/tools/dependencies/known-dependencies.txt b/tools/dependencies/known-dependencies.txt
index b2c9c2d..df30815 100755
--- a/tools/dependencies/known-dependencies.txt
+++ b/tools/dependencies/known-dependencies.txt
@@ -171,6 +171,7 @@ protostuff-core-1.7.2.jar
protostuff-runtime-1.7.2.jar
protostuff-api-1.7.2.jar
protostuff-collectionschema-1.7.2.jar
+py4j-0.10.9.jar
quartz-2.3.0.jar
quartz-jobs-2.3.0.jar
reflections-0.9.12.jar