Repository: sql-machine-learning/elasticdl

Issue:
Add entry point to client.py for better CLI user experience
Currently, we support subcommands using:
```
python elasticdl/python/elasticdl/client/client.py train
python elasticdl/python/elasticdl/client/client.py evaluate
```
This is not very user-friendly since it requires users to know the path to `client.py`. Instead, in `setup.py` file, we can add `entry_points` for `console_scripts` ([reference](https://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point)) to `client.py` so when elasticdl Python package is installed, users can just do the following:
```
elasticdl train
elasticdl evaluate
``` 


Discussion:

Resolving patch:
diff --git a/elasticdl/python/elasticdl/client/client.py b/elasticdl/python/elasticdl/client/client.py
--- a/elasticdl/python/elasticdl/client/client.py
+++ b/elasticdl/python/elasticdl/client/client.py
@@ -5,6 +5,8 @@
 
 import docker
 import yaml
+import shutil
+from string import Template
 
 from kubernetes.client.apis import core_v1_api
 from kubernetes import config
@@ -40,16 +42,15 @@ def _add_train_params(parser):
     parser.add_argument(
         "--model_file", help="Path to the model file", required=True
     )
-    parser.add_argument(
-        "--image_base",
-        help="Base image containing ElasticDL runtime environment",
-        required=True,
-    )
     parser.add_argument(
         "--push_image",
         action="store_true",
         help="Whether to push the newly built image to remote registry",
     )
+    parser.add_argument(
+        "--image_name",
+        help="The docker image name built by ElasticDL client",
+    )
     parser.add_argument("--job_name", help="ElasticDL job name", required=True)
     parser.add_argument(
         "--master_resource_request",
@@ -93,6 +94,9 @@ def _add_train_params(parser):
         default="Always",
         help="The image pull policy of master and worker",
     )
+    parser.add_argument(
+        "--extra_pypi_index", help="The extra python package repository"
+    )
 
 
 def _add_evaluate_params(parser):
@@ -102,14 +106,10 @@ def _add_evaluate_params(parser):
 
 def _train(args, argv):
     job_name = args.job_name
-    image_name = args.image_base + "_" + job_name
     _build_docker_image(
-        args.model_file,
-        image_name,
-        args.push_image,
-        image_base=args.image_base,
+        args.model_file, args.image_name, args.push_image, args.extra_pypi_index
     )
-    _submit(image_name, args.model_file, job_name, args, argv)
+    _submit(args.image_name, args.model_file, job_name, args, argv)
 
 
 def _evaluate(args, argv):
@@ -121,34 +121,64 @@ def _m_file_in_docker(model_file):
     return "/model/" + os.path.basename(model_file)
 
 
-def _build_docker_image(
-    m_file, image_name, push_image, image_base="elasticdl:dev"
-):
+def _build_docker_image(m_file, image_name, push_image, extra_pypi_index):
     docker_template = """
-FROM {}
-COPY {} {}
-"""
+FROM tensorflow/tensorflow:2.0.0b0-py3 as base
 
-    with tempfile.NamedTemporaryFile(mode="w+", delete=False) as df:
-        df.write(
-            docker_template.format(
-                image_base, m_file, _m_file_in_docker(m_file)
-            )
-        )
+# Install gRPC tools in Python
+RUN pip install grpcio-tools --extra-index-url={EXTRA_PYPI_INDEX}
+
+# Install the Kubernetes Python client
+RUN pip install kubernetes --extra-index-url={EXTRA_PYPI_INDEX}
+
+# Install Docker python SDK
+RUN pip install docker --extra-index-url={EXTRA_PYPI_INDEX}
+
+# Install RecordIO
+RUN pip install 'pyrecordio>=0.0.6' --extra-index-url={EXTRA_PYPI_INDEX}
 
-    client = docker.APIClient(base_url="unix://var/run/docker.sock")
-    print("===== Building Docker Image =====")
-    for line in client.build(
-        dockerfile=df.name, path=".", rm=True, tag=image_name, decode=True
-    ):
-        if "error" in line:
-            raise RuntimeError(
-                "Docker image build failure: %s" % line["error"]
+# Install Pillow for sample data processing Spark job
+RUN pip install Pillow --extra-index-url=${EXTRA_PYPI_INDEX}
+
+ENV PYTHONPATH=/
+WORKDIR /
+COPY elasticdl /elasticdl
+COPY elasticdl/Makefile /Makefile
+RUN make
+COPY {SOURCE_MODEL_FILE} {TARGET_MODEL_FILE}
+"""
+    with tempfile.TemporaryDirectory() as ctx_dir:
+        shutil.copy(m_file, ctx_dir)
+        base_dir = os.path.abspath(
+            os.path.join(os.path.dirname(__file__), "../../../")
+        )
+        shutil.copytree(base_dir, ctx_dir + "/" + os.path.basename(base_dir))
+        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as df:
+            df.write(
+                docker_template.format(
+                    SOURCE_MODEL_FILE=os.path.basename(m_file),
+                    TARGET_MODEL_FILE=_m_file_in_docker(m_file),
+                    EXTRA_PYPI_INDEX=extra_pypi_index,
+                )
             )
-        text = line.get("stream", None)
-        if text:
-            sys.stdout.write(text)
-            sys.stdout.flush()
+        client = docker.APIClient(base_url="unix://var/run/docker.sock")
+        print("===== Building Docker Image =====")
+        for line in client.build(
+            dockerfile=df.name,
+            path=ctx_dir,
+            rm=True,
+            tag=image_name,
+            decode=True,
+        ):
+            if "error" in line:
+                raise RuntimeError(
+                    "Docker image build failure: %s" % line["error"]
+                )
+            text = line.get("stream", None)
+            if text:
+                sys.stdout.write(text)
+                sys.stdout.flush()
+
     if push_image:
         print("===== Pushing Docker Image =====")
         for line in client.push(image_name, stream=True, decode=True):
diff --git a/elasticdl/python/examples/__init__.py b/elasticdl/python/examples/__init__.py
new file mode 100644
diff --git a/setup.py b/setup.py
new file mode 100644
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,15 @@
+from setuptools import setup, find_packages
+
+
+setup(
+    name="ElasticDL",
+    version='0.0.1',
+    description="A Kubernetes-naitve Elastic Deep Learning Framework",
+    author="Ant Financial",
+    url="https://github.com/wangkuiyi/elasticdl",
+    packages=find_packages(exclude=['*test*']),
+    package_data={'': ['proto/elasticdl.proto', 'docker/*', 'Makefile']},
+    entry_points = {
+        'console_scripts': ['elasticdl=elasticdl.python.elasticdl.client.client:main'],
+    },
+)


Test patch: