diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 123c3e91..cb83fa36 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -26,12 +26,11 @@ jobs: - name: Install dependencies run: | + python --version python -m pip install --upgrade pip - pip install -e .[install] pip install -e .[build] pip install -e .[test] - python setup.py build - python res/versioning.py + doit version python setup.py build # https://github.com/marketplace/actions/pytest-coverage-comment @@ -42,8 +41,9 @@ jobs: uses: MishaKav/pytest-coverage-comment@main with: pytest-coverage-path: ./pytest-coverage.txt - # broken? Error: The head commit for this pull_request event is not ahead of the base commit. - # Please submit an issue on this action's GitHub repo + # TODO: check if this has been fixed + # Error: The head commit for this pull_request event is not ahead of the base commit. + # Please submit an issue on this action's GitHub repo # report-only-changed-files: true # TODO update badge on readme: diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index 582c9e0d..12513ace 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -25,6 +25,9 @@ jobs: doit version python setup.py build + - name: Generate dependencies list + run: python bin/license-checker.py + - name: Type-check run: mypy src tests diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 10fd8808..f0a36b53 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -8,7 +8,6 @@ on: required: true type: boolean push: - branches: main tags: 'pypi' jobs: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5239ffc0..37266837 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -2,9 +2,8 @@ name: Python tests on: workflow_dispatch: - push: - branches: - - development + schedule: + - cron: 0 11 * * * # Every day at 11AM UTC (7AM EST) pull_request: branches: - development diff --git a/.gitignore b/.gitignore index aa66e7fb..97a5509d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,9 @@ __pycache__/ *.pytest_cache *.pyc *.pkl -.coverage +venv/ +tabcmd-dev/ +workon/ # code coverage outputs .coverage diff --git a/bin/black.sh b/bin/black.sh deleted file mode 100644 index 0d34ed7b..00000000 --- a/bin/black.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -x -black --line-length 120 tabcmd tests diff --git a/bin/e2e.sh b/bin/e2e.sh deleted file mode 100644 index 8b0142ea..00000000 --- a/bin/e2e.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# not tested as a script -pytest -q tests\e2e\online_tests.py -r pfE diff --git a/bin/license-checker.py b/bin/license-checker.py new file mode 100644 index 00000000..b9bc00df --- /dev/null +++ b/bin/license-checker.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# https://git.soma.salesforce.com/python-at-sfdc/license_checker +# Modified version of https://stackoverflow.com/a/44090218 +from __future__ import print_function +from collections import defaultdict +import pkg_resources + +def get_pkg_license(pkg): + try: + lines = pkg.get_metadata_lines('METADATA') + except: + lines = pkg.get_metadata_lines('PKG-INFO') + + for line in lines: + if line.startswith('License:'): + return line[9:] + return '(Licence not found)' + exit() + +def print_table(table): + column_index_to_max_width = defaultdict(int) + for row_index, row in enumerate(table): + for cell_index, cell in enumerate(row): + cur_max = column_index_to_max_width[cell_index] + cell_width = len(cell) + if cell_width > cur_max: + column_index_to_max_width[cell_index] = cell_width + for row_index, row in enumerate(table): + line = '' + for cell_index, cell in enumerate(row): + cell_width = column_index_to_max_width[cell_index] + line += cell.ljust(cell_width) + line += " - " + line = line.ljust(25) + line = line.rstrip(" -") + print(line) + if row_index == 0: + print("-" * len(line)) + + +def get_directory(package_name): + return pkg_resources.working_set.find(pkg_resources.Requirement.parse(package_name)).location + + +def print_packages_and_licenses(): + table = [] + table.append(['Package', 'License', 'Location']) + for pkg in sorted(pkg_resources.working_set): + table.append([str(pkg).rjust(25)[:25], get_pkg_license(pkg)[:25], pkg.location]) + print_table(table) + + +if __name__ == "__main__": + print_packages_and_licenses() diff --git a/contributing.md b/contributing.md index ae8dd0ce..c7c77036 100644 --- a/contributing.md +++ b/contributing.md @@ -30,9 +30,9 @@ _(note that running mypy and black is required for code being submitted to the r > pytest - run tests against a live server > python -m tabcmd login {your server info here} -> bin/e2e.sh +> pytest -q tests\e2e\online_tests.py -r pfE - autoformat your code with black (https://pypi.org/project/black/) -> bin/black.sh +> black . - check types > mypy src tests - do test coverage calculation (https://coverage.readthedocs.io/en/6.3.2) diff --git a/dodo.py b/dodo.py index ef2b8b42..3e4272a5 100644 --- a/dodo.py +++ b/dodo.py @@ -4,10 +4,7 @@ import ftfy import setuptools_scm -LOCALES = [ - "en", "de", "es", "fr", "ga", "it", "pt", - "sv", "ja", "ko", - "zh"] +LOCALES = ["en", "de", "es", "fr", "ga", "it", "pt", "sv", "ja", "ko", "zh"] """ https://pydoit.org/ @@ -46,8 +43,8 @@ def process_locales(): outfile.write(ftfy.fixes.decode_escapes(data)) return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } @@ -65,40 +62,48 @@ def task_po(): - 3.x, from pip install translate-toolkit: it copies key->comment, value-> msgid, ""->msgstr which is not at all what we want """ + def process_locales(): for current_locale in LOCALES: LOC_PATH = "src/locales/" + current_locale - for file in glob.glob(LOC_PATH+"/*.properties"): + for file in glob.glob(LOC_PATH + "/*.properties"): basename = os.path.basename(file).split(".")[0] print("processing", basename) - result = subprocess.run(["python", - "bin/i18n/prop2po.py", - "--encoding", "utf-8", # for the .po header - "--language", current_locale, # for the .po header - LOC_PATH + "/"+basename+".properties", - LOC_PATH + "/LC_MESSAGES/"+basename+".po"]) + result = subprocess.run( + [ + "python", + "bin/i18n/prop2po.py", + "--encoding", + "utf-8", # for the .po header + "--language", + current_locale, # for the .po header + LOC_PATH + "/" + basename + ".properties", + LOC_PATH + "/LC_MESSAGES/" + basename + ".po", + ] + ) print("\n", result) # print("stdout:", result.stdout) if not result.returncode == 0: print("stderr:", result.stderr) + return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } def task_clean_all(): - """For all languages: removes all generated artifacts (.po, .mo) which source from properties files. """ + """For all languages: removes all generated artifacts (.po, .mo) which source from properties files.""" def process_locales(): for current_locale in LOCALES: LOC_PATH = "src/locales/" + current_locale - for file in glob.glob(LOC_PATH+"/*.properties"): + for file in glob.glob(LOC_PATH + "/*.properties"): basename = os.path.basename(file).split(".")[0] - print("deleting",basename + ".*") + print("deleting", basename + ".*") try: - os.remove(LOC_PATH + "/LC_MESSAGES/"+basename+".po") + os.remove(LOC_PATH + "/LC_MESSAGES/" + basename + ".po") except OSError: pass try: @@ -111,10 +116,9 @@ def process_locales(): except OSError: pass - return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } @@ -128,7 +132,7 @@ def process_locales(): LOC_PATH = "src/locales/" + current_locale + "/LC_MESSAGES" - with open(LOC_PATH + "/tabcmd.po", 'w+', encoding="utf-8") as outfile: + with open(LOC_PATH + "/tabcmd.po", "w+", encoding="utf-8") as outfile: for file in glob.glob(LOC_PATH + "/*.po"): if file.endswith("tabcmd.po"): pass @@ -139,8 +143,8 @@ def process_locales(): outfile.write("\n") return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } @@ -164,14 +168,15 @@ def process_locales(): print("stderr:", result.stderr) return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } def task_version(): - """ Generates a metadata info file with current version to be bundled by pyinstaller""" + """Generates a metadata info file with current version to be bundled by pyinstaller""" + def write_for_pyinstaller(): import pyinstaller_versionfile import os @@ -183,12 +188,13 @@ def write_for_pyinstaller(): output_file = os.path.join(".", "program_metadata.txt") input_file = os.path.join("res", "metadata.yml") pyinstaller_versionfile.create_versionfile_from_input_file( - output_file, input_file, + output_file, + input_file, # optional, can be set to overwrite version information (equivalent to --version when using the CLI) - version=numeric_version + version=numeric_version, ) return { - 'actions': [write_for_pyinstaller], - 'verbosity': 2, + "actions": [write_for_pyinstaller], + "verbosity": 2, } diff --git a/pyproject.toml b/pyproject.toml index cad7b64a..485decd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,7 @@ [build-system] -requires = ["setuptools>=62", "wheel", "setuptools_scm>=6.2"] +requires = ["build", "setuptools>=62", "wheel", "setuptools_scm>=6.2"] [tool.setuptools_scm] "local_scheme"= "no-local-version" # require pypi supported versions always +[tool.black] +line-length = 120 +extend-exclude = '^/bin/*' diff --git a/setup.py b/setup.py index 9955108f..57466d7d 100644 --- a/setup.py +++ b/setup.py @@ -1,61 +1,63 @@ from setuptools import setup, find_packages setup( - name='tabcmd', - author='Tableau', - author_email='github@tableau.com', - description='A command line client for working with Tableau Server.', - long_description='A command line client for working with Tableau Server.', - license='MIT', - url='https://github.com/tableau/tabcmd', - - python_requires='>=3.7', + name="tabcmd", + author="Tableau", + author_email="github@tableau.com", + description="A command line client for working with Tableau Server.", + long_description="A command line client for working with Tableau Server.", + license="MIT", + url="https://github.com/tableau/tabcmd", + python_requires=">=3.7", packages=find_packages(), - package_data={'tabcmd': ['src.locales/**/*.mo']}, + package_data={"tabcmd": ["src.locales/**/*.mo"]}, include_package_data=True, - entry_points={ - 'console_scripts': [ - 'tabcmd = src.tabcmd:main' - ] - }, + entry_points={"console_scripts": ["tabcmd = src.tabcmd:main"]}, setup_requires=[ # copy of pyproject.toml for back compat - "setuptools>=62", "wheel", "setuptools_scm>=6.2" + "build", + "setuptools>=62", + "setuptools_scm>=6.2", + "wheel", ], install_requires=[ - 'polling2', - 'requests>=2.11,<3.0', - 'tableauserverclient>=0.19', - 'urllib3>=1.24.3,<2.0', + "polling2", + "requests>=2.11,<3.0", + "tableauserverclient>=0.19", + "urllib3>=1.24.3,<2.0", ], extras_require={ - 'localize': [ - 'doit', - 'ftfy', + "localize": [ + "doit", + "ftfy", ], - 'build': [ - 'appdirs', - 'black', - 'doit', - 'ftfy', - 'mypy', - 'pyinstaller_versionfile', - 'setuptools>=62', - 'setuptools_scm', - 'types-appdirs', - 'types-mock', - 'types-requests', + "build": [ + "appdirs", + "black", + "doit", + "ftfy", + "mypy", + "pyinstaller_versionfile", + "setuptools>=62", + "setuptools_scm", + "types-appdirs", + "types-mock", + "types-requests", + "types-setuptools", ], - 'package': [ - 'pyinstaller>=5.1', - 'pyinstaller-versionfile', + "package": [ + "pyinstaller>=5.1", + "pyinstaller-versionfile", ], - 'test': [ - 'mock', - 'pytest', 'pytest-cov', 'pytest-order', 'pytest-runner', - 'requests-mock>=1.0,<2.0', + "test": [ + "mock", + "pytest", + "pytest-cov", + "pytest-order", + "pytest-runner", + "requests-mock>=1.0,<2.0", ], }, - test_suite='tests', + test_suite="tests", zip_safe=False, ) diff --git a/src/commands/datasources_and_workbooks/get_url_command.py b/src/commands/datasources_and_workbooks/get_url_command.py index 924c1a2d..52989ad5 100644 --- a/src/commands/datasources_and_workbooks/get_url_command.py +++ b/src/commands/datasources_and_workbooks/get_url_command.py @@ -144,7 +144,7 @@ def generate_pdf(logger, server, args): f.write(view_item.pdf) logger.info(_("export.success").format(view_item.name, filename)) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @staticmethod def generate_png(logger, server, args): @@ -159,7 +159,7 @@ def generate_png(logger, server, args): f.write(view_item.png) logger.info(_("export.success").format(view_item.name, filename)) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @staticmethod def generate_csv(logger, server, args): @@ -174,7 +174,7 @@ def generate_csv(logger, server, args): f.write(view_item.csv) logger.info(_("export.success"), views_from_list.name, formatted_file_name) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) except Exception as e: Errors.exit_with_error(logger, exception=e) @@ -188,4 +188,4 @@ def generate_twb(logger, server, args, file_extension): server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_path)) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) diff --git a/src/execution/_version.py b/src/execution/_version.py index fcf69b5e..1d50c3ef 100644 --- a/src/execution/_version.py +++ b/src/execution/_version.py @@ -1,5 +1,5 @@ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -version = '38' +version = "38" version_tuple = (38,) diff --git a/src/execution/parent_parser.py b/src/execution/parent_parser.py index b04b6168..d7018031 100644 --- a/src/execution/parent_parser.py +++ b/src/execution/parent_parser.py @@ -1,8 +1,10 @@ import argparse from .localize import _ + # when we drop python 3.8, this could be replaced with this lighter weight option # from importlib.metadata import version, PackageNotFoundError from pkg_resources import get_distribution, DistributionNotFound + try: version = get_distribution("tabcmd").version except DistributionNotFound: diff --git a/tabcmd.py b/tabcmd.py index 981ca265..1c987d62 100644 --- a/tabcmd.py +++ b/tabcmd.py @@ -1,4 +1,4 @@ from src import tabcmd -if __name__ == '__main__': +if __name__ == "__main__": tabcmd.main()