From cc23232f32789e7510fe94fbde735d42dac119eb Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Sun, 15 Mar 2020 14:50:58 +0100 Subject: [PATCH] replace pytimeparse with dateparser --- Pipfile | 2 +- Pipfile.lock | 84 +++++++++++++++++++--------- dockertidy/Cli.py | 34 +++++------ dockertidy/GarbageCollector.py | 4 +- dockertidy/Parser.py | 33 ++++++----- docs/content/configuration/_index.md | 10 ++-- docs/content/usage/_index.md | 6 +- setup.py | 5 +- 8 files changed, 103 insertions(+), 75 deletions(-) diff --git a/Pipfile b/Pipfile index 1b12f51..60eb147 100644 --- a/Pipfile +++ b/Pipfile @@ -34,7 +34,6 @@ docker-pycreds = "*" idna = "*" ipaddress = "*" python-dateutil = "*" -pytimeparse = "*" requests = "*" appdirs = "*" colorama = "*" @@ -46,3 +45,4 @@ environs = "*" nested-lookup = "*" "ruamel.yaml" = "*" websocket-client = "*" +dateparser = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 12c6f48..5e0c330 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bf5cf1a93672e4870e15ed4800cde2558d3915c10d294353e6e4c28c4e14524e" + "sha256": "2655d80711d8731029d69fc90a9adf0fcad44197d18758eaffceb9a93ac38091" }, "pipfile-spec": 6, "requires": {}, @@ -60,6 +60,14 @@ "index": "pypi", "version": "==0.4.3" }, + "dateparser": { + "hashes": [ + "sha256:1b1f0e3034f82d1f92b45fa445826da6a36d67af8a1169e04869685594276011", + "sha256:fb5bfde4795fa4b179fe05c2c25b3981f785de26bec37e247dee1079c63d5689" + ], + "index": "pypi", + "version": "==0.7.4" + }, "docker": { "hashes": [ "sha256:1c2ddb7a047b2599d1faec00889561316c674f7099427b9c51e8cb804114b553", @@ -167,13 +175,38 @@ "index": "pypi", "version": "==0.1.11" }, - "pytimeparse": { + "pytz": { "hashes": [ - "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd", - "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a" + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" ], - "index": "pypi", - "version": "==1.1.8" + "version": "==2019.3" + }, + "regex": { + "hashes": [ + "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", + "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", + "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", + "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", + "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", + "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", + "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", + "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", + "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", + "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", + "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", + "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", + "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", + "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", + "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", + "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", + "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", + "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", + "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", + "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", + "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" + ], + "version": "==2020.2.20" }, "requests": { "hashes": [ @@ -223,6 +256,13 @@ ], "version": "==1.14.0" }, + "tzlocal": { + "hashes": [ + "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048", + "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590" + ], + "version": "==2.0.0" + }, "urllib3": { "hashes": [ "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", @@ -324,10 +364,10 @@ }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", + "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" ], - "version": "==7.0" + "version": "==7.1.1" }, "colorama": { "hashes": [ @@ -438,11 +478,11 @@ }, "flake8-builtins": { "hashes": [ - "sha256:29bc0f7e68af481d088f5c96f8aeb02520abdfc900500484e3af969f42a38a5f", - "sha256:c44415fb19162ef3737056e700d5b99d48c3612a533943b4e16419a5d3de3a64" + "sha256:5de3917b9b6d81e8b92d56ebc2873cc178978658848a7a16a638a6ea5842f70c", + "sha256:b4aaa42bf503ae287c436a822374996542003ddfd73a971988b4c383652c9c58" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.5.0" }, "flake8-colors": { "hashes": [ @@ -620,10 +660,10 @@ }, "pip-shims": { "hashes": [ - "sha256:1cc3e2e4e5d5863edd4760d2032b180a6ef81719277fe95404df1bb0e58b7261", - "sha256:b5bb01c4394a2e0260bddb4cfdc7e6fdd9d6e61c8febd18c3594e2ea2596c190" + "sha256:2b9a88ff0fd31e7d27a362d3e36e6e75d8fbc339c9c4367f4a97b72b22e6f4f4", + "sha256:5861da6f48e60b55d40b984795c63681e4db7ac576c1c3b05f4b54a9d508e3da" ], - "version": "==0.5.0" + "version": "==0.5.1" }, "pipenv-setup": { "hashes": [ @@ -701,11 +741,11 @@ }, "pytest": { "hashes": [ - "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", - "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" + "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", + "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" ], "index": "pypi", - "version": "==5.3.5" + "version": "==5.4.1" }, "pytest-cov": { "hashes": [ @@ -745,14 +785,6 @@ "index": "pypi", "version": "==0.1.11" }, - "pytimeparse": { - "hashes": [ - "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd", - "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a" - ], - "index": "pypi", - "version": "==1.1.8" - }, "pyyaml": { "hashes": [ "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", diff --git a/dockertidy/Cli.py b/dockertidy/Cli.py index 04e10db..53a453f 100644 --- a/dockertidy/Cli.py +++ b/dockertidy/Cli.py @@ -30,15 +30,13 @@ class DockerTidy: :return: args objec """ - parser = argparse.ArgumentParser( - description="Generate documentation from annotated Ansible roles using templates" - ) + parser = argparse.ArgumentParser(description="keep docker hosts tidy") parser.add_argument( "--dry-run", action="store_true", default=None, dest="dry_run", - help="Only log actions, don't stop anything." + help="only log actions, don't stop anything" ) parser.add_argument( "-t", @@ -46,7 +44,7 @@ class DockerTidy: type=int, dest="http_timeout", metavar="HTTP_TIMEOUT", - help="HTTP timeout in seconds for making docker API calls." + help="HTTP timeout in seconds for making docker API calls" ) parser.add_argument( "-v", dest="logging.level", action="append_const", const=-1, help="increase log level" @@ -61,30 +59,28 @@ class DockerTidy: subparsers = parser.add_subparsers(dest="command", help="sub-command help") subparsers.required = True - parser_gc = subparsers.add_parser("gc", help="Run docker garbage collector.") + parser_gc = subparsers.add_parser("gc", help="run docker garbage collector") parser_gc.add_argument( "--max-container-age", type=timedelta_validator, dest="gc.max_container_age", metavar="MAX_CONTAINER_AGE", - help="Maximum age for a container. Containers older than this age " - "will be removed. Age can be specified in any pytimeparse " - "supported format." + help="maximum age for a container, containers older than this age " + "will be removed (dateparser value)" ) parser_gc.add_argument( "--max-image-age", type=timedelta_validator, dest="gc.max_image_age", metavar="MAX_IMAGE_AGE", - help="Maxium age for an image. Images older than this age will be " - "removed. Age can be specified in any pytimeparse supported " - "format." + help="maxium age for an image, images older than this age will be " + "removed (dateparser value)" ) parser_gc.add_argument( "--dangling-volumes", action="store_true", dest="gc.dangling_volumes", - help="Dangling volumes will be removed." + help="dangling volumes will be removed" ) parser_gc.add_argument( "--exclude-image", @@ -92,7 +88,7 @@ class DockerTidy: type=str, dest="gc.exclude_images", metavar="EXCLUDE_IMAGE", - help="Never remove images with this tag." + help="never remove images with this tag" ) parser_gc.add_argument( "--exclude-container-label", @@ -100,20 +96,19 @@ class DockerTidy: type=str, dest="gc.exclude_container_labels", metavar="EXCLUDE_CONTAINER_LABEL", - help="Never remove containers with this label key " + help="never remove containers with this label key " "or label key=value" ) parser_stop = subparsers.add_parser( - "stop", help="Stop containers that have been running for too long." + "stop", help="stop containers that have been running for too long" ) parser_stop.add_argument( "--max-run-time", type=timedelta_validator, dest="stop.max_run_time", metavar="MAX_RUN_TIME", - help="Maximum time a container is allows to run. Time may " - "be specified in any pytimeparse supported format." + help="maximum time a container is allows to run (dateparser value)" ) parser_stop.add_argument( "--prefix", @@ -121,8 +116,7 @@ class DockerTidy: type=str, dest="stop.prefix", metavar="PREFIX", - help="Only stop containers which match one of the " - "prefix." + help="only stop containers which match one of the prefix" ) return parser.parse_args().__dict__ diff --git a/dockertidy/GarbageCollector.py b/dockertidy/GarbageCollector.py index 6b416a6..33c652b 100644 --- a/dockertidy/GarbageCollector.py +++ b/dockertidy/GarbageCollector.py @@ -313,7 +313,7 @@ class GarbageCollector: self.cleanup_volumes() if ( - not config["gc"]["max_container_age"] or not config["gc"]["max_image_age"] - or not config["gc"]["dangling_volumes"] + not config["gc"]["max_container_age"] and not config["gc"]["max_image_age"] + and not config["gc"]["dangling_volumes"] ): self.logger.warn("Skipped, no arguments given") diff --git a/dockertidy/Parser.py b/dockertidy/Parser.py index 47d650d..9e03e9c 100644 --- a/dockertidy/Parser.py +++ b/dockertidy/Parser.py @@ -1,52 +1,51 @@ #!/usr/bin/env python3 """Custom input type parser.""" -import datetime +from argparse import ArgumentTypeError +import dateparser import environs -from dateutil import tz -from pytimeparse import timeparse env = environs.Env() def timedelta_validator(value): - """Return the :class:`datetime.datetime.DateTime` for a time in the past. + """Return the dateparser string for a time in the past. :param value: a string containing a time format supported by - mod:`pytimeparse` + mod:`dateparser` """ if value is None: return None - try: - _datetime_seconds_ago(timeparse.timeparse(value)) - return value - except TypeError: - raise + if not dateparser.parse(value): + raise ArgumentTypeError("'{}' is not a valid timedelta string".format(value)) + + return value def timedelta(value, dt_format=None): """Return the :class:`datetime.datetime.DateTime` for a time in the past. :param value: a string containing a time format supported by - mod:`pytimeparse` + mod:`dateparser` """ if value is None: return None - timedelta = _datetime_seconds_ago(timeparse.timeparse(value)) + timedelta = dateparser.parse( + value, settings={ + "TO_TIMEZONE": "UTC", + "RETURN_AS_TIMEZONE_AWARE": True + } + ) + if dt_format: timedelta = timedelta.strftime(dt_format) return timedelta -def _datetime_seconds_ago(seconds): - now = datetime.datetime.now(tz.tzutc()) - return now - datetime.timedelta(seconds=seconds) - - @env.parser_for("timedelta_validator") def timedelta_parser(value): try: diff --git a/docs/content/configuration/_index.md b/docs/content/configuration/_index.md index 51c402c..8f00273 100644 --- a/docs/content/configuration/_index.md +++ b/docs/content/configuration/_index.md @@ -74,18 +74,18 @@ $ docker-tidy --help usage: docker-tidy [-h] [--dry-run] [-t HTTP_TIMEOUT] [-v] [-q] [--version] {gc,stop} ... -Generate documentation from annotated Ansible roles using templates +keep docker hosts tidy positional arguments: {gc,stop} sub-command help - gc Run docker garbage collector. - stop Stop containers that have been running for too long. + gc run docker garbage collector + stop stop containers that have been running for too long optional arguments: -h, --help show this help message and exit - --dry-run Only log actions, don't stop anything. + --dry-run only log actions, don't stop anything -t HTTP_TIMEOUT, --timeout HTTP_TIMEOUT - HTTP timeout in seconds for making docker API calls. + HTTP timeout in seconds for making docker API calls -v increase log level -q decrease log level --version show program's version number and exit diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md index 0493ee0..1d11ded 100644 --- a/docs/content/usage/_index.md +++ b/docs/content/usage/_index.md @@ -13,12 +13,12 @@ than \"max age\". Running containers, and images which are used by a container are never removed. Maximum age can be specificied with any format supported by -[pytimeparse](https://github.com/wroberts/pytimeparse). +[dateparser](https://dateparser.readthedocs.io/en/latest/index.html#features). __Example:__ ```Shell -docker-tidy gc --max-container-age 3days --max-image-age 30days +docker-tidy gc --max-container-age "3 days ago" --max-image-age "30 days ago" ``` ### Prevent images from being removed @@ -58,5 +58,5 @@ If no prefix is set, __all__ containers matching the `max-run-time` will be stop __Example:__ ```Shell -docker-tidy stop --max-run-time 2days --prefix "projectprefix_" +docker-tidy stop --max-run-time "2 days ago" --prefix "projectprefix_" ``` diff --git a/setup.py b/setup.py index c34b8ec..32ce7e8 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ setup( "certifi==2019.11.28", "chardet==3.0.4", "colorama==0.4.3", + "dateparser==0.7.4", "docker==4.2.0", "docker-pycreds==0.4.0", "environs==7.3.0", @@ -83,11 +84,13 @@ setup( "python-dateutil==2.8.1", "python-dotenv==0.12.0", "python-json-logger==0.1.11", - "pytimeparse==1.1.8", + "pytz==2019.3", + "regex==2020.2.20", "requests==2.23.0", "ruamel.yaml==0.16.10", "ruamel.yaml.clib==0.2.0; platform_python_implementation == 'CPython' and python_version < '3.9'", "six==1.14.0", + "tzlocal==2.0.0", "urllib3==1.25.8", "websocket-client==0.57.0", "zipp==1.2.0",