# HG changeset patch
# User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>
# Date 1599826184 -7200
#      Fri Sep 11 14:09:44 2020 +0200
# Node ID 60350f857f141fd8fc9ae605b638012e8747c0e4
# Parent  9889f48ee32d547a4c1ff0be78fa9553c43fbf2e
✨ update python docker requirements to use the API directly rather than using subprocesses

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,7 +12,7 @@
   TAG_LATEST: branch/default
   DOCKER_IMAGE: xcgd/odoo_scripts
   HTML_DOC_SOURCES: doc/_build/html
-  SH_SCRIPTS: isort create_archive start
+  SH_SCRIPTS: create_archive start
 
 checkbashisms:
   allow_failure: true
diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -23,6 +23,12 @@
 
 Colored logs.
 
+Docker calls are now done with the API rather than with subprocesses.
+
+:file:`isort` shell script has been removed, it is now available as a python 3 script :ref:`docker_isort`.
+
+Logging default is now the info level. Use `-q`/`--quiet` to decrease verbosity.
+
 4.1
 ---
 
diff --git a/README.rst b/README.rst
--- a/README.rst
+++ b/README.rst
@@ -73,17 +73,6 @@
 
 zsh script to create a tar.xz file containing all sources.
 
-isort
------
-
-Run dockerized isort on current directory. This uses a configuration file that is adapted to OCA guidelines for imports.
-
-For help:
-
-.. code-block:: SH
-
-    ./isort --help
-
 Scripts
 =======
 
@@ -94,6 +83,13 @@
 The prerequisites for this module or one of its section can be installed by using pip3 or the package manager; the requirements are defined in ``setup.py``.
 As there is no way of indicating sections (it is even handling them incorrectly), do not use ``python3 setup.py install``.
 
+docker_isort
+------------
+
+Run dockerized isort on current directory. This uses a configuration file that is adapted to OCA guidelines for imports.
+
+This is part of the docker section.
+
 conf2reST.py
 ------------
 
@@ -223,7 +219,7 @@
 
 Generate completion file for python scripts (from the superproject for the scripts that need to be run from there, and with the required requirements too)::
 
-  for command_name in docker_dev_start docker_build docker_build_copy docker_build_clean do_tests conf2reST import_base_import import_jsonrpc import_sql ; do
+  for command_name in docker_dev_start docker_build docker_build_copy docker_build_clean do_tests conf2reST import_base_import import_jsonrpc import_sql docker_flake8 docker_isort ; do
     $command_name --help | ~/src/zsh-completion-generator/help2comp.py $command_name > ~/.local/share/zsh/completion/_$command_name
   done
 
diff --git a/isort b/isort
deleted file mode 100755
--- a/isort
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-/usr/bin/docker run --rm --volume "$PWD:/mnt" -ti -w /mnt xcgd/isort:odoo isort "$@"
diff --git a/odoo_scripts/conf2reST.py b/odoo_scripts/conf2reST.py
--- a/odoo_scripts/conf2reST.py
+++ b/odoo_scripts/conf2reST.py
@@ -1,7 +1,6 @@
 #!/usr/bin/env python3
 """Generate a reST file from configuration files.
 """
-import argparse
 import configparser
 import logging
 import os
@@ -10,7 +9,7 @@
 
 import yaml
 
-from .parsing import add_verbosity_to_parser, logging_from_verbose
+from .parsing import apply, basic_parser
 
 _logger = logging.getLogger(__name__)
 
@@ -50,15 +49,10 @@
         str(__date__),
     )
     # Argument parsing
-    parser = argparse.ArgumentParser(
-        description=program_license,
-        formatter_class=argparse.RawDescriptionHelpFormatter,
+    parser = basic_parser(
+        description=program_license, version=program_version_message
     )
     parser.add_argument(
-        "-V", "--version", action="version", version=program_version_message
-    )
-    add_verbosity_to_parser(parser)
-    parser.add_argument(
         "-d",
         "--directory",
         help="Source directory [default: %(default)s]",
@@ -67,7 +61,7 @@
     parser.add_argument("-o", "--output", help="Output file name")
 
     nmspc = parser.parse_args()
-    logging_from_verbose(nmspc)
+    apply(nmspc)
     conf2rst(nmspc.directory, nmspc.output)
 
 
diff --git a/odoo_scripts/do_tests.py b/odoo_scripts/do_tests.py
--- a/odoo_scripts/do_tests.py
+++ b/odoo_scripts/do_tests.py
@@ -1,37 +1,29 @@
 #!/usr/bin/env python3
 # vim: set shiftwidth=4 softtabstop=4:
 """Script to run test with local modules
-
-isort:skip_file
 """
 # Version 4.2.1
-import argparse
-from configparser import ConfigParser
 import logging
 import os
+import sys
+from configparser import ConfigParser
 from subprocess import call
-import sys
 
-import docker  # apt python3-docker (1.9) or pip3 install docker
-from psycopg2 import connect, OperationalError  # apt python3-psycopg2
+from odoo_scripts.docker_postgresql import docker_run_postgresql
+from psycopg2 import OperationalError, connect
 
-from .docker_dev_start import (
-    docker_run_postgresql,
-    flake8,
-    main as docker_dev_start_main,
-)
 from .config import Config
-
-if docker.__version__ > "2.5.0":
-    from docker import APIClient as docker_api
-else:
-    from docker import Client as docker_api
+from .docker_dev_start import main as docker_dev_start_main
+from .docker_flake8 import flake8
+from .docker_isort import isort
+from .docker_postgresql import POSTGRES_PASSWORD
+from .parsing import apply, basic_parser
 
 _logger = logging.getLogger(__name__)
 
-__version__ = "1.0.2"
+__version__ = "2.0.0"
 __date__ = "2018-04-13"
-__updated__ = "2020-09-02"
+__updated__ = "2020-09-15"
 
 
 def main(argv=None):  # IGNORE:C0111
@@ -60,32 +52,14 @@
         str(__date__),
     )
 
-    # TODO the script assume it is launched from the parent super project
+    # the script assume it is launched from the super project
     project_path = os.path.realpath(".")
     project_name = os.path.basename(project_path)
-    # XXX is this test still valid?
-    if project_name == "odoo_scripts":
-        logging.fatal(
-            "You must run this script from the super project"
-            " (docker_build.py)"
-        )
-        return 1
 
     # TODO add a way to store configuration options in a project file
     # Argument parsing
-    parser = argparse.ArgumentParser(
-        description=program_license,
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-    )
-    parser.add_argument(
-        "-V", "--version", action="version", version=program_version_message
-    )
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        dest="verbose",
-        action="count",
-        help="set verbosity level [default: %(default)s]",
+    parser = basic_parser(
+        description=program_license, version=program_version_message
     )
     docker_group = parser.add_mutually_exclusive_group()
     docker_group.add_argument(
@@ -173,13 +147,7 @@
     # - db user for creation/remove
 
     nmspc = parser.parse_args(argv)
-    verbose = nmspc.verbose
-    if not verbose:
-        logging.basicConfig(level=logging.WARN)
-    if verbose == 1:
-        logging.basicConfig(level=logging.INFO)
-    if verbose and verbose > 1:
-        logging.basicConfig(level=logging.DEBUG)
+    apply(nmspc)
     dbname = nmspc.dbname
     odoo_db_user = nmspc.db_user
     odoo_db_password = nmspc.db_password
@@ -254,9 +222,9 @@
             "-d",
             dbname,
             "--stop-after-init",
-            "--max-cron-threads",
-            "0",
+            "--max-cron-threads=0",
             "--no-flake8",
+            "--no-isort",
             "--no-dev",
         ]
         if odoo_db_user:
@@ -269,7 +237,9 @@
             args.append("--dbport")
             args.append(dbport)
 
+    # only run flake8 and isort once rather than once per docker_dev_start call
     flake8(odoo_type)
+    isort()
     if nmspc.log_handler:
         for lh in nmspc.log_handler:
             args.append("--log-handler")
@@ -281,9 +251,8 @@
         args.append(nmspc.LOAD_LANGUAGE)
     if recreate_db:
         if start_postgresql:
-            docker_client = docker_api(base_url="unix://var/run/docker.sock")
-            name, stop_method, socket_path = docker_run_postgresql(
-                docker_client, project_name, postgresql_version, None
+            container, stop_method, socket_path = docker_run_postgresql(
+                project_name, postgresql_version
             )
             # system user used in the pg docker image
             pg_user = "postgres"
@@ -302,8 +271,10 @@
                     user=pg_user,
                     database="postgres",
                     host=socket_path,
-                    post=5432,
+                    port=5432,
+                    password=POSTGRES_PASSWORD,
                 )
+                _logger.info("Creating role %s", odoo_db_user)
                 with connection.cursor() as cursor:
                     # not injection safe but you are on your own machine
                     # with already full access to db
@@ -333,7 +304,9 @@
                 for extension in extensions:
                     _logger.info("Adding extension %s", extension)
                     cursor.execute("CREATE EXTENSION %s" % extension)
-            stop_method()
+            # TODO do we really need to stop the container each time?
+            #  that seems faster
+            # stop_method()
         else:
             # TODO use psycopg2 (see above)
             # drop database
diff --git a/odoo_scripts/docker_build.py b/odoo_scripts/docker_build.py
--- a/odoo_scripts/docker_build.py
+++ b/odoo_scripts/docker_build.py
@@ -11,25 +11,17 @@
 import sys
 from subprocess import call, check_output
 
-import docker  # apt python3-docker (1.9) or pip3 install docker
-
 from .config import Config
 from .docker_build_clean import clean
 from .docker_build_copy import copy
-from .parsing import basic_parser, logging_from_verbose
-
-# TODO change requirements to increase docker version to avoid that
-if docker.__version__ > "2.5.0":
-    from docker import APIClient as docker_api
-else:
-    from docker import Client as docker_api
-
+from .docker_client import DockerClient
+from .parsing import apply, basic_parser
 
 _logger = logging.getLogger(__name__)
 
-__version__ = "0.2.0"
+__version__ = "1.0.0"
 __date__ = "2018-04-04"
-__updated__ = "2020-06-30"
+__updated__ = "2020-09-11"
 
 
 def __parser():
@@ -43,7 +35,7 @@
     program_license = """%s
 
       Created by Vincent Hatakeyama on %s.
-      Copyright 2018, 2020 XCG Consulting. All rights reserved.
+      Copyright 2018, 2019, 2020 XCG Consulting. All rights reserved.
 
       Licensed under the MIT License
 
@@ -66,7 +58,7 @@
     project_path = os.path.realpath(".")
     project_name = os.path.basename(project_path)
     if project_name == "odoo_scripts":
-        logging.fatal("You must run this script from the super project")
+        _logger.fatal("You must run this script from the super project")
         return 1
 
     # TODO add a way to store configuration options in a project file
@@ -101,10 +93,8 @@
     )
     # TODO (maybe) add argument for other build arg
 
-    # TODO detect that user is member of docker group
-
     nmspc = parser.parse_args(argv)
-    logging_from_verbose(nmspc)
+    apply(nmspc)
     ensureconf = nmspc.ensureconf
     dev = nmspc.dev
     push = nmspc.push
@@ -114,7 +104,7 @@
     project = c.image
     odoo_type = c.odoo_type
     image = "%s/%s:latest" % (registry, project)
-    logging.debug("Docker image: %s", image)
+    _logger.debug("Docker image: %s", image)
     # TODO ensureconf
     if ensureconf:
         raise NotImplementedError
@@ -127,23 +117,26 @@
         clean()
         # XXX needed?
 
-    #    sys.exit(0)
-
     signal.signal(signal.SIGINT, signal_handler)
     signal.signal(signal.SIGTERM, signal_handler)
 
-    # TODO docker build
     buildargs = dict()
     if os.path.exists(".hg"):
-        buildargs["REVISION"] = (
-            check_output("hg identify -i".split()).split()[0].decode("utf-8")
-        )
-    buildargs["CREATED"] = datetime.datetime.now().isoformat()
+        tags = check_output(["hg", "identify", "--tags"]).decode()
+        if tags:
+            buildargs["VERSION"] = tags
+        buildargs["VCS_URL"] = check_output(
+            ["hg", "paths", "default"]
+        ).decode()
+        buildargs["VCS_REF"] = check_output(
+            ["hg", "identify", "--id"]
+        ).decode()
+    buildargs["BUILD_DATE"] = datetime.datetime.now().isoformat()
     if nmspc.build_arg:
         for arg in nmspc.build_arg:
             a = arg.split("=")
             buildargs[a[0]] = a[1]
-    logging.debug("Build args: %s", buildargs)
+    _logger.debug("Build args: %s", buildargs)
     dockerfile = "Dockerfile"
     if dev:
         debug_dockerfile = "Dockerfile.debug"
@@ -172,10 +165,10 @@
         dockerfile = debug_dockerfile
         # TODO remove temp image
 
-    docker_client = docker_api(base_url="unix://var/run/docker.sock")
+    docker_client = DockerClient.client
     pull = not nmspc.no_pull
-    logging.debug("Docker Pull %s", pull)
-    builder = docker_client.build(
+    _logger.debug("Docker Pull %s", pull)
+    builder = docker_client.api.build(
         path=".",
         rm=True,
         pull=pull,
@@ -183,18 +176,16 @@
         tag=image,
         dockerfile=dockerfile,
     )
-    # this is for python docker 1.8-1.9
-    # TODO add compatibility with newer python docker
     for line in builder:
         d = json.loads(line.decode("utf-8"))
         if "stream" in d:
-            logging.info(d["stream"])
+            for line in d["stream"].rstrip().split("\n"):
+                _logger.info(line)
         if "errorDetail" in d:
-            logging.fatal(d["errorDetail"])
+            _logger.fatal(d["errorDetail"])
             return 1
     if dev:
         call(["rm", dockerfile])
-    # TODO exit if build failed
 
     # TODO docker tag with tags/bookmarks (unused so maybe no need)
     # TODO docker push (only when asked for)
diff --git a/odoo_scripts/docker_build_clean.py b/odoo_scripts/docker_build_clean.py
--- a/odoo_scripts/docker_build_clean.py
+++ b/odoo_scripts/docker_build_clean.py
@@ -8,7 +8,7 @@
 import sys
 
 from .list_modules import MODULES_LIST_FILE
-from .parsing import basic_parser, logging_from_verbose
+from .parsing import apply, basic_parser
 
 _logger = logging.getLogger(__name__)
 
@@ -49,7 +49,7 @@
     """
     parser = __parser()
     nmspc = parser.parse_args(argv)
-    logging_from_verbose(nmspc)
+    apply(nmspc)
     # TODO check odoo_scripts before deleting anything
     clean()
     return 0
diff --git a/odoo_scripts/docker_build_copy.py b/odoo_scripts/docker_build_copy.py
--- a/odoo_scripts/docker_build_copy.py
+++ b/odoo_scripts/docker_build_copy.py
@@ -9,13 +9,13 @@
 
 from .config import Config
 from .list_modules import list_modules
-from .parsing import basic_parser, logging_from_verbose
+from .parsing import apply, basic_parser
 
 _logger = logging.getLogger(__name__)
 
-__version__ = "1.0.0"
+__version__ = "1.0.1"
 __date__ = "2020-06-30"
-__updated__ = "2020-06-30"
+__updated__ = "2020-09-11"
 
 
 def __parser():
@@ -50,7 +50,7 @@
     """
     parser = __parser()
     nmspc = parser.parse_args(argv)
-    logging_from_verbose(nmspc)
+    apply(nmspc)
     copy()
     return 0
 
diff --git a/odoo_scripts/docker_client.py b/odoo_scripts/docker_client.py
new file mode 100644
--- /dev/null
+++ b/odoo_scripts/docker_client.py
@@ -0,0 +1,54 @@
+import logging
+import os
+import tarfile
+from typing import Dict
+
+import docker
+
+_logger = logging.getLogger(__name__)
+if docker.__version__ < "3.4.0":
+    _logger.warning(
+        "Unexepected python docker version: %s", docker.__version__
+    )
+
+# TODO detect that user is member of docker group
+
+
+class DockerClient:
+    client = docker.from_env()
+
+    @classmethod
+    def create_volume(
+        cls, volume_name: str, extra_labels: Dict[str, str] = None
+    ) -> docker.models.volumes.Volume:
+        """Return the volume passed in parameter, creating it if it does not
+        exists.
+        :param volume_name: name of the volume to create
+        :param extra_labels: extra labels to put on the volume (only on
+        creation)
+        """
+        volumes = cls.client.volumes.list(filters={"name": volume_name})
+        if volumes:
+            _logger.debug("Volume %s already exists", volume_name)
+            return volumes[0]
+        else:
+            _logger.debug("Creating volume %s", volume_name)
+            labels = dict(odoo_scripts="")
+            if extra_labels:
+                labels.update(extra_labels)
+            return cls.client.volumes.create(name=volume_name, labels=labels)
+
+    @staticmethod
+    def put_file(src, container, dst):
+        # copied from https://stackoverflow.com/questions/46390309/how-to-copy-a-file-from-host-to-container-using-docker-py-docker-sdk # noqa: E501
+        os.chdir(os.path.dirname(src))
+        srcname = os.path.basename(src)
+        # TODO put that in a temporary directory
+        tar = tarfile.open(src + ".tar", mode="w")
+        try:
+            tar.add(srcname)
+        finally:
+            tar.close()
+
+        data = open(src + ".tar", "rb").read()
+        container.put_archive(os.path.dirname(dst), data)
diff --git a/odoo_scripts/docker_dev_start.py b/odoo_scripts/docker_dev_start.py
--- a/odoo_scripts/docker_dev_start.py
+++ b/odoo_scripts/docker_dev_start.py
@@ -2,107 +2,36 @@
 # vim: set shiftwidth=4 softtabstop=4:
 """Launch a docker image but bind the local directory inside the container
 and define the module path automatically.
-
-isort:skip_file
 """
 # Version 4.2.1
-import argparse
-import atexit
-from configparser import ConfigParser
 import logging
 import os
 import pwd
+import sys
+from configparser import ConfigParser
 from subprocess import call
-import sys
-import tempfile
-import time
-from typing import Callable, Dict, Optional, Tuple
+from typing import List
+
+import dockerpty
+from docker.types import Mount
+from psycopg2 import OperationalError, connect
 
-import docker  # apt python3-docker (1.9) or pip install docker
-from psycopg2 import connect, OperationalError  # apt python3-psycopg2
-
+from . import docker_build
 from .config import Config
-from . import docker_build
-
-if docker.__version__ > "2.5.0":
-    from docker import APIClient as docker_api
-else:
-    from docker import Client as docker_api
+from .docker_client import DockerClient
+from .docker_flake8 import flake8
+from .docker_isort import isort
+from .docker_postgresql import POSTGRES_PASSWORD, docker_run_postgresql
+from .docker_py3o import start_py3o
+from .parsing import apply, basic_parser
 
 # TODO auto create list of module
 
 _logger = logging.getLogger(__name__)
 
-__version__ = "2.0.4"
+__version__ = "3.0.0"
 __date__ = "2017-08-11"
-__updated__ = "2020-09-02"
-
-
-def which(program):
-    """Return path of program if it exists
-from:
-https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python/377028#377028
-    """
-
-    def is_exe(fpath):
-        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
-
-    fpath, fname = os.path.split(program)
-    if fpath:
-        if is_exe(program):
-            return program
-    else:
-        for path in os.environ["PATH"].split(os.pathsep):
-            exe_file = os.path.join(path, program)
-            if is_exe(exe_file):
-                return exe_file
-
-    return None
-
-
-def flake8(odoo_type):
-    """Run flake8
-    """
-    _logger.info("Running flake8")
-    if odoo_type in ("odoo7", "odoo8", "odoo9", "odoo10"):
-        image = "xcgd/flake8:2"
-    else:
-        image = "xcgd/flake8:3"
-    call(
-        [
-            "docker",
-            "run",
-            "--rm",
-            "--volume",
-            "{}:/mnt:ro".format(os.environ["PWD"]),
-            "--workdir",
-            "/mnt",
-            image,
-            ".",
-        ]
-    )
-
-
-def isort(docker_client):
-    """Run isort
-    """
-    _logger.info("Pulling isort")
-    docker_client.pull(repository="xcgd/isort", tag="odoo")
-    _logger.info("Running isort")
-    call(
-        [
-            "docker",
-            "run",
-            "--rm",
-            "--volume",
-            "{}:/mnt".format(os.environ["PWD"]),
-            "-w",
-            "/mnt",
-            "xcgd/isort:odoo",
-            "isort",
-            "-c",
-        ]
-    )
+__updated__ = "2020-09-15"
 
 
 def main(argv=None):  # IGNORE:C0111
@@ -142,9 +71,8 @@
 
     # TODO add a way to store configuration options in a project file
     # Argument parsing
-    parser = argparse.ArgumentParser(
-        description=program_license,
-        formatter_class=argparse.RawDescriptionHelpFormatter,
+    parser = basic_parser(
+        description=program_license, version=program_version_message
     )
     parser.add_argument(
         "--name",
@@ -155,16 +83,6 @@
         default=project_name,
     )
     parser.add_argument(
-        "-V", "--version", action="version", version=program_version_message
-    )
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        dest="verbose",
-        action="count",
-        help="set verbosity level [default: %(default)s]",
-    )
-    parser.add_argument(
         "--db_user", help="Database user [default: %(default)s]", default=None
     )
     parser.add_argument(
@@ -371,13 +289,7 @@
 
     nmspc = parser.parse_args(argv)
     project_name = nmspc.name
-    verbose = nmspc.verbose
-    if not verbose:
-        logging.basicConfig(level=logging.WARN)
-    elif verbose == 1:
-        logging.basicConfig(level=logging.INFO)
-    else:
-        logging.basicConfig(level=logging.DEBUG)
+    apply(nmspc)
     db_user = nmspc.db_user
     db_password = nmspc.db_password
     use_host_network = nmspc.host_network
@@ -427,8 +339,8 @@
 
     _logger.debug("Docker image: %s", image)
     # detect if docker image already exists
-    docker_client = docker_api(base_url="unix://var/run/docker.sock")
-    image_list = docker_client.images(name=image, quiet=True)
+    client = DockerClient.client
+    image_list = client.images.list(name=image)
     if not image_list:
         _logger.info("Image %s does not exist", image)
     else:
@@ -436,20 +348,22 @@
     if (not image_list and not nmspc.no_build) or nmspc.force_build:
         _logger.info("Building image %s", image)
         arguments = ["--dev"]
-        if verbose == 1:
-            arguments.append("-v")
-        elif verbose and verbose > 1:
-            arguments.append("-vv")
         failed = docker_build.main(arguments)
         if failed:
             return failed
 
     # options is only used with subprocess call
-    options = ["--name", project_name, "--rm", "--tty", "--interactive"]
+    options = {
+        "name": project_name,
+        "hostname": project_name,
+        "tty": True,
+        "extra_hosts": dict(),
+        "ports": dict(),
+        "environment": dict(),
+    }
     if not use_host_network:
-        options.append("--publish")
-        options.append("8069:8069")
-    binds = []
+        options["ports"][8069] = 8069
+    mounts: List[Mount] = list()
     # TODO handle other version of odoo (base image need to be changed too)
     coverage = (nmspc.test or nmspc.test_default) and odoo_type in (
         "odoo11",
@@ -512,8 +426,7 @@
     # auto detect local ip
     if use_host_network:
         local_ip = "127.0.0.1"
-        options.append("--network")
-        options.append("host")
+        options["network_mode"] = "host"
     else:
         local_ip = None
         try:
@@ -567,17 +480,19 @@
             if odoo_type in ("odoo11", "odoo12", "odoo13")
             else "/opt/odoo/etc/odoo.conf"
         )
-        binds.append(
-            "{}:{}".format(
-                os.path.join(project_path, local_conf_path), odoo_conf_file
+        mounts.append(
+            Mount(
+                odoo_conf_file,
+                os.path.join(project_path, local_conf_path),
+                "bind",
             )
         )
         cp_local = ConfigParser()
         cp_local.read(local_conf_path)
         if not user:
-            user = cp_local.get("options", "db_user")
+            user = cp_local.get("options", "db_user", fallback=None)
         if not password:
-            password = cp_local.get("options", "db_password")
+            password = cp_local.get("options", "db_password", fallback=None)
         # doing this avoid having to change the value of redis_host
         # used by smile_redis_session_store
         redis_host = (
@@ -586,8 +501,7 @@
             and cp_local.get("options", "redis_host")
         ) or None
         if redis_host:
-            options.append("--add-host")
-            options.append("{}:{}".format(redis_host, local_ip))
+            options["extra_hosts"][redis_host] = local_ip
     else:
         _logger.info("No configuration file at: %s", local_conf_path)
     # default values if nothing else
@@ -599,44 +513,43 @@
 
     # data volume handling
     base_data_volume_name = "{}_data".format(project_name)
-    data_volumes = dict()
     if odoo_type == "odoo7":
         # this path is in odoo code, the last part is the system user
-        data_volumes["_sessions"] = "/tmp/oe-sessions-{}".format("odoo")
+        session_volume_name = "{}{}".format(base_data_volume_name, "_sessions")
+        target = "/tmp/oe-sessions-{}".format("odoo")
+        client.create_volume(session_volume_name, {"mounted in": target})
+        mounts.append(Mount(target, session_volume_name))
         # by default odoo store attachments in database but it can be set on
         # disk. mount the volume in all cases.
         # XXX data is the path set in the database (in the form file:///data)
         #  it should be read from there, in that case target directory needs
         #  to include the database name.
         #  our databases only use file:///data so this should be fine
-        data_volumes["_filestore"] = (
+        target = (
             "/usr/local/lib/python2.7/dist-packages/"
-            "openerp-7.0-py2.7.egg/openerp/{}"
-        ).format("data")
+            "openerp-7.0-py2.7.egg/openerp/data"
+        )
+        filestore_volume_name = "{}{}".format(
+            base_data_volume_name, "_filestore"
+        )
+        client.create_volume(filestore_volume_name, {"mounted in": target})
+        mounts.append(target, filestore_volume_name)
     else:
         data_volume_path = "/mnt/data"
-        data_volumes[""] = data_volume_path
-        arg.append("--data-dir {}".format(data_volume_path))
+        mounts.append(Mount(data_volume_path, base_data_volume_name))
+        arg.append("--data-dir")
+        arg.append(data_volume_path)
 
-    mount_opts = list()
-    for key, val in data_volumes.items():
-        data_volume_name = "{}{}".format(base_data_volume_name, key)
-        _logger.debug("Using data volume %s", data_volume_name)
-        createVolume(docker_client, data_volume_name, {"mounted in": val})
-        binds.append("{}:{}".format(data_volume_name, val))
-        mount_opts.append("--mount")
-        mount_opts.append(
-            "source={},target=/mnt/{}".format(data_volume_name, key)
-        )
     # make sure the permission in the volumes are correct
     # TODO replace by something cleaner if possible
-    chown_cmd = (
-        ["docker", "run", "--rm"]
-        + mount_opts
-        + ["--entrypoint", "/bin/chown", image, "odoo", "--recursive", "/mnt"]
+    _logger.info("chown")
+    client.containers.run(
+        image,
+        command=["odoo", "--recursive", "/mnt"],
+        remove=True,
+        entrypoint="/bin/chown",
+        mounts=mounts,
     )
-    _logger.debug("Calling %s", " ".join(chown_cmd))
-    call(chown_cmd)
 
     # avoid the duplication of unbind volumes with all addons
     if odoo_type == "odoo7":
@@ -664,35 +577,33 @@
         volume_name = "{}_{}".format(
             project_name, extra_volume.replace("/", "_")
         )
-        createVolume(
-            docker_client,
+        DockerClient.create_volume(
             volume_name,
             {
                 "mounted in": extra_volume,
                 "comment": "only used to avoid creating too many volumes",
             },
         )
-        binds.append("{}:{}".format(volume_name, extra_volume))
+        mounts.append(Mount(extra_volume, volume_name))
 
     if start_py3o_stack:
-        start_py3o(docker_client)
-        options.append("--add-host")
-        options.append("py3o:{}".format(local_ip))
-        options.append("--add-host")
-        options.append("py3o-fusion-server:{}".format(local_ip))
+        start_py3o()
+        # provide a couple of handy dns name for py3o
+        options["extra_hosts"]["py3o"] = local_ip
+        options["extra_hosts"]["py3o-fusion-server"] = local_ip
 
     if start_postgresql and not odoo_help:
         # Use socket rather than export port (avoid having to find a free port
         # number)
-        name, stop_postgresql, socket_path = docker_run_postgresql(
-            docker_client, project_name, postgresql_version, host_pg_port=None
+        pg, stop_postgresql, socket_path = docker_run_postgresql(
+            project_name, postgresql_version, host_pg_port=None
         )
-        binds.append("{}:/var/run/postgresql".format(socket_path))
+        mounts.append(Mount("/var/run/postgresql", socket_path, "bind"))
 
     # Check that connection can be done, try to create user if asked to
     # TODO: handle the case where the database is still starting up
     # TODO: (and remove the sleep)
-    _logger.info("Testing postgres connection")
+    _logger.info("Testing database connection on base postgres")
     try:
         if start_postgresql:
             connect(
@@ -725,6 +636,7 @@
                     database="postgres",
                     host=socket_path,
                     port=5432,
+                    password=POSTGRES_PASSWORD,
                 )
             else:
                 loginname = pwd.getpwuid(os.getuid())[0]
@@ -751,37 +663,26 @@
     if start_postgresql and not odoo_help and restore_filename:
         restore_basename = os.path.basename(restore_filename)
         _logger.info("Copying dump file in docker")
-        call(
+        inside_restore_filename = "/tmp/{}".format(restore_basename)
+        DockerClient.put_file(restore_filename, pg, inside_restore_filename)
+        _logger.info("Creating database %s for %s", database, user)
+        pg.exec_run(["createdb", "-U", user, database])
+        _logger.info("Restoring database %s", database)
+        restore, output = pg.exec_run(
             [
-                "docker",
-                "cp",
-                restore_filename,
-                "{}:/tmp/{}".format(name, restore_basename),
-            ]
-        )
-        _logger.info("Creating database")
-        call(["docker", "exec", name, "createdb", "-U", user, database])
-        _logger.info("Restoring database %s", database)
-        restore = call(
-            [
-                "docker",
-                "exec",
-                name,
                 "pg_restore",
                 "-U",
                 user,
-                "-O",
+                "--no-owner",
                 "-d",
                 database,
-                "/tmp/{}".format(restore_basename),
+                inside_restore_filename,
             ]
         )
         if not restore:
             return 15
         _logger.info("Removing dump file in docker")
-        call(
-            ["docker", "exec", name, "rm", "/tmp/{}".format(restore_basename)]
-        )
+        pg.exec_run(["rm", inside_restore_filename])
 
     if not start_postgresql and not odoo_help and restore_filename:
         _logger.info("Creating database %s", database)
@@ -802,46 +703,32 @@
             _logger.info(
                 "Copying SQL script %s in docker", pre_sql_script_basename
             )
-            call(
-                [
-                    "docker",
-                    "cp",
-                    pre_sql_script,
-                    "{}:/tmp/{}".format(name, pre_sql_script_basename),
-                ]
-            )
+            inside_pre_sql_script = "/tmp/{}".format(pre_sql_script_basename)
+            client.put_file(pre_sql_script, pg, inside_pre_sql_script)
             _logger.info(
                 "Running SQL script %s in %s",
                 pre_sql_script_basename,
                 database,
             )
-            script = call(
+            exit_code, output = pg.exec_run(
                 [
-                    "docker",
-                    "exec",
-                    name,
                     "psql",
                     "-U",
                     user,
                     "-d",
                     database,
                     "-f",
-                    "/tmp/{}".format(pre_sql_script_basename),
+                    inside_pre_sql_script,
                 ]
             )
-            if script > 0:
-                _logger.fatal("Error when running script (%s)", script)
+            if exit_code > 0:
+                _logger.fatal(
+                    "Error when running SQL script %s", pre_sql_script
+                )
                 return 16
-            _logger.info("Removing SQL script %s in docker")
-            call(
-                [
-                    "docker",
-                    "exec",
-                    name,
-                    "rm",
-                    "/tmp/{}".format(pre_sql_script_basename),
-                ]
-            )
+            _logger.info("Removing SQL script %s in docker", pre_sql_script)
+            # remove script
+            pg.exec_run(["rm", inside_pre_sql_script])
         else:
             _logger.info(
                 "Running SQL script %s in %s", pre_sql_script, database
@@ -850,18 +737,17 @@
                 ["psql", "-U", user, "-d", database, "-f", pre_sql_script]
             )
             if script > 0:
-                _logger.fatal("Error when running script (%s)", script)
+                _logger.fatal("Error when running SQL script %s", script)
                 return 16
 
     # volume magic
     target_module_directory = "/mnt/addons"
     for module in modules:
-        binds.append(
-            "%s:%s/%s"
-            % (
+        mounts.append(
+            Mount(
+                "%s/%s" % (target_module_directory, os.path.basename(module)),
                 os.path.realpath(os.path.join(project_path, module)),
-                target_module_directory,
-                os.path.basename(module),
+                "bind",
             )
         )
     all_addons_dir = [target_module_directory]
@@ -875,7 +761,7 @@
         flake8(odoo_type)
 
     if run_isort:
-        isort(docker_client)
+        isort()
 
     if dev:
         if odoo_type in ("odoo10", "odoo11", "odoo13"):
@@ -902,29 +788,39 @@
             else:
                 pythons = ("python3.6",)
             for python in pythons:
-                binds.append(
-                    "%s/odoo:/usr/local/lib/%s/dist-packages/odoo"
-                    % (odoo_sources, python)
+                mounts.append(
+                    Mount(
+                        "/usr/local/lib/%s/dist-packages/odoo" % python,
+                        os.path.join(odoo_sources, "odoo"),
+                        "bind",
+                    )
                 )
-                binds.append(
-                    "%s/addons:/usr/local/lib/%s/dist-packages/odoo/addons"
-                    % (odoo_sources, python)
+                mounts.append(
+                    Mount(
+                        "/usr/local/lib/%s/dist-packages/odoo/addons" % python,
+                        os.path.join(odoo_sources, "addons"),
+                        "bind",
+                    )
                 )
-                binds.append(
-                    "%s/odoo/addons/base:/usr/local/lib/%s/dist-packages/"
-                    "odoo/addons/base" % (odoo_sources, python)
+                mounts.append(
+                    Mount(
+                        "/usr/local/lib/%s/dist-packages/odoo/addons/base"
+                        % python,
+                        os.path.join(odoo_sources, "odoo", "addons", "base"),
+                        "bind",
+                    )
                 )
         elif odoo_type in ("odoo7", "odoo8"):
-            binds.append("%s/odoo:/opt/odoo/sources/odoo" % odoo_sources)
+            mounts.append(
+                Mount("/opt/odoo/sources/odoo", odoo_sources, "bind")
+            )
         else:
             raise Exception(
                 "Unexpected odoo_type when binding sources: %s" % odoo_type
             )
 
-    # use call to allow usage of pdb
-    for bind in binds:
-        options.append("--volume")
-        options.append(bind)
+    # avoid chowning at each run, faster but can cause trouble
+    options["environment"]["ODOO_CHOWN"] = "false"
 
     if coverage:
         if nmspc.test_default:
@@ -937,174 +833,29 @@
                     for module in nmspc.test.split(",")
                 ]
             )
-        options.extend(
-            ("--env", "COVERAGE_RUN_OPTIONS=--source=" + source_files)
+        options["environment"]["COVERAGE_RUN_OPTIONS"] = (
+            "--source=" + source_files
         )
 
-    cmd = ["docker", "run"]
-    cmd.extend(options)
-    cmd.append(image)
-    cmd.extend(arg)
-    _logger.debug(" ".join(cmd))
-    result = call(cmd)
-    return result
-
-
-def createVolume(
-    docker_client, volume_name: str, extra_labels: Dict[str, str] = None
-):
-    """Return the volume passed in parameter, creating it if it does not exists
-    :param docker_client: docker api object
-    :param volume_name: name of the volume to create
-    :param extra_labels: extra labels to put on the volume (only on creation)
-    """
-    volumes = docker_client.volumes(filters={"name": volume_name})
-    if volumes["Volumes"]:
-        _logger.debug("Volume %s already exist", volume_name)
-        return volumes["Volumes"][0]
-    else:
-        _logger.debug("Creating volume %s", volume_name)
-        labels = dict(odoo_scripts="", docker_dev_start=__version__)
-        if extra_labels:
-            labels.update(extra_labels)
-        return docker_client.create_volume(name=volume_name, labels=labels)
-
-
-def isRunning(docker_client, container_id):
-    """Return true if the container is still running
-    """
-    return len(
-        docker_client.containers(filters={"id": container_id}, quiet=True)
-    )
-
-
-def docker_run_postgresql(
-    docker_client,
-    project_name: str,
-    postgresql_version: str,
-    host_pg_port=None,
-    stop_at_exit: bool = True,
-) -> Tuple[Optional[str], Callable, Optional[str]]:
-    """
-
-    :param docker_client:
-    :param project_name:
-    :param postgresql_version:
-    :param host_pg_port: if None, put the socket in a temp directory,
-     otherwise publish the port
-    :param stop_at_exit: True to stop the container at exit
-    :return: name of the container, method to call to stop it, socket directory
-     if any
-    """
-    pg_repository = "postgres"
-    version = "{}-alpine".format(postgresql_version)
-    pg_image = "{}:{}".format(pg_repository, version)
-    name = "postgresql-{}-{}".format(postgresql_version, project_name)
-    # docker_client.pull(repository=pg_repository, tag=version)
-    pg_data_volume_name = "postgresql_{}-{}".format(
-        postgresql_version, project_name
-    )
-    path: str = None
-
-    # volume = createVolume(docker_client, pg_data_volume_name)
-    binds = ["{}:/var/lib/postgresql/data".format(pg_data_volume_name)]
-    port_bindings = {}
-    if host_pg_port:
-        port_bindings[5432] = host_pg_port
-    else:
-        path = tempfile.TemporaryDirectory(
-            prefix="odoo_scripts_postgres_socket-"
-        ).name
-        binds.append("{}:/var/run/postgresql".format(path))
-    host_config = docker_client.create_host_config(
-        binds=binds, port_bindings=port_bindings
+    interactive = True
+    if interactive:
+        options["stdin_open"] = True
+    odoo_container = client.containers.create(
+        image, command=arg, mounts=mounts, **options
     )
-    if any(
-        "/{}".format(name) in container["Names"]
-        for container in docker_client.containers()
-    ):
-        _logger.debug("Postgresql Container already running")
-        return name, False, None
-    _logger.debug("Creating postgresql container")
-    pg = docker_client.create_container(
-        image=pg_image, host_config=host_config, name=name
-    )
-
-    _logger.debug("Starting postgresql container")
-    docker_client.start(pg.get("Id"))
-
-    def stop_postgresql():
-        # TODO test if still exists
-        try:
-            _logger.info("Stopping postgresql")
-            docker_client.stop(pg.get("Id"))
-            _logger.info("Removing container postgresql")
-            docker_client.remove_container(pg.get("Id"))
-        except docker.errors.NotFound:
-            _logger.info("Postgresql already stopped")
-        # crash because docker or pg changes the owner of the directory
-        # temp_directory.cleanup()
-
-    if stop_at_exit:
-        atexit.register(stop_postgresql)
-    _logger.debug("Waiting for postgres to start")
-    # give pg the time to start up
-    time.sleep(5)
-    return name, stop_postgresql, path
-
-
-def find_container(docker_client, name):
-    """Return container object from its name
-    """
-    for container in docker_client.containers():
-        if "/{}".format(name) in container["Names"]:
-            return container
-
-
-def remove_and_stop(docker_client, name):
-    # TODO handle the case when the container has been created, not running
-    tostop = find_container(docker_client, name)
-    docker_client.stop(tostop.get("Id"))
-    docker_client.remove_container(tostop.get("Id"))
-
-
-def start_py3o(docker_client, host_fusion_port=8765, stop_at_exit=True):
-    fusion_repository = "xcgd/py3o"
-    fusion_version = "1.0.0"
-    fusion_name = "py3o_fusion"
-    fusion_image = "{}:{}".format(fusion_repository, fusion_version)
-    try:
-        docker_client.pull(repository=fusion_repository, tag=fusion_version)
-    except Exception as e:
-        _logger.warning("Exception when trying to pull: %s", e)
-    if any(
-        "/{}".format(fusion_name) in container["Names"]
-        for container in docker_client.containers()
-    ):
-        _logger.debug("%s Container already running", fusion_name)
-        remove_and_stop(docker_client, fusion_name)
-    # TODO handle --host-network option
-    port_bindings = {8765: host_fusion_port}
-    host_config = docker_client.create_host_config(
-        binds=[], port_bindings=port_bindings
-    )
-    _logger.debug("Starting %s container", fusion_name)
-    fusion = docker_client.create_container(
-        image=fusion_image, host_config=host_config, name=fusion_name
-    )
-    _logger.debug("Starting %s container", fusion_name)
-    docker_client.start(container=fusion.get("Id"))
-
-    def stop_py3o():
-        # TODO test if still exists
-        _logger.info("Stopping fusion")
-        docker_client.stop(fusion.get("Id"))
-        _logger.info("Removing containers")
-        docker_client.remove_container(fusion.get("Id"))
-
-    if stop_at_exit:
-        atexit.register(stop_py3o)
-    return fusion_name, stop_py3o
+    _logger.info("Starting Odoo")
+    if interactive:
+        dockerpty.start(client.api, odoo_container.id)
+        # already stopped but this is the easiest way to get the return code
+        result_dict = odoo_container.wait()
+        result = result_dict["StatusCode"]
+    else:
+        # XXX untested
+        #  that might need a way to pass any signal to the container
+        result = odoo_container.start()
+    # only remove after getting the return code
+    odoo_container.remove()
+    return result
 
 
 if __name__ == "__main__":
diff --git a/odoo_scripts/docker_flake8.py b/odoo_scripts/docker_flake8.py
new file mode 100644
--- /dev/null
+++ b/odoo_scripts/docker_flake8.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+# vim: set shiftwidth=4 softtabstop=4:
+"""Run flake8 in a docker
+"""
+import atexit
+import logging
+import os
+import sys
+
+from .config import Config
+from .docker_client import DockerClient
+from .parsing import apply, basic_parser
+
+__version__ = "1.0.0"
+__date__ = "2020-09-10"
+__updated__ = "2020-09-10"
+
+_logger = logging.getLogger(__name__)
+
+
+def __parser():
+    program_version = __version__
+    program_build_date = str(__updated__)
+    program_version_message = "%%(prog)s %s (%s)" % (
+        program_version,
+        program_build_date,
+    )
+    program_shortdesc = __doc__.split(".")[0]
+    program_license = """%s
+
+      Created by Vincent Hatakeyama on %s.
+      Copyright 2020 XCG Consulting. All rights reserved.
+
+      Licensed under the MIT License
+
+      Distributed on an "AS IS" basis without warranties
+      or conditions of any kind, either express or implied.
+
+    USAGE
+    """ % (
+        program_shortdesc,
+        str(__date__),
+    )
+    parser = basic_parser(program_license, program_version_message)
+    return parser
+
+
+def main(argv=None):
+    """Copy modules for a build, callable version that parses arguments
+    """
+    parser = __parser()
+    nmspc = parser.parse_args(argv)
+    apply(nmspc)
+    c = Config()
+    odoo_type = c.odoo_type
+    return flake8(odoo_type)
+
+
+def flake8(odoo_type):
+    """Run flake8
+    """
+    client = DockerClient.client
+    repository = "xcgd/flake8"
+    if odoo_type in ("odoo7", "odoo8", "odoo9", "odoo10"):
+        tag = "2"
+        _logger.info("Running flake8 for Python 2")
+    else:
+        tag = "3"
+        _logger.info("Running flake8 for Python 3")
+    client.images.pull(repository, tag)
+    image = "{}:{}".format(repository, tag)
+    container = None
+
+    # XXX When pressing ctrl-c during the container creating, the container
+    #  is not removed as there is no return of the value of the Container
+    #  object. There is several ways to do that:
+    #  - give the container a unique name we know, and use that to clean
+    #  - maybe use some kind of time frame to identify the container
+    #  - warn the user of a potential container leak by using signals
+
+    def stop_remove():
+        """Stop then remove the container."""
+        if container:
+            if container.status == "running":
+                container.stop()
+                container.wait()
+            container.remove()
+
+    atexit.register(stop_remove)
+
+    # split create and start so in case of SIGTERM, container has fewer chances
+    # of being None
+    container = client.containers.create(
+        image,
+        command=["."],
+        volumes={os.environ["PWD"]: {"bind": "/mnt", "mode": "ro"}},
+        working_dir="/mnt",
+        detach=True,
+    )
+    container.start()
+    result = container.wait()
+    atexit.unregister(stop_remove)
+    atexit.register(container.remove)
+    print(container.logs().decode().rstrip())
+    if result.get("Error", None):
+        _logger.warning(result.decode())
+    container.remove(v=True)
+    atexit.unregister(container.remove)
+    return result["StatusCode"]
+
+
+if __name__ == "__main__":
+    return_code = main(sys.argv[1:])
+    if return_code:
+        exit(return_code)
diff --git a/odoo_scripts/docker_isort.py b/odoo_scripts/docker_isort.py
new file mode 100644
--- /dev/null
+++ b/odoo_scripts/docker_isort.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# vim: set shiftwidth=4 softtabstop=4:
+"""Run isort in a docker
+"""
+import atexit
+import logging
+import os
+import sys
+
+import docker
+
+from .docker_client import DockerClient
+from .parsing import apply, basic_parser
+
+__version__ = "1.0.0"
+__date__ = "2020-09-10"
+__updated__ = "2020-09-10"
+
+_logger = logging.getLogger(__name__)
+
+
+def __parser():
+    program_version = __version__
+    program_build_date = str(__updated__)
+    program_version_message = "%%(prog)s %s (%s)" % (
+        program_version,
+        program_build_date,
+    )
+    program_shortdesc = __doc__.split(".")[0]
+    program_license = """%s
+
+      Created by Vincent Hatakeyama on %s.
+      Copyright 2020 XCG Consulting. All rights reserved.
+
+      Licensed under the MIT License
+
+      Distributed on an "AS IS" basis without warranties
+      or conditions of any kind, either express or implied.
+
+    USAGE
+    """ % (
+        program_shortdesc,
+        str(__date__),
+    )
+    parser = basic_parser(program_license, program_version_message)
+    return parser
+
+
+def main(argv=None):
+    """Copy modules for a build, callable version that parses arguments
+    """
+    parser = __parser()
+    nmspc = parser.parse_args(argv)
+    apply(nmspc)
+    isort()
+    return 0
+
+
+def isort(pull=True):
+    """Run isort
+    """
+    client = DockerClient.client
+    if pull:
+        _logger.info("Pulling isort")
+        # TODO find a better way to avoid error when pulling without a
+        #  connection
+        try:
+            client.images.pull(repository="xcgd/isort", tag="odoo")
+        except docker.errors.APIError:
+            _logger.warning(
+                "No connection to server, using existing version, if any"
+            )
+    _logger.info("Running isort for Odoo")
+    container = None
+
+    # XXX When pressing ctrl-c during the container creating, the container
+    #  is not removed as there is no return of the value of the Container
+    #  object. There is several ways to do that:
+    #  - give the container a unique name we know, and use that to clean
+    #  - maybe use some kind of time frame to identify the container
+    #  - warn the user of a potential container leak by using signals
+
+    def stop_remove():
+        """Stop then remove the container."""
+        if container:
+            if container.status == "running":
+                container.stop()
+                container.wait()
+            container.remove()
+
+    atexit.register(stop_remove)
+
+    # split create and start so in case of SIGTERM, container has fewer chances
+    # of being None
+    container = client.containers.create(
+        "xcgd/isort:odoo",
+        command=["isort", "--check"],
+        volumes={os.environ["PWD"]: {"bind": "/mnt", "mode": "ro"}},
+        working_dir="/mnt",
+        detach=True,
+    )
+    container.start()
+    result = container.wait()
+    atexit.unregister(stop_remove)
+    atexit.register(container.remove)
+    print(container.logs().decode().rstrip())
+    if result.get("Error", None):
+        _logger.warning(result.decode())
+    container.remove(v=True)
+    atexit.unregister(container.remove)
+    return result["StatusCode"]
+
+
+if __name__ == "__main__":
+    return_code = main(sys.argv[1:])
+    if return_code:
+        exit(return_code)
diff --git a/odoo_scripts/docker_postgresql.py b/odoo_scripts/docker_postgresql.py
new file mode 100644
--- /dev/null
+++ b/odoo_scripts/docker_postgresql.py
@@ -0,0 +1,104 @@
+# vim: set shiftwidth=4 softtabstop=4:
+import atexit
+import logging
+import tempfile
+import time
+from typing import Callable, Tuple
+
+import docker
+
+from .docker_client import DockerClient
+
+_logger = logging.getLogger(__name__)
+
+POSTGRES_PASSWORD = "this-could-be-anything"
+
+
+def docker_run_postgresql(
+    project_name: str,
+    postgresql_version: str,
+    host_pg_port=None,
+    stop_at_exit: bool = True,
+) -> Tuple[docker.models.containers.Container, Callable, str]:
+    """
+
+    :param project_name:
+    :param postgresql_version:
+    :param host_pg_port: if None, put the socket in a temp directory,
+     otherwise publish the port
+    :param stop_at_exit: True to stop the container at exit
+    :return: container, method to call to stop it, socket directory
+    """
+    docker_client = DockerClient.client
+    pg_repository = "postgres"
+    version = "{}-alpine".format(postgresql_version)
+    pg_image = "{}:{}".format(pg_repository, version)
+    name = "postgresql-{}-{}".format(postgresql_version, project_name)
+    # need to pull otherwise the containers.create might fail
+    docker_client.images.pull(repository=pg_repository, tag=version)
+    pg_data_volume_name = "postgresql_{}-{}".format(
+        postgresql_version, project_name
+    )
+    path: str = None
+    env = dict()
+    env["POSTGRES_PASSWORD"] = POSTGRES_PASSWORD
+
+    def stop_postgresql(pg_container: docker.models.containers.Container):
+        # TODO test if still exists
+        #  maybe by looking at pg.status
+        # TODO factorized with other methods that start dockers
+        try:
+            _logger.info("Stopping postgresql")
+            # Need to stop too?
+            pg_container.stop()
+            pg_container.wait()
+            _logger.info("Removing container postgresql")
+            pg_container.remove()
+        except docker.errors.NotFound:
+            _logger.info("Postgresql already stopped")
+        # crash because docker or pg changes the owner of the directory
+        # temp_directory.cleanup()
+
+    for container in docker_client.containers.list():
+        if name == container.name:
+            _logger.info("Postgresql Container already running")
+            source = None
+            for mount_dict in container.attrs["Mounts"]:
+                if mount_dict["Destination"] == "/var/run/postgresql":
+                    source = mount_dict["Source"]
+            return container, lambda: stop_postgresql(container), source
+    volumes = dict()
+
+    volumes[pg_data_volume_name] = {
+        "bind": "/var/lib/postgresql/data",
+        "mode": "rw",
+    }
+    port_bindings = dict()
+    if host_pg_port:
+        port_bindings[5432] = host_pg_port
+    else:
+        path = tempfile.TemporaryDirectory(
+            prefix="odoo_scripts_postgres_socket-"
+        ).name
+        volumes[path] = {"bind": "/var/run/postgresql", "mode": "rw"}
+    _logger.debug("Creating postgresql container")
+    pg = docker_client.containers.create(
+        pg_image,
+        volumes=volumes,
+        ports=port_bindings,
+        name=name,
+        environment=env,
+    )
+
+    _logger.debug("Starting postgresql container")
+    pg.start()
+
+    def stop_pg():
+        return stop_postgresql(pg)
+
+    if stop_at_exit:
+        atexit.register(stop_pg)
+    _logger.debug("Waiting for postgres to start")
+    # give pg the time to start up
+    time.sleep(5)
+    return pg, stop_pg, path
diff --git a/odoo_scripts/docker_py3o.py b/odoo_scripts/docker_py3o.py
new file mode 100644
--- /dev/null
+++ b/odoo_scripts/docker_py3o.py
@@ -0,0 +1,66 @@
+# vim: set shiftwidth=4 softtabstop=4:
+"""Py3o methods
+"""
+import atexit
+import logging
+
+from .docker_client import DockerClient
+
+_logger = logging.getLogger(__name__)
+
+
+# TODO move to docker_client directly
+def find_container(name):
+    """Return container object from its name
+    """
+    for container in DockerClient.client.containers():
+        if name == container.names:
+            return container
+
+
+def remove_and_stop(docker_client, name):
+    # TODO handle the case when the container has been created, not running
+    tostop = find_container(name)
+    docker_client.stop(tostop.get("Id"))
+    docker_client.remove_container(tostop.get("Id"))
+
+
+def start_py3o(host_fusion_port=8765, stop_at_exit=True):
+    """start a py3o container"""
+    docker_client = DockerClient.client
+    fusion_repository = "xcgd/py3o"
+    fusion_version = "1.0.0"
+    fusion_name = "py3o_fusion"
+    fusion_image = "{}:{}".format(fusion_repository, fusion_version)
+    try:
+        docker_client.pull(repository=fusion_repository, tag=fusion_version)
+    except Exception as e:
+        _logger.warning("Exception when trying to pull: %s", e)
+    if any(
+        "/{}".format(fusion_name) in container["Names"]
+        for container in docker_client.containers()
+    ):
+        _logger.debug("%s Container already running", fusion_name)
+        remove_and_stop(docker_client, fusion_name)
+    # TODO handle --host-network option
+    port_bindings = {8765: host_fusion_port}
+    host_config = docker_client.create_host_config(
+        binds=[], port_bindings=port_bindings
+    )
+    _logger.debug("Starting %s container", fusion_name)
+    fusion = docker_client.create_container(
+        image=fusion_image, host_config=host_config, name=fusion_name
+    )
+    _logger.debug("Starting %s container", fusion_name)
+    docker_client.start(container=fusion.get("Id"))
+
+    def stop_py3o():
+        # TODO test if still exists
+        _logger.info("Stopping fusion")
+        docker_client.stop(fusion.get("Id"))
+        _logger.info("Removing containers")
+        docker_client.remove_container(fusion.get("Id"))
+
+    if stop_at_exit:
+        atexit.register(stop_py3o)
+    return fusion_name, stop_py3o
diff --git a/odoo_scripts/import_base_import.py b/odoo_scripts/import_base_import.py
--- a/odoo_scripts/import_base_import.py
+++ b/odoo_scripts/import_base_import.py
@@ -16,7 +16,7 @@
     add_importing_file_parsing,
     extract_model_lang_from_parsed,
 )
-from .parsing import logging_from_verbose
+from .parsing import apply
 
 _logger = logging.getLogger(__name__)
 
@@ -159,7 +159,7 @@
     add_importing_file_parsing(parser)
 
     nmspc = parser.parse_args(argv)
-    logging_from_verbose(nmspc)
+    apply(nmspc)
 
     return import_with_base_import(
         login=nmspc.login,
diff --git a/odoo_scripts/import_jsonrpc.py b/odoo_scripts/import_jsonrpc.py
--- a/odoo_scripts/import_jsonrpc.py
+++ b/odoo_scripts/import_jsonrpc.py
@@ -17,13 +17,13 @@
     extract_model_lang_from_parsed,
 )
 from .odoo import odoo_connect_parser, odoo_login
-from .parsing import logging_from_verbose
+from .parsing import apply
 
 _logger = logging.getLogger(__name__)
 
-__version__ = "1.0.1"
+__version__ = "1.0.2"
 __date__ = "2020-02-17"
-__updated__ = "2020-02-26"
+__updated__ = "2020-09-11"
 
 
 def import_with_jsonrpc(
@@ -314,7 +314,7 @@
     )
 
     nmspc = parser.parse_args(argv)
-    logging_from_verbose(nmspc)
+    apply(nmspc)
 
     extra_context = dict()
     if nmspc.context:
diff --git a/odoo_scripts/import_sql.py b/odoo_scripts/import_sql.py
--- a/odoo_scripts/import_sql.py
+++ b/odoo_scripts/import_sql.py
@@ -212,6 +212,7 @@
         delimiter=nmspc.delimiter,
     )
     conn.close()
+    return 0
 
 
 if __name__ == "__main__":
diff --git a/odoo_scripts/list_modules.py b/odoo_scripts/list_modules.py
--- a/odoo_scripts/list_modules.py
+++ b/odoo_scripts/list_modules.py
@@ -9,15 +9,15 @@
 import sys
 
 from .config import Config
-from .parsing import basic_parser, logging_from_verbose
+from .parsing import apply, basic_parser
 
 MODULES_LIST_FILE = "odoo_modules_list"
 
 _logger = logging.getLogger(__name__)
 
-__version__ = "1.0.2"
+__version__ = "1.0.3"
 __date__ = "2020-02-26"
-__updated__ = "2020-06-18"
+__updated__ = "2020-09-11"
 
 
 def main(argv=None):  # IGNORE:C0111
@@ -53,7 +53,7 @@
     # Argument parsing
     parser = basic_parser(program_license, program_version_message)
     nmspc = parser.parse_args()
-    logging_from_verbose(nmspc)
+    apply(nmspc)
     list_modules()
 
 
diff --git a/odoo_scripts/parsing.py b/odoo_scripts/parsing.py
--- a/odoo_scripts/parsing.py
+++ b/odoo_scripts/parsing.py
@@ -8,22 +8,36 @@
 
 
 def add_verbosity_to_parser(parser: argparse.ArgumentParser):
-    parser.add_argument(
+    verbosity_group = parser.add_mutually_exclusive_group()
+    verbosity_group.add_argument(
         "-v",
         "--verbose",
         dest="verbose",
         action="count",
-        help="set verbosity level [default: %(default)s]",
+        help="Increase verbosity level [default: %(default)s]",
+        default=0,
+    )
+    verbosity_group.add_argument(
+        "-q",
+        "--quiet",
+        dest="quiet",
+        action="count",
+        help="Decreate verbosity level [default: %(default)s]",
+        default=0,
     )
 
 
 def logging_from_verbose(namespace: argparse.Namespace):
-    verbose = namespace.verbose
-    if not verbose:
-        level = logging.WARN
-    elif verbose == 1:
+    verbosity = namespace.verbose - namespace.quiet
+    if verbosity == 0:
         level = logging.INFO
-    else:
+    elif verbosity == -1:
+        level = logging.WARNING
+    elif verbosity == -2:
+        level = logging.ERROR
+    elif verbosity == -3:
+        level = logging.CRITICAL
+    elif verbosity > 0:
         level = logging.DEBUG
     coloredlogs.install(
         level, fmt="%(asctime)s %(levelname)8s %(name)s %(message)s"
diff --git a/odoo_scripts/update_duplicate_sources.py b/odoo_scripts/update_duplicate_sources.py
--- a/odoo_scripts/update_duplicate_sources.py
+++ b/odoo_scripts/update_duplicate_sources.py
@@ -11,7 +11,7 @@
 import hglib
 
 from .config import Config
-from .parsing import basic_parser, logging_from_verbose
+from .parsing import apply, basic_parser
 
 _logger = logging.getLogger(__name__)
 
@@ -52,7 +52,7 @@
     """
     parser = __parser()
     nmspc = parser.parse_args(argv)
-    logging_from_verbose(nmspc)
+    apply(nmspc)
     # XXX probably needs an option to indicate path to use, defaulting to
     #  default
     _update_duplicate_sources()
diff --git a/odoo_scripts/which.py b/odoo_scripts/which.py
new file mode 100644
--- /dev/null
+++ b/odoo_scripts/which.py
@@ -0,0 +1,27 @@
+# vim: set shiftwidth=4 softtabstop=4:
+"""Function to help find a program path
+"""
+import os
+from typing import AnyStr, Optional
+
+
+def which(program: AnyStr) -> Optional[AnyStr]:
+    """Return path of program if it exists
+from:
+https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python/377028#377028
+    """
+
+    def is_exe(fpath):
+        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
+
+    fpath, fname = os.path.split(program)
+    if fpath:
+        if is_exe(program):
+            return program
+    else:
+        for path in os.environ["PATH"].split(os.pathsep):
+            exe_file = os.path.join(path, program)
+            if is_exe(exe_file):
+                return exe_file
+
+    return None
diff --git a/requirements b/requirements
--- a/requirements
+++ b/requirements
@@ -5,6 +5,7 @@
 PyYAML
 coloredlogs
 docker
+dockerpty
 psycopg2
 mercurial >=5.2
 python-hglib
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@
     extras_require={
         # used to require PyYAML but it is used in import scripts too
         "conf2reST": [],
-        "docker": ["docker"],
+        "docker": ["docker >=3.4", "dockerpty"],
         "import_sql": ["psycopg2"],
         "source_control": [
             # Only mercurial 5.2 support Python 3.5+
@@ -39,6 +39,8 @@
             "docker_build=odoo_scripts.docker_build:main [docker]",
             "docker_build_clean=odoo_scripts.docker_build_clean:main",
             "docker_build_copy=odoo_scripts.docker_build_copy:main",
+            "docker_flake8=odoo_scripts.docker_flake8:main [docker]",
+            "docker_isort=odoo_scripts.docker_isort:main [docker]",
             "conf2reST=odoo_scripts.conf2reST:main [conf2reST]",
             "list_modules=odoo_scripts.list_modules:main",
             "update_duplicate_sources="