2018-12-11 17:11:01 +00:00
|
|
|
import argparse
|
|
|
|
import sys
|
2018-12-11 20:26:21 +00:00
|
|
|
import os
|
2018-12-13 08:01:26 +00:00
|
|
|
import logging
|
2017-05-23 03:39:12 +00:00
|
|
|
from .app import Repo2Docker
|
2020-02-11 18:30:18 +00:00
|
|
|
from .engine import BuildError, ImageLoadError
|
2018-12-11 17:11:01 +00:00
|
|
|
from . import __version__
|
2022-10-03 21:49:22 +00:00
|
|
|
from .utils import (
|
|
|
|
validate_and_generate_port_mapping,
|
|
|
|
is_valid_docker_image_name,
|
|
|
|
R2dState,
|
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
2019-04-30 15:35:58 +00:00
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
def validate_image_name(image_name):
|
|
|
|
"""
|
|
|
|
Validate image_name read by argparse
|
|
|
|
|
|
|
|
Note: Container names must start with an alphanumeric character and
|
|
|
|
can then use _ . or - in addition to alphanumeric.
|
|
|
|
[a-zA-Z0-9][a-zA-Z0-9_.-]+
|
|
|
|
|
|
|
|
Args:
|
|
|
|
image_name (string): argument read by the argument parser
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
unmodified image_name
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
ArgumentTypeError: if image_name contains characters that do not
|
|
|
|
meet the logic that container names must start
|
|
|
|
with an alphanumeric character and can then
|
|
|
|
use _ . or - in addition to alphanumeric.
|
|
|
|
[a-zA-Z0-9][a-zA-Z0-9_.-]+
|
|
|
|
"""
|
|
|
|
if not is_valid_docker_image_name(image_name):
|
2019-05-31 09:10:17 +00:00
|
|
|
msg = (
|
2020-07-27 19:32:34 +00:00
|
|
|
"%r is not a valid docker image name. Image name "
|
|
|
|
"must start with a lowercase or numeric character and "
|
|
|
|
"can then use _ . or - in addition to lowercase and numeric." % image_name
|
2019-05-31 09:10:17 +00:00
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
return image_name
|
|
|
|
|
2019-05-31 09:10:17 +00:00
|
|
|
|
2020-08-10 07:33:05 +00:00
|
|
|
# See https://github.com/jupyterhub/repo2docker/issues/871 for reason
|
2020-04-19 23:00:10 +00:00
|
|
|
class MimicDockerEnvHandling(argparse.Action):
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
# There are 3 cases:
|
|
|
|
# key=value pass as is
|
|
|
|
# key= pass as is
|
|
|
|
# key pass using current value, or don't pass
|
|
|
|
if "=" not in values:
|
|
|
|
try:
|
|
|
|
value_to_append = "{}={}".format(values, os.environ[values])
|
|
|
|
except KeyError:
|
|
|
|
# no local def, so don't pass
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
value_to_append = values
|
|
|
|
|
|
|
|
# destination variable is initially defined as an empty list, so
|
|
|
|
# no special casing of first time is needed.
|
|
|
|
getattr(namespace, self.dest).append(value_to_append)
|
|
|
|
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
def get_argparser():
|
|
|
|
"""Get arguments that may be used by repo2docker"""
|
2018-12-17 19:49:43 +00:00
|
|
|
argparser = argparse.ArgumentParser(
|
2019-05-31 09:10:17 +00:00
|
|
|
description="Fetch a repository and build a container image"
|
2018-12-17 19:49:43 +00:00
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
2022-01-27 13:01:45 +00:00
|
|
|
argparser.add_argument(
|
|
|
|
"--help-all",
|
|
|
|
dest="help_all",
|
|
|
|
action="store_true",
|
|
|
|
help="Display all configurable options and exit.",
|
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
|
|
|
"--version",
|
|
|
|
dest="version",
|
|
|
|
action="store_true",
|
|
|
|
help="Print the repo2docker version and exit.",
|
|
|
|
)
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--config",
|
|
|
|
default="repo2docker_config.py",
|
|
|
|
help="Path to config file for repo2docker",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--json-logs",
|
2018-12-11 17:11:01 +00:00
|
|
|
default=False,
|
2019-05-31 09:10:17 +00:00
|
|
|
action="store_true",
|
|
|
|
help="Emit JSON logs instead of human readable logs",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"repo",
|
|
|
|
help=(
|
|
|
|
"Path to repository that should be built. Could be "
|
|
|
|
"local path or a git URL."
|
|
|
|
),
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--image-name",
|
|
|
|
help=("Name of image to be built. If unspecified will be " "autogenerated"),
|
|
|
|
type=validate_image_name,
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--ref",
|
|
|
|
help=(
|
2020-10-22 05:46:20 +00:00
|
|
|
"Reference to build instead of default reference. For example"
|
|
|
|
" branch name or commit for a Git repository."
|
2019-05-31 09:10:17 +00:00
|
|
|
),
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
2019-05-31 09:10:17 +00:00
|
|
|
argparser.add_argument("--debug", help="Turn on debug logging", action="store_true")
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--no-build",
|
|
|
|
dest="build",
|
|
|
|
action="store_false",
|
|
|
|
help=(
|
|
|
|
"Do not actually build the image. Useful in conjunction " "with --debug."
|
|
|
|
),
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--build-memory-limit",
|
|
|
|
help="Total Memory that can be used by the docker build process",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"cmd",
|
2018-12-11 17:11:01 +00:00
|
|
|
nargs=argparse.REMAINDER,
|
2019-05-31 09:10:17 +00:00
|
|
|
help="Custom command to run after building container",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--no-run",
|
|
|
|
dest="run",
|
|
|
|
action="store_false",
|
|
|
|
help="Do not run container after it has been built",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--publish",
|
|
|
|
"-p",
|
|
|
|
dest="ports",
|
|
|
|
action="append",
|
|
|
|
help=(
|
|
|
|
"Specify port mappings for the image. Needs a command to "
|
|
|
|
"run in the container."
|
|
|
|
),
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--publish-all",
|
|
|
|
"-P",
|
|
|
|
dest="all_ports",
|
|
|
|
action="store_true",
|
|
|
|
help="Publish all exposed ports to random host ports.",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--no-clean",
|
|
|
|
dest="clean",
|
|
|
|
action="store_false",
|
|
|
|
help="Don't clean up remote checkouts after we are done",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--push",
|
|
|
|
dest="push",
|
|
|
|
action="store_true",
|
|
|
|
help="Push docker image to repository",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--volume",
|
|
|
|
"-v",
|
|
|
|
dest="volumes",
|
|
|
|
action="append",
|
|
|
|
help="Volumes to mount inside the container, in form src:dest",
|
|
|
|
default=[],
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--user-id", help="User ID of the primary user in the image", type=int
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--user-name", help="Username of the primary user in the image"
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
2020-04-18 17:23:38 +00:00
|
|
|
# Process the environment options the same way that docker does, as
|
|
|
|
# they are passed directly to docker as the environment to use. This
|
|
|
|
# requires a custom action for argparse.
|
2018-12-11 17:11:01 +00:00
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--env",
|
|
|
|
"-e",
|
|
|
|
dest="environment",
|
2020-04-19 23:00:10 +00:00
|
|
|
action=MimicDockerEnvHandling,
|
2019-05-31 09:10:17 +00:00
|
|
|
help="Environment variables to define at container run time",
|
|
|
|
default=[],
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-05-31 09:10:17 +00:00
|
|
|
"--editable",
|
|
|
|
"-E",
|
|
|
|
dest="editable",
|
|
|
|
action="store_true",
|
|
|
|
help="Use the local repository in edit mode",
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
2019-05-31 09:10:17 +00:00
|
|
|
argparser.add_argument("--target-repo-dir", help=Repo2Docker.target_repo_dir.help)
|
2018-12-17 23:32:17 +00:00
|
|
|
|
2019-09-07 11:34:07 +00:00
|
|
|
argparser.add_argument("--appendix", type=str, help=Repo2Docker.appendix.help)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
2021-11-23 19:37:37 +00:00
|
|
|
argparser.add_argument(
|
|
|
|
"--label",
|
|
|
|
dest="labels",
|
|
|
|
action="append",
|
|
|
|
help="Extra label to set on the image, in form name=value",
|
|
|
|
default=[],
|
|
|
|
)
|
|
|
|
|
2021-11-24 18:50:29 +00:00
|
|
|
argparser.add_argument(
|
|
|
|
"--build-arg",
|
|
|
|
dest="build_args",
|
|
|
|
action="append",
|
|
|
|
help="Extra build arg to pass to the build process, in form name=value",
|
|
|
|
default=[],
|
|
|
|
)
|
|
|
|
|
2019-09-07 11:34:07 +00:00
|
|
|
argparser.add_argument("--subdir", type=str, help=Repo2Docker.subdir.help)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
argparser.add_argument(
|
2019-09-07 11:34:07 +00:00
|
|
|
"--cache-from", action="append", default=[], help=Repo2Docker.cache_from.help
|
2018-12-11 17:11:01 +00:00
|
|
|
)
|
|
|
|
|
2020-02-13 11:46:29 +00:00
|
|
|
argparser.add_argument("--engine", help="Name of the container engine")
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
return argparser
|
|
|
|
|
|
|
|
|
2018-12-17 19:06:32 +00:00
|
|
|
argparser = get_argparser()
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
def make_r2d(argv=None):
|
|
|
|
if argv is None:
|
|
|
|
argv = sys.argv[1:]
|
|
|
|
|
2022-01-27 13:01:45 +00:00
|
|
|
argparser = get_argparser()
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
# version must be checked before parse, as repo/cmd are required and
|
|
|
|
# will spit out an error if allowed to be parsed first.
|
2019-05-31 09:10:17 +00:00
|
|
|
if "--version" in argv:
|
2018-12-11 17:11:01 +00:00
|
|
|
print(__version__)
|
|
|
|
sys.exit(0)
|
|
|
|
|
2022-01-27 13:01:45 +00:00
|
|
|
if "--help-all" in argv:
|
|
|
|
argparser.print_help()
|
|
|
|
print("\nAll configurable options:\n")
|
|
|
|
Repo2Docker().print_help(classes=True)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
args, traitlet_args = argparser.parse_known_args(argv)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
r2d = Repo2Docker()
|
2022-01-26 09:15:38 +00:00
|
|
|
r2d.parse_command_line(traitlet_args)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
if args.debug:
|
|
|
|
r2d.log_level = logging.DEBUG
|
|
|
|
|
|
|
|
r2d.load_config_file(args.config)
|
|
|
|
if args.appendix:
|
|
|
|
r2d.appendix = args.appendix
|
|
|
|
|
2021-11-23 19:37:37 +00:00
|
|
|
for l in args.labels:
|
2021-12-16 12:38:56 +00:00
|
|
|
key, _, val = l.partition("=")
|
|
|
|
r2d.labels[key] = val
|
2021-11-23 19:37:37 +00:00
|
|
|
|
2021-11-24 18:50:29 +00:00
|
|
|
for a in args.build_args:
|
|
|
|
key, _, val = a.partition("=")
|
|
|
|
r2d.extra_build_args[key] = val
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
r2d.repo = args.repo
|
|
|
|
r2d.ref = args.ref
|
|
|
|
|
|
|
|
# user wants to mount a local directory into the container for
|
|
|
|
# editing
|
|
|
|
if args.editable:
|
|
|
|
# the user has to point at a directory, not just a path for us
|
|
|
|
# to be able to mount it. We might have content providers that can
|
|
|
|
# provide content from a local `something.zip` file, which we
|
|
|
|
# couldn't mount in editable mode
|
|
|
|
if os.path.isdir(args.repo):
|
2019-05-31 09:10:17 +00:00
|
|
|
r2d.volumes[os.path.abspath(args.repo)] = "."
|
2018-12-11 17:11:01 +00:00
|
|
|
else:
|
2019-05-31 09:10:17 +00:00
|
|
|
r2d.log.error(
|
|
|
|
'Cannot mount "{}" in editable mode '
|
|
|
|
"as it is not a directory".format(args.repo),
|
2022-10-03 21:49:22 +00:00
|
|
|
extra=dict(phase=R2dState.FAILED),
|
2019-05-31 09:10:17 +00:00
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
if args.image_name:
|
|
|
|
r2d.output_image_spec = args.image_name
|
2018-12-17 08:18:30 +00:00
|
|
|
else:
|
|
|
|
# we will pick a name after fetching the repository
|
|
|
|
r2d.output_image_spec = ""
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
r2d.json_logs = args.json_logs
|
|
|
|
|
2018-12-12 00:06:53 +00:00
|
|
|
r2d.dry_run = not args.build
|
2018-12-11 20:36:06 +00:00
|
|
|
|
2018-12-12 00:06:53 +00:00
|
|
|
if r2d.dry_run:
|
2018-12-11 17:11:01 +00:00
|
|
|
# Can't push nor run if we aren't building
|
2018-12-11 20:36:06 +00:00
|
|
|
args.run = False
|
|
|
|
args.push = False
|
|
|
|
|
|
|
|
r2d.run = args.run
|
|
|
|
r2d.push = args.push
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
# check against r2d.run and not args.run as r2d.run is false on
|
2018-12-13 08:01:26 +00:00
|
|
|
# --no-build. Also r2d.volumes and not args.volumes since --editable
|
|
|
|
# modified r2d.volumes
|
|
|
|
if r2d.volumes and not r2d.run:
|
2018-12-11 17:11:01 +00:00
|
|
|
# Can't mount if we aren't running
|
2019-05-31 09:10:17 +00:00
|
|
|
print("To Mount volumes with -v, you also need to run the " "container")
|
2018-12-11 17:11:01 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
for v in args.volumes:
|
2019-05-31 09:10:17 +00:00
|
|
|
src, dest = v.split(":")
|
2018-12-11 17:11:01 +00:00
|
|
|
r2d.volumes[src] = dest
|
|
|
|
|
|
|
|
r2d.run_cmd = args.cmd
|
|
|
|
|
|
|
|
if args.all_ports and not r2d.run:
|
2019-05-31 09:10:17 +00:00
|
|
|
print(
|
|
|
|
"To publish user defined port mappings, the container must " "also be run"
|
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
if args.ports and not r2d.run:
|
2019-05-31 09:10:17 +00:00
|
|
|
print(
|
|
|
|
"To publish user defined port mappings, the container must " "also be run"
|
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
2020-06-26 10:38:25 +00:00
|
|
|
if args.ports and len(args.ports) > 1 and not r2d.run_cmd:
|
2019-05-31 09:10:17 +00:00
|
|
|
print(
|
|
|
|
"To publish user defined port mapping, user must specify "
|
|
|
|
"the command to run in the container"
|
|
|
|
)
|
2018-12-11 17:11:01 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
r2d.ports = validate_and_generate_port_mapping(args.ports)
|
|
|
|
r2d.all_ports = args.all_ports
|
|
|
|
|
|
|
|
if args.user_id:
|
|
|
|
r2d.user_id = args.user_id
|
|
|
|
if args.user_name:
|
|
|
|
r2d.user_name = args.user_name
|
2019-06-28 16:46:01 +00:00
|
|
|
if r2d.user_id == 0 and not r2d.dry_run:
|
|
|
|
print("Root as the primary user in the image is not permitted.")
|
|
|
|
print(
|
|
|
|
"The uid and the username of the user invoking repo2docker "
|
|
|
|
"is used to create a mirror account in the image by default. "
|
|
|
|
"To override that behavior pass --user-id <numeric_id> and "
|
|
|
|
" --user-name <string> to repo2docker.\n"
|
|
|
|
"Please see repo2docker --help for more details.\n"
|
|
|
|
)
|
|
|
|
sys.exit(1)
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
if args.build_memory_limit:
|
2019-04-27 08:49:32 +00:00
|
|
|
# if the string only contains numerals we assume it should be an int
|
2019-04-30 17:46:34 +00:00
|
|
|
# and specifies a size in bytes
|
2019-04-27 08:49:32 +00:00
|
|
|
if args.build_memory_limit.isnumeric():
|
|
|
|
r2d.build_memory_limit = int(args.build_memory_limit)
|
|
|
|
else:
|
|
|
|
r2d.build_memory_limit = args.build_memory_limit
|
2018-12-11 17:11:01 +00:00
|
|
|
|
|
|
|
if args.environment and not r2d.run:
|
2019-05-31 09:10:17 +00:00
|
|
|
print("To specify environment variables, you also need to run " "the container")
|
2018-12-11 17:11:01 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
if args.subdir:
|
|
|
|
r2d.subdir = args.subdir
|
|
|
|
|
|
|
|
if args.cache_from:
|
|
|
|
r2d.cache_from = args.cache_from
|
|
|
|
|
2020-02-13 11:46:29 +00:00
|
|
|
if args.engine:
|
|
|
|
r2d.engine = args.engine
|
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
r2d.environment = args.environment
|
|
|
|
|
2018-12-11 20:26:21 +00:00
|
|
|
# if the source exists locally we don't want to delete it at the end
|
2018-12-13 18:03:55 +00:00
|
|
|
# FIXME: Find a better way to figure out if repo is 'local'. Push this into ContentProvider?
|
2018-12-11 20:26:21 +00:00
|
|
|
if os.path.exists(args.repo):
|
|
|
|
r2d.cleanup_checkout = False
|
|
|
|
else:
|
|
|
|
r2d.cleanup_checkout = args.clean
|
|
|
|
|
2018-12-18 19:21:19 +00:00
|
|
|
if args.target_repo_dir:
|
|
|
|
r2d.target_repo_dir = args.target_repo_dir
|
2018-12-17 23:32:17 +00:00
|
|
|
|
2018-12-11 17:11:01 +00:00
|
|
|
return r2d
|
2017-05-23 03:39:12 +00:00
|
|
|
|
2018-02-05 21:41:40 +00:00
|
|
|
|
2017-05-23 04:30:28 +00:00
|
|
|
def main():
|
2018-12-11 17:11:01 +00:00
|
|
|
r2d = make_r2d()
|
|
|
|
r2d.initialize()
|
2018-12-12 18:42:52 +00:00
|
|
|
try:
|
|
|
|
r2d.start()
|
2020-02-11 18:30:18 +00:00
|
|
|
except BuildError as e:
|
2018-12-12 18:42:52 +00:00
|
|
|
# This is only raised by us
|
2019-02-07 22:49:25 +00:00
|
|
|
if r2d.log_level == logging.DEBUG:
|
2018-12-12 18:42:52 +00:00
|
|
|
r2d.log.exception(e)
|
|
|
|
sys.exit(1)
|
2020-02-11 18:30:18 +00:00
|
|
|
except ImageLoadError as e:
|
2018-12-12 18:42:52 +00:00
|
|
|
# This is only raised by us
|
2019-02-07 22:49:25 +00:00
|
|
|
if r2d.log_level == logging.DEBUG:
|
2018-12-12 18:42:52 +00:00
|
|
|
r2d.log.exception(e)
|
|
|
|
sys.exit(1)
|
2018-02-05 21:41:40 +00:00
|
|
|
|
2018-12-17 08:18:30 +00:00
|
|
|
|
2019-05-31 09:10:17 +00:00
|
|
|
if __name__ == "__main__":
|
2017-05-23 04:30:28 +00:00
|
|
|
main()
|