├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── artifactory_du ├── __init__.py ├── __main__.py ├── artifactory_du.py ├── du.py ├── requirements.txt └── version.py ├── cli.py ├── setup.cfg ├── setup.py └── tests └── test_du.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # PyCharm 35 | .idea/ 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | sudo: required 3 | dist: trusty 4 | language: python 5 | matrix: 6 | include: 7 | - python: 3.4 8 | if: branch != master 9 | - python: 3.5 10 | if: branch != master 11 | - python: 3.6 12 | branches: 13 | only: 14 | - master 15 | - develop 16 | install: 17 | - pip install --upgrade pip 18 | - pip install -r artifactory_du/requirements.txt 19 | script: 20 | - printenv 21 | - ls -la 22 | - python -m pytest tests 23 | 24 | deploy: 25 | - provider: pypi 26 | user: devopshq 27 | password: 28 | secure: "OEUMpcA+R4jj59lhw99hSz9RI0FbQF2Oz1tr77Shxf8FNUflsLqjQIIdtUfUEwsxt3paEYJQmc6TMciRZTpaup+iADdeCBYAEl3E42xKs5i+q4765BTO8G6fQQP1mIDakty2pBX97YzinjARrcDw+hB5XkcRY2qWKjbpnutBWtYT9WYmBsLABq4pM35rwhrj0Bh7mKu56XrxjemIb5zDZFmC1kZW/5yhYx3L+YoN88dkypcAsmcVaEZC9es5D20wqeVTQYGfkKbgw4mVxQENlJ7DeM8fS/3gTWBTemn1HDEA02K+C6vVEUiFuXHIV60B47O/mZmt64o7bdXhPtlAS0VUjqDkid4k/3GPjMXqoNbMR2rSt5PhuwIGxskNFTv+nvvEKiJE/3eqS6g9Dx5r5Ix8elCgPnUTvxvRzXv2tCto5/8qA+tWo3aPyYciCSAXN1I9h333zIaxZVNac+37KUS/Go+sYSqbnj53kTojvtKvVnLV/FrE4vy5rh7tC4ctgxRcjYIDSiSsCEHMHV4Vd4QQPXpJWGFMnwss3zTYZbTtlAsAtFnBhhnDL7EjkTkrcTDBD/kJrX2yTTuOGguXeeHRLTLTk09s75yVHkPbmp73ul+JNw7WFeldVBu8NmSxZuuJAD3AFERlsf2t9tDU1wwnHjyguWz2i7EZ7ajC7K4=" 29 | distributions: sdist bdist_wheel 30 | on: 31 | branch: master 32 | skip_cleanup: true 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | RUN python -mpip install artifactory-du && rm -rf ~/.cache 4 | ENTRYPOINT [ "artifactory-du" ] 5 | CMD [ "--help" ] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Open DevOps Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-release: 2 | docker build . -t --no-cache devopshq/artifactory-du:latest 3 | docker push devopshq/artifactory-du:latest 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Artifactory Disk Usage cli (artifactory-du) 2 | ========================================== 3 | 4 | [![docs](https://img.shields.io/readthedocs/pip.svg)](https://devopshq.github.io/artifactory-du/) [![dohq build status](https://travis-ci.org/devopshq/artifactory-du.svg)](https://travis-ci.org/devopshq/artifactory-du) [![dohq on PyPI](https://img.shields.io/pypi/v/artifactory-du.svg)](https://pypi.python.org/pypi/artifactory-du) [![artifactory-du license](https://img.shields.io/pypi/l/vspheretools.svg)](https://github.com/devopshq/artifactory-du/blob/master/LICENSE) 5 | 6 | `artifactory-du` - estimate file space usage 7 | 8 | Summarize disk usage in JFrog Artifactory of the set of FILEs, recursively for directories. 9 | 10 | # Table of Contents 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Artifactory options](#artifactory-options) 14 | - [Connection](#connection) 15 | - [Specific](#specific) 16 | - [DU options](#du-options) 17 | - [Known issues](#known-issues) 18 | - [CONTRIBUTING](#contributing) 19 | - [AD](#ad) 20 | 21 | 22 | # Installation 23 | The easiest way is using docker! 24 | ```bash 25 | docker pull devopshq/artifactory-du 26 | docker run devopshq/artifactory-du --version 27 | ``` 28 | 29 | ```cmd 30 | # Install from PyPi 31 | # python -mpip install artifactory-du 32 | 33 | # From git 34 | python -mpip install git+https://github.com/devopshq/artifactory-du.git 35 | 36 | # and try to get help 37 | artifactory-du --help 38 | ``` 39 | 40 | # Usage 41 | `artifactory-du` is used in the same manner as original `du` from *nix, although launch options are different. See artifactory-du --help for details. 42 | ```cmd 43 | # Recursive summary for root folder in repo.snapshot 44 | artifactory-du --username username --password password --artifactory-url https://repo.example.ru/artifactory --repository repo.snapshot -h -s * 45 | 46 | # Set alias for linux 47 | alias adu=artifactory-du --username username --password password --artifactory-url https://repo.example.ru/artifactory --repository repo.snapshot -h 48 | # usage 49 | adu --max-depth=2 /* 50 | 51 | # Set alias for Windows 52 | set "adu=artifactory-du --username username --password password --artifactory-url https://repo.example.ru/artifactory --repository repo.snapshot -h" 53 | # usage 54 | %adu% --max-depth=2 /* 55 | 56 | ``` 57 | 58 | Below we skip artifactory-specific options: `username, password, artifactory-url, repository`, because we use ALIAS (for [linux-bash](https://askubuntu.com/questions/17536/how-do-i-create-a-permanent-bash-alias) or [windows-cmd](https://superuser.com/a/560558) 59 | 60 | ```cmd 61 | # Summary for subfolder in folder 62 | adu --max-depth=2 folder/* 63 | 64 | # show 2 folder level inside repository 65 | adu --max-depth=2 * 66 | 67 | # Show only directory with GB size 68 | adu --max-depth=0 * | grep G 69 | 70 | # Show artifacts that have never been downloaded 71 | adu --max-depth=0 * --without-downloads | grep G 72 | 73 | # Show artifacts older than 30 days 74 | adu --max-depth=0 * --older-than 30 | grep G 75 | 76 | ``` 77 | 78 | ## Artifactory options 79 | ### Connection 80 | - `--artifactory-url http://arti.example.com/artifactory` -URL to artifactory, e.g: https://arti.example.com/artifactory" 81 | - `--username USERNAME` - user which has READ access to repository 82 | - `--password PASSWORD`, - user's password which has READ access to repository 83 | - `--repository REPOSITORY` - Specify repository 84 | - `--verbose` - increase output verbosity 85 | 86 | ### Specific 87 | - `--without-downloads` - Find items that have never been downloaded (`stat.downloads == 0`) 88 | - `--older-than DAY_COUNT` - only counts size for files older than `DAY_COUNT` 89 | 90 | ## DU options 91 | - `--max-depth N` - print the total size for a directory (or file, with --all) only if it is N or fewer levels below the command line argument; `--max-depth=0` is the same as `--summarize` 92 | - `--human-readable, -h` - print sizes in human readable format (e.g., 1K 234M 2G) 93 | - `--all` - write counts for all files, not just directories 94 | - `--summarize` - display only a total for each argument 95 | 96 | # Known issues 97 | 1. Does not support filename in ``: `artifactory-du -h -s */*.deb` will fail 98 | 2. Does not print folder if `summarize` folder: `artifactory-du -h -s foldername` will out: `123G /` , expected as original `du`: `123G foldername` 99 | 100 | # CONTRIBUTING 101 | How to contribute to the project: 102 | - Create your own github-fork 103 | - Change files 104 | - Create a pull request to the `develop`-branch 105 | 106 | How to create a release: 107 | - Dump the version on `develop`-branch in [artifactory_du/version.py](artifactory_du/version.py) 108 | - Create a pull request `develop=>master` 109 | - Merge it and wait till all travis-ci jobs are passed and we have the new version in pypi https://pypi.org/project/artifactory-du/#history 110 | - Build and push docker image: `make docker-release` 111 | 112 | # Advertising 113 | - [artifactory-cleanup](https://github.com/devopshq/artifactory-cleanup) - is an extended and flexible cleanup tool for JFrog Artifactory. 114 | 115 | 116 | --------------- 117 | Inspired by https://github.com/reversefold/artifactory-disk-usage 118 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /artifactory_du/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devopshq/artifactory-du/d89ff5502dfed120f92690c88ddd425399a99170/artifactory_du/__init__.py -------------------------------------------------------------------------------- /artifactory_du/__main__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devopshq/artifactory-du/d89ff5502dfed120f92690c88ddd425399a99170/artifactory_du/__main__.py -------------------------------------------------------------------------------- /artifactory_du/artifactory_du.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | from datetime import date, timedelta 5 | 6 | import requests 7 | from artifactory import ArtifactoryPath 8 | from hurry.filesize import size 9 | from requests_kerberos import HTTPKerberosAuth, DISABLED 10 | 11 | from artifactory_du.du import out_as_du 12 | from artifactory_du.version import __version__ 13 | 14 | requests.packages.urllib3.disable_warnings() 15 | 16 | 17 | def init_logging(): 18 | """ 19 | Init logging 20 | :return: 21 | """ 22 | logger_format_string = "%(levelname)-8s %(message)s" 23 | logging.basicConfig(level=logging.DEBUG, format=logger_format_string, stream=sys.stdout) 24 | 25 | 26 | def artifactory_aql( 27 | artifactory_url="", 28 | access_token="", 29 | username="", 30 | password="", 31 | aql_query_dict=None, 32 | kerberos=False, 33 | verify=False, 34 | artifactory_path=None, 35 | ): 36 | """ 37 | Send AQL to Artifactory and get list of Artifacts 38 | :param artifactory_path: (ArtifactoryPath) if provided then use this object as URL, username and password 39 | :param artifactory_url: URL path to artifactory (not the repo), not applied with artifactory_path 40 | :param access_token: access token for authentication, not applied with artifactory_path 41 | :param username: username for authentication, not applied with artifactory_path 42 | :param password: password for authentication, not applied with artifactory_path 43 | :param kerberos: Boolean if kerberos authentication should be used 44 | :param verify: Boolean if SSL certificate should be checked 45 | :param aql_query_dict: (dict) that you get from prepare_aql function 46 | :return: 47 | """ 48 | if artifactory_path: 49 | aql = artifactory_path 50 | elif access_token: 51 | aql = ArtifactoryPath(artifactory_url, token=access_token, verify=verify) 52 | else: 53 | if kerberos: 54 | auth = HTTPKerberosAuth(mutual_authentication=DISABLED, sanitize_mutual_error_response=False) 55 | else: 56 | if not password: 57 | raise ValueError("argument 'password' needs to be set for basic authentication") 58 | auth = (username, password) 59 | aql = ArtifactoryPath(artifactory_url, auth=auth, verify=verify) 60 | 61 | logging.debug("AQL query: items.find({})".format(aql_query_dict)) 62 | artifacts = aql.aql("items.find", aql_query_dict) 63 | logging.debug("Artifacts count: {}".format(len(artifacts))) 64 | artifacts_size = sum([x["size"] for x in artifacts]) 65 | logging.debug("Summary size: {}".format(size(artifacts_size))) 66 | 67 | return artifacts 68 | 69 | 70 | def prepare_aql(file, max_depth, repository, without_downloads, older_than): 71 | """ 72 | Prepare AQL and calculate new max_depth for du 73 | :param older_than: 74 | :param file: pattern for file 75 | :param max_depth: 76 | :param repository: 77 | :return: tuple - aql.dict and new max_depth 78 | """ 79 | aql_query_dict = {"repo": repository} 80 | if file: 81 | # Remove first / in path: /path => path 82 | file = file[1:] if file.startswith("/") else file 83 | if "*" in file: 84 | max_depth += file.count("*") 85 | max_depth += file.count("/") 86 | aql_query_dict["path"] = {"$match": file} 87 | # replace null string to * 88 | else: 89 | aql_query_dict["path"] = {"$match": file + "/*"} 90 | 91 | if without_downloads: 92 | aql_query_dict["stat.downloads"] = {"$eq": None} 93 | logging.debug("Counts for artifacts without downloads") 94 | 95 | if older_than: 96 | today = date.today() 97 | older_than_date = today - timedelta(older_than) 98 | older_than_date_txt = older_than_date.isoformat() 99 | aql_query_dict["created"] = {"$lt": older_than_date_txt} 100 | logging.debug("Counts for artifacts older than {}".format(older_than_date_txt)) 101 | 102 | return aql_query_dict, max_depth 103 | 104 | 105 | APP_DESCRIPTION = "Summarize disk usage in JFrog Artifactory of the set of FILEs, recursively for directories." 106 | 107 | 108 | def parse_args(): 109 | parser = argparse.ArgumentParser(add_help=False, description=APP_DESCRIPTION) 110 | 111 | # replace argparse.help, because du use -h flag for --human-readable 112 | parser.add_argument("--help", action="help", default=argparse.SUPPRESS, help="show this help message and exit.") 113 | parser.add_argument("--version", action="version", version="%(prog)s {}".format(__version__)) 114 | 115 | # Artifactory CONNECTION arguments 116 | parser.add_argument( 117 | "--artifactory-url", 118 | action="store", 119 | required=True, 120 | help="URL to artifactory, e.g: https://arti.example.com/artifactory", 121 | ) 122 | parser.add_argument( 123 | "--repository", 124 | action="store", 125 | required=True, 126 | help="Specify repository", 127 | ) 128 | parser.add_argument("--ignorecert", help="ignore SSL certificate of the artifactory server", action="store_true") 129 | parser.add_argument("--verbose", "-v", "--debug", help="increase output verbosity", action="store_true") 130 | 131 | # Authentication arguments 132 | auth_args = parser.add_mutually_exclusive_group(required=True) 133 | auth_args.add_argument( 134 | "--username", 135 | action="store", 136 | help="user how have READ access to repository (password needed in this case)", 137 | ) 138 | auth_args.add_argument( 139 | "--kerberos", 140 | "-k", 141 | action="store_true", 142 | help="use kerberos authentication (no password needed here)", 143 | ) 144 | auth_args.add_argument( 145 | "--access-token", 146 | action="store", 147 | help="access token how have READ access to repository (no password needed here)" 148 | ) 149 | 150 | parser.add_argument( 151 | "--password", 152 | action="store", 153 | help="users password how have READ access to repository", 154 | ) 155 | 156 | # Artifactory SPECIFIC arguments 157 | parser.add_argument("--without-downloads", action="store_true", help="Find items that have never been downloaded") 158 | parser.add_argument( 159 | "--older-than", action="store", default=0, type=int, help="only counts size for files older than" 160 | ) 161 | 162 | # du arguments 163 | parser.add_argument( 164 | "--human-readable", 165 | "-h", 166 | action="store_true", 167 | required=False, 168 | help="print sizes in human readable format (e.g., 1K 234M 2G)", 169 | ) 170 | parser.add_argument( 171 | "--max-depth", 172 | action="store", 173 | required=False, 174 | default=100, 175 | type=int, 176 | help="print the total size for a directory (or file, with --all) only if it is N or fewer levels" 177 | "below the command line argument; --max-depth=0 is the same as --summarize", 178 | ) 179 | parser.add_argument("--all", action="store_true", help="write counts for all files, not just directories") 180 | parser.add_argument("--summarize", "-s", action="store_true", help="display only a total for each argument") 181 | 182 | parser.add_argument( 183 | "file", 184 | type=str, 185 | ) 186 | 187 | args_ = parser.parse_args() 188 | return args_ 189 | 190 | 191 | def main(): 192 | args = parse_args() 193 | if args.all and args.summarize: 194 | print("artifactory-du: cannot both summarize and show all entries", file=sys.stderr) 195 | 196 | if args.summarize: 197 | if args.max_depth != 100: 198 | print("artifactory-du: warning: summarizing is the same as using --max-depth=0", file=sys.stderr) 199 | args.max_depth = 0 200 | 201 | if args.verbose: 202 | init_logging() 203 | 204 | aql_query_dict, max_depth_print = prepare_aql( 205 | file=args.file, 206 | max_depth=args.max_depth, 207 | repository=args.repository, 208 | without_downloads=args.without_downloads, 209 | older_than=args.older_than, 210 | ) 211 | artifacts = artifactory_aql( 212 | artifactory_url=args.artifactory_url, 213 | access_token=args.access_token, 214 | username=args.username, 215 | password=args.password, 216 | aql_query_dict=aql_query_dict, 217 | kerberos=args.kerberos, 218 | verify=not args.ignorecert, 219 | ) 220 | print_str = out_as_du(artifacts, max_depth_print, args.human_readable, args.all) 221 | print(print_str) 222 | 223 | 224 | if __name__ == "__main__": 225 | main() 226 | -------------------------------------------------------------------------------- /artifactory_du/du.py: -------------------------------------------------------------------------------- 1 | from itertools import groupby 2 | 3 | from hurry.filesize import size 4 | 5 | 6 | def out_as_du(artifacts, max_depth, human_readable=False, all=False): 7 | """ 8 | Return result as original linux cli utility du 9 | Method have some magic, I did not write comment and forget how it works :( 10 | But we have test, so you can refactor this :) 11 | :param artifacts: List of dict with two key (mandatory): path, name, size (in byte) 12 | :param max_depth: 13 | :param human_readable: 14 | :param all: 15 | :return: 16 | """ 17 | # Add full path with path and name 18 | for artifact in artifacts: 19 | artifact["fullpath"] = artifact["path"] + "/" + artifact["name"] 20 | path_key = "fullpath" if all else "path" 21 | 22 | artifacts = sorted(artifacts, key=lambda x: x[path_key]) 23 | depth = lambda path_, max_depth_: tuple(path_.split("/", maxsplit=max_depth_)[:max_depth_]) 24 | artifacts_group = groupby(artifacts, key=lambda x: depth(x[path_key], max_depth)) 25 | 26 | result = [] 27 | for group, artifacts_ in artifacts_group: 28 | artifacts_size = sum([x["size"] for x in artifacts_]) 29 | if human_readable: 30 | artifacts_size = size(artifacts_size) 31 | artifacts_size = str(artifacts_size) 32 | result.append("{} {}".format(artifacts_size.ljust(7), "/".join(group if group else "/"))) 33 | return "\n".join(result) 34 | -------------------------------------------------------------------------------- /artifactory_du/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | setuptools>=46.1 ; python_version >= '3.6' 3 | setuptools-rust ; python_version >= '3.6' 4 | requests_kerberos 5 | dohq-artifactory 6 | hurry.filesize 7 | -------------------------------------------------------------------------------- /artifactory_du/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.5" 2 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from artifactory_du.artifactory_du import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | See: 3 | https://packaging.python.org/en/latest/distributing.html 4 | https://github.com/pypa/sampleproject 5 | """ 6 | 7 | import os 8 | import re 9 | 10 | # To use a consistent encoding 11 | from codecs import open 12 | from os import path 13 | 14 | # Always prefer setuptools over distutils 15 | from setuptools import setup, find_packages 16 | 17 | here = path.abspath(path.dirname(__file__)) 18 | 19 | 20 | def get_requires(filename): 21 | requirements = [] 22 | with open(filename, "rt") as req_file: 23 | for line in req_file.read().splitlines(): 24 | if not line.strip().startswith("#"): 25 | requirements.append(line) 26 | return requirements 27 | 28 | 29 | project_requirements = get_requires("artifactory_du/requirements.txt") 30 | 31 | 32 | def load_version(): 33 | """ 34 | Loads a file content''' 35 | :return: 36 | """ 37 | filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "artifactory_du", "version.py")) 38 | with open(filename, "rt") as version_file: 39 | artifactory_du_init = version_file.read() 40 | version = re.search("__version__ = \"([0-9a-z.-]+)\"", artifactory_du_init).group(1) 41 | return version 42 | 43 | 44 | # def generate_long_description_file(): 45 | # import pypandoc 46 | # 47 | # output = pypandoc.convert('README.md', 'rst') 48 | # return output 49 | 50 | LONG_DESCRIPTION = """artifactory-du is used in the same manner as original du from *nix, although launch options are different. 51 | 52 | See artifactory-du –help for details. 53 | 54 | `https://devopshq.github.io/artifactory-du/ `_ 55 | """ 56 | 57 | setup( 58 | name="artifactory-du", 59 | # Versions should comply with PEP440. For a discussion on single-sourcing 60 | # the version across setup.py and the project code, see 61 | # https://packaging.python.org/en/latest/single_source_version.html 62 | version=load_version(), # + ".rc1", 63 | description="Artifactory Disk Usage command line interface (artifactory-du)", 64 | long_description=LONG_DESCRIPTION, 65 | # long_description=generate_long_description_file(), 66 | # The project's main homepage. 67 | url="https://devopshq.github.io/artifactory-du", 68 | # Author details 69 | author="DevOpsHQ", 70 | author_email="allburov@gmail.com", 71 | # Choose your license 72 | license="MIT", 73 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 74 | classifiers=[ 75 | "Development Status :: 5 - Production/Stable", 76 | "Intended Audience :: Developers", 77 | "Topic :: Software Development :: Build Tools", 78 | "License :: OSI Approved :: MIT License", 79 | "Programming Language :: Python :: 3", 80 | "Programming Language :: Python :: 3.4", 81 | "Programming Language :: Python :: 3.5", 82 | "Programming Language :: Python :: 3.6", 83 | ], 84 | # What does your project relate to? 85 | keywords=[ 86 | "disk", 87 | "usage", 88 | "artifactory", 89 | "jfrog", 90 | "devopshq", 91 | ], 92 | # You can just specify the packages manually here if your project is 93 | # simple. Or you can use find_packages(). 94 | packages=find_packages(), 95 | # Alternatively, if you want to distribute just a my_module.py, uncomment 96 | # this: 97 | # py_modules=["my_module"], 98 | # List run-time dependencies here. These will be installed by pip when 99 | # your project is installed. For an analysis of "install_requires" vs pip's 100 | # requirements files see: 101 | # https://packaging.python.org/en/latest/requirements.html 102 | install_requires=project_requirements, 103 | # List additional groups of dependencies here (e.g. development 104 | # dependencies). You can install these using the following syntax, 105 | # for example: 106 | # $ pip install -e .[dev,test] 107 | # extras_require={ 108 | # 'dev': dev_requirements, 109 | # }, 110 | setup_requires=[ 111 | "pytest-runner", 112 | "pytest", 113 | ], 114 | # If there are data files included in your packages that need to be 115 | # installed, specify them here. If using Python 2.6 or less, then these 116 | # have to be included in MANIFEST.in as well. 117 | package_data={ 118 | "artifactory_du": ["*.txt"], 119 | }, 120 | # Although 'package_data' is the preferred approach, in some case you may 121 | # need to place data files outside of your packages. See: 122 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 123 | # In this case, 'data_file' will be installed into '/my_data' 124 | # data_files=[('my_data', ['data/data_file'])], 125 | # To provide executable scripts, use entry points in preference to the 126 | # "scripts" keyword. Entry points provide cross-platform support and allow 127 | # pip to create the appropriate form of executable for the target platform. 128 | entry_points={ 129 | "console_scripts": [ 130 | "artifactory_du=artifactory_du.artifactory_du:main", 131 | "artifactory-du=artifactory_du.artifactory_du:main", 132 | ], 133 | }, 134 | ) 135 | -------------------------------------------------------------------------------- /tests/test_du.py: -------------------------------------------------------------------------------- 1 | from artifactory_du.du import out_as_du 2 | 3 | 4 | def test_out_as_du_simple_human(): 5 | artifacts = [ 6 | { 7 | "name": "filename1.txt", 8 | "path": "level1", 9 | "size": 1024, 10 | }, 11 | ] 12 | expected_result = """1K level1""" 13 | result = out_as_du(artifacts, max_depth=1, human_readable=True, all=False) 14 | assert expected_result == result 15 | 16 | 17 | def test_out_as_du_simple_byte(): 18 | artifacts = [ 19 | { 20 | "name": "filename1.txt", 21 | "path": "level1", 22 | "size": 1024, 23 | }, 24 | ] 25 | expected_result = """1024 level1""" 26 | result = out_as_du(artifacts, max_depth=1, human_readable=False, all=False) 27 | assert expected_result == result 28 | 29 | 30 | ARTIFACTS = [ 31 | { 32 | "name": "filename11.txt", 33 | "path": "1/2", 34 | "size": 1024, 35 | }, 36 | { 37 | "name": "filename12.txt", 38 | "path": "1/2", 39 | "size": 1024, 40 | }, 41 | { 42 | "name": "filename21.txt", 43 | "path": "2/2", 44 | "size": 1024, 45 | }, 46 | { 47 | "name": "filename22.txt", 48 | "path": "2/2", 49 | "size": 1024, 50 | }, 51 | ] 52 | 53 | 54 | def test_out_as_du_max_depth_1(): 55 | expected_result = """2K 1 56 | 2K 2""" 57 | result = out_as_du(ARTIFACTS, max_depth=1, human_readable=True, all=False) 58 | assert expected_result == result 59 | 60 | 61 | def test_out_as_du_max_depth_2(): 62 | expected_result = """2K 1/2 63 | 2K 2/2""" 64 | result = out_as_du(ARTIFACTS, max_depth=2, human_readable=True, all=False) 65 | assert expected_result == result 66 | 67 | 68 | def test_out_as_du_max_depth_all(): 69 | expected_result = """1K 1/2/filename11.txt 70 | 1K 1/2/filename12.txt 71 | 1K 2/2/filename21.txt 72 | 1K 2/2/filename22.txt""" 73 | result = out_as_du(ARTIFACTS, max_depth=100, human_readable=True, all=True) 74 | assert expected_result == result 75 | 76 | 77 | def test_out_as_du_max_depth_0(): 78 | expected_result = """4K /""" 79 | result = out_as_du(ARTIFACTS, max_depth=0, human_readable=True, all=True) 80 | assert expected_result == result 81 | --------------------------------------------------------------------------------