From 0668af2afa45eebe5b0e847a4b9dc2445077deb0 Mon Sep 17 00:00:00 2001 From: allburov Date: Wed, 18 May 2022 17:14:02 +0700 Subject: [PATCH 1/6] Add pre-commit - black, format imports --- .pre-commit-config.yaml | 20 ++ docs/conf.py | 68 +++--- setup.py | 102 ++++---- tests/conftest.py | 21 +- tests/test_connection.py | 193 +++++++++------ tests/test_resources.py | 53 +++-- tfs/connection.py | 497 +++++++++++++++++++++++++-------------- tfs/resources.py | 235 +++++++++++------- 8 files changed, 725 insertions(+), 464 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9865e82 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/humitos/mirrors-autoflake + rev: v1.1 + hooks: + - id: autoflake + args: ['-i', '--remove-all-unused-imports'] + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + language_version: python3 + - repo: https://github.com/asottile/blacken-docs + rev: v1.12.1 + hooks: + - id: blacken-docs + additional_dependencies: [black] + - repo: https://github.com/asottile/reorder_python_imports + rev: v3.0.1 + hooks: + - id: reorder-python-imports diff --git a/docs/conf.py b/docs/conf.py index 1a15e34..bac6952 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,9 +5,7 @@ # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config - # -- Path setup -------------------------------------------------------------- - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -15,21 +13,21 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- -project = 'TFS' -copyright = '2018, DevOpsHQ (devopshq)' -author = 'Alexey Burov (allburov)' +project = "TFS" +copyright = "2018, DevOpsHQ (devopshq)" +author = "Alexey Burov (allburov)" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" -highlight_language = 'python3' +highlight_language = "python3" # -- General configuration --------------------------------------------------- @@ -41,24 +39,21 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.intersphinx"] -autodoc_default_flags = ['members', 'undoc-members', 'show-inheritance'] +autodoc_default_flags = ["members", "undoc-members", "show-inheritance"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -70,10 +65,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- @@ -81,7 +76,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -92,7 +87,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -102,15 +97,13 @@ # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # -html_sidebars = { - "**": ["about.html", "navigation.html", "searchbox.html"] -} +html_sidebars = {"**": ["about.html", "navigation.html", "searchbox.html"]} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'TFSdoc' +htmlhelp_basename = "TFSdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -119,15 +112,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -137,8 +127,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'TFS.tex', 'TFS Documentation', - 'Alexey Burov (allburov)', 'manual'), + (master_doc, "TFS.tex", "TFS Documentation", "Alexey Burov (allburov)", "manual"), ] @@ -146,10 +135,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'tfs', 'TFS Documentation', - [author], 1) -] +man_pages = [(master_doc, "tfs", "TFS Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -158,7 +144,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'TFS', 'TFS Documentation', - author, 'TFS', 'One line description of project.', - 'Miscellaneous'), -] \ No newline at end of file + ( + master_doc, + "TFS", + "TFS Documentation", + author, + "TFS", + "One line description of project.", + "Miscellaneous", + ), +] diff --git a/setup.py b/setup.py index 2fcc9de..8e41561 100644 --- a/setup.py +++ b/setup.py @@ -1,89 +1,73 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - - import os from setuptools import setup -__version__ = '1.0' # identify main version of dohq-tfs tool -devStatus = '4 - Beta' # default build status, see: https://pypi.python.org/pypi?%3Aaction=list_classifiers +__version__ = "1.0" # identify main version of dohq-tfs tool +devStatus = "4 - Beta" # default build status, see: https://pypi.python.org/pypi?%3Aaction=list_classifiers -if 'TRAVIS_BUILD_NUMBER' in os.environ and 'TRAVIS_BRANCH' in os.environ: +if "TRAVIS_BUILD_NUMBER" in os.environ and "TRAVIS_BRANCH" in os.environ: print("This is TRAVIS-CI build") - print("TRAVIS_BUILD_NUMBER = {}".format(os.environ['TRAVIS_BUILD_NUMBER'])) - print("TRAVIS_BRANCH = {}".format(os.environ['TRAVIS_BRANCH'])) - - __version__ += '.{}{}'.format( - '' if 'release' in os.environ['TRAVIS_BRANCH'] or os.environ['TRAVIS_BRANCH'] == 'master' else 'dev', - os.environ['TRAVIS_BUILD_NUMBER'], + print("TRAVIS_BUILD_NUMBER = {}".format(os.environ["TRAVIS_BUILD_NUMBER"])) + print("TRAVIS_BRANCH = {}".format(os.environ["TRAVIS_BRANCH"])) + + __version__ += ".{}{}".format( + "" + if "release" in os.environ["TRAVIS_BRANCH"] + or os.environ["TRAVIS_BRANCH"] == "master" + else "dev", + os.environ["TRAVIS_BUILD_NUMBER"], ) - devStatus = '5 - Production/Stable' if 'release' in os.environ['TRAVIS_BRANCH'] or os.environ[ - 'TRAVIS_BRANCH'] == 'master' else devStatus + devStatus = ( + "5 - Production/Stable" + if "release" in os.environ["TRAVIS_BRANCH"] + or os.environ["TRAVIS_BRANCH"] == "master" + else devStatus + ) else: print("This is local build") - __version__ += '.dev0' # set version as major.minor.localbuild if local build: python setup.py install + __version__ += ".dev0" # set version as major.minor.localbuild if local build: python setup.py install print("dohq-tfs build version = {}".format(__version__)) setup( - name='dohq-tfs', - + name="dohq-tfs", version=__version__, - - description='Microsoft TFS Python Library (TFS API Python client) that can work with TFS workflow and workitems.', - - long_description='You can see detailed user manual here: https://devopshq.github.io/tfs/', - - license='MIT', - - author='Alexey Burov', - - author_email='allburov@gmail.com', - - url='https://devopshq.github.io/tfs/', - - download_url='https://github.com/devopshq/tfs.git', - + description="Microsoft TFS Python Library (TFS API Python client) that can work with TFS workflow and workitems.", + long_description="You can see detailed user manual here: https://devopshq.github.io/tfs/", + license="MIT", + author="Alexey Burov", + author_email="allburov@gmail.com", + url="https://devopshq.github.io/tfs/", + download_url="https://github.com/devopshq/tfs.git", entry_points={}, - classifiers=[ - 'Development Status :: {}'.format(devStatus), - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3.4', + "Development Status :: {}".format(devStatus), + "Environment :: Console", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3.4", ], - packages=[ - 'tfs', - ], - - setup_requires=[ - 'pytest-runner' + "tfs", ], - + setup_requires=["pytest-runner"], tests_require=[ - 'pytest==3.1.2', - 'HTTPretty', - 'pytest_httpretty', - ], - - install_requires=[ - 'requests', - 'requests_ntlm' + "pytest==3.1.2", + "HTTPretty", + "pytest_httpretty", ], - + install_requires=["requests", "requests_ntlm"], package_data={ - '': [ - '../LICENSE', - '../README.md', + "": [ + "../LICENSE", + "../README.md", ], }, - zip_safe=True, ) diff --git a/tests/conftest.py b/tests/conftest.py index 135be1d..48cf1a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,13 +11,13 @@ def request_callback_get(request, uri, headers): # Map path from url to a file - path = urlparse(uri).path.split('DefaultCollection/')[1] - response_file = os.path.normpath('tests/resources/{}'.format(path)) - response_file = os.path.join(response_file, 'response.json') + path = urlparse(uri).path.split("DefaultCollection/")[1] + response_file = os.path.normpath("tests/resources/{}".format(path)) + response_file = os.path.join(response_file, "response.json") if os.path.exists(response_file): code = 200 - response = open(response_file, mode='r', encoding="utf-8-sig").read() + response = open(response_file, mode="r", encoding="utf-8-sig").read() else: code = 404 response = "Cannot find file {}".format(response_file) @@ -28,12 +28,17 @@ def request_callback_get(request, uri, headers): @pytest.fixture(autouse=True) def tfs_server_mock(): for method in (httpretty.GET, httpretty.POST, httpretty.PUT, httpretty.PATCH): - httpretty.register_uri(method, re.compile(r"http://.*/DefaultCollection/.*"), - body=request_callback_get, - content_type="application/json") + httpretty.register_uri( + method, + re.compile(r"http://.*/DefaultCollection/.*"), + body=request_callback_get, + content_type="application/json", + ) @pytest.fixture() def tfsapi(): - client = TFSAPI("http://tfs.tfs.ru/tfs", 'DefaultCollection/MyProject', 'username', 'password') + client = TFSAPI( + "http://tfs.tfs.ru/tfs", "DefaultCollection/MyProject", "username", "password" + ) yield client diff --git a/tests/test_connection.py b/tests/test_connection.py index 21a9f53..923dc49 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -4,49 +4,73 @@ import httpretty import pytest -from tfs import * from tests import conftest +from tfs import * class TestTFSAPI: @httpretty.activate def test_get_gitrepositories_with_pat(self): def request_callback_get_pat(request, uri, headers): - authorization = request.headers.get('Authorization') - assert authorization == "Basic OmtsNWt0bnR3M2V6dnh0aGl0YzVhZTR1YmdzZWRpMnFrcWh6cWNuZ2hnNzV0azJuNHJnZmE=" - - code, headers, response = conftest.request_callback_get(request, uri, headers) + authorization = request.headers.get("Authorization") + assert ( + authorization + == "Basic OmtsNWt0bnR3M2V6dnh0aGl0YzVhZTR1YmdzZWRpMnFrcWh6cWNuZ2hnNzV0azJuNHJnZmE=" + ) + + code, headers, response = conftest.request_callback_get( + request, uri, headers + ) return code, headers, response - httpretty.register_uri(httpretty.GET, re.compile(r"http://tfs.tfs.ru(.*)"), - body=request_callback_get_pat) + httpretty.register_uri( + httpretty.GET, + re.compile(r"http://tfs.tfs.ru(.*)"), + body=request_callback_get_pat, + ) - client = TFSAPI("http://tfs.tfs.ru/tfs", 'DefaultCollection', - pat='kl5ktntw3ezvxthitc5ae4ubgsedi2qkqhzqcnghg75tk2n4rgfa') + client = TFSAPI( + "http://tfs.tfs.ru/tfs", + "DefaultCollection", + pat="kl5ktntw3ezvxthitc5ae4ubgsedi2qkqhzqcnghg75tk2n4rgfa", + ) repos = client.get_gitrepositories() - name = repos[0].data['name'] - assert name == 'AnotherRepository' + name = repos[0].data["name"] + assert name == "AnotherRepository" @httpretty.activate def test_get_gitrepositories_with_pat_and_password(self): # if pat and password are given then it is expected that the password is ignored and the pat header is added def request_callback_get_pat(request, uri, headers): - authorization = request.headers.get('Authorization') - assert authorization == "Basic OmtsNWt0bnR3M2V6dnh0aGl0YzVhZTR1YmdzZWRpMnFrcWh6cWNuZ2hnNzV0azJuNHJnZmE=" - - code, headers, response = conftest.request_callback_get(request, uri, headers) + authorization = request.headers.get("Authorization") + assert ( + authorization + == "Basic OmtsNWt0bnR3M2V6dnh0aGl0YzVhZTR1YmdzZWRpMnFrcWh6cWNuZ2hnNzV0azJuNHJnZmE=" + ) + + code, headers, response = conftest.request_callback_get( + request, uri, headers + ) return code, headers, response - httpretty.register_uri(httpretty.GET, re.compile(r"http://tfs.tfs.ru(.*)"), - body=request_callback_get_pat) + httpretty.register_uri( + httpretty.GET, + re.compile(r"http://tfs.tfs.ru(.*)"), + body=request_callback_get_pat, + ) - client = TFSAPI("http://tfs.tfs.ru/tfs", 'DefaultCollection', user='any_user', password='any_password', - pat='kl5ktntw3ezvxthitc5ae4ubgsedi2qkqhzqcnghg75tk2n4rgfa') + client = TFSAPI( + "http://tfs.tfs.ru/tfs", + "DefaultCollection", + user="any_user", + password="any_password", + pat="kl5ktntw3ezvxthitc5ae4ubgsedi2qkqhzqcnghg75tk2n4rgfa", + ) repos = client.get_gitrepositories() - name = repos[0].data['name'] - assert name == 'AnotherRepository' + name = repos[0].data["name"] + assert name == "AnotherRepository" @pytest.mark.httpretty def test_get_workitems(self, tfsapi): @@ -73,8 +97,9 @@ def test_get_workitems_with_int(self, tfsapi): @pytest.mark.httpretty def test_create_workitem(self, tfsapi): - workitem = tfsapi.create_workitem('Task', - {'System.Title': 'JavaScript implementation for Microsoft Account'}) + workitem = tfsapi.create_workitem( + "Task", {"System.Title": "JavaScript implementation for Microsoft Account"} + ) assert isinstance(workitem, Workitem) assert workitem.id == 298 @@ -98,7 +123,7 @@ def test_get_changesets_only_from(self, tfsapi): @pytest.mark.httpretty def test_get_changesets_only_to(self, tfsapi): changesets = tfsapi.get_changesets(to_=11) - + # for now httpretty get full json and ignore any filter. # TODO: fix this - now len(changesets) == 5 # assert len(changesets) == 2 @@ -112,12 +137,12 @@ def test_get_changeset(self, tfsapi): @pytest.mark.httpretty def test_run_query(self, tfsapi): - query = tfsapi.run_query('My Queries/AssignedToMe') + query = tfsapi.run_query("My Queries/AssignedToMe") assert isinstance(query, TFSQuery) assert isinstance(query.result, Wiql) - assert query.name == 'AssignedToMe' - assert query.path == 'My Queries/AssignedToMe' + assert query.name == "AssignedToMe" + assert query.path == "My Queries/AssignedToMe" @pytest.mark.httpretty def test_get_wiql(self, tfsapi): @@ -126,33 +151,33 @@ def test_get_wiql(self, tfsapi): assert isinstance(wiql, Wiql) assert wiql.workitem_ids == [100, 101] - assert httpretty.last_request().headers['Content-Type'] == 'application/json' + assert httpretty.last_request().headers["Content-Type"] == "application/json" @pytest.mark.httpretty def test_get_projects(self, tfsapi): projects = tfsapi.get_projects() assert len(projects) == 1 - assert projects[0]['name'] == 'ProjectName' + assert projects[0]["name"] == "ProjectName" @pytest.mark.httpretty def test_projects(self, tfsapi): projects = tfsapi.projects assert len(projects) == 1 - assert projects[0]['name'] == 'ProjectName' + assert projects[0]["name"] == "ProjectName" @pytest.mark.httpretty def test_get_project(self, tfsapi): - project = tfsapi.get_project('ProjectName') + project = tfsapi.get_project("ProjectName") - assert project.name == 'ProjectName' + assert project.name == "ProjectName" @pytest.mark.httpretty def test_project(self, tfsapi): - project = tfsapi.project('ProjectName') + project = tfsapi.project("ProjectName") - assert project.name == 'ProjectName' + assert project.name == "ProjectName" @pytest.mark.httpretty def test_get_teams(self, tfsapi): @@ -160,21 +185,21 @@ def test_get_teams(self, tfsapi): teams = projects[0].teams assert isinstance(teams[0], Team) - assert teams[1]['name'] == 'TeamPink' + assert teams[1]["name"] == "TeamPink" @pytest.mark.httpretty def test_get_gitrepositories(self, tfsapi): repos = tfsapi.get_gitrepositories() - name = repos[0].data['name'] + name = repos[0].data["name"] - assert name == 'AnotherRepository' + assert name == "AnotherRepository" @pytest.mark.httpretty def test_get_gitrepository(self, tfsapi): - repo = tfsapi.get_gitrepository('AnotherRepository') - name = repo.data['name'] + repo = tfsapi.get_gitrepository("AnotherRepository") + name = repo.data["name"] - assert name == 'AnotherRepository' + assert name == "AnotherRepository" @pytest.mark.httpretty def test_get_runs(self, tfsapi): @@ -182,7 +207,7 @@ def test_get_runs(self, tfsapi): assert len(runs) == 4 assert isinstance(runs[0], Run) - assert httpretty.last_request().querystring['$top'] == ['39'] + assert httpretty.last_request().querystring["$top"] == ["39"] assert runs @pytest.mark.httpretty @@ -191,32 +216,37 @@ def test_get_run(self, tfsapi): results = run.results result = run.result(100000) - assert run.name == 'sprint1 (Manual)' + assert run.name == "sprint1 (Manual)" assert len(results) == 3 - assert result.outcome == 'Passed' + assert result.outcome == "Passed" @pytest.mark.httpretty def test_get_results(self, tfsapi): results = tfsapi.results(1, 26) - assert results[0].outcome == 'Passed' - assert results[1].outcome == 'Failed' - assert httpretty.last_request().querystring['$top'] == ['26'] + assert results[0].outcome == "Passed" + assert results[1].outcome == "Failed" + assert httpretty.last_request().querystring["$top"] == ["26"] @pytest.mark.httpretty def test_get_result(self, tfsapi): result = tfsapi.result(1, 100000) - assert result.outcome == 'Passed' - assert result.automatedTestStorage == 'unittestproject1.dll' + assert result.outcome == "Passed" + assert result.automatedTestStorage == "unittestproject1.dll" @pytest.mark.httpretty def test_adjusted_area_iteration(self): - new_project = 'NewProject' - api = TFSAPI("http://tfs.tfs.ru/tfs", 'DefaultCollection/{}'.format(new_project), 'username', 'password') + new_project = "NewProject" + api = TFSAPI( + "http://tfs.tfs.ru/tfs", + "DefaultCollection/{}".format(new_project), + "username", + "password", + ) - old_area = 'OldProject\\Area1' - new_area = '{}\\Area1'.format(new_project) + old_area = "OldProject\\Area1" + new_area = "{}\\Area1".format(new_project) assert api._TFSAPI__adjusted_area_iteration(old_area) == new_area @@ -225,15 +255,14 @@ def test_wit_queries_myqueries_empty(self, tfsapi): # Get id from first file and get workitem from second by id # tests/resources/tfs/DefaultCollection/_apis/wit/queries/MyQueries/AssignedToMe/response.json # tests/resources/tfs/DefaultCollection/_apis/wit/wiql/fd4f2f90-8922-4fe7-b215-e8a3a5a03e91/response.json - wis = tfsapi.run_query('MyQueries/AssignedToMe') + wis = tfsapi.run_query("MyQueries/AssignedToMe") assert len(wis.workitems) == 2 - pass @pytest.mark.httpretty def test_get_definitions_by_name(self, tfsapi): - definitions = tfsapi.definitions('release_*') + definitions = tfsapi.definitions("release_*") - assert httpretty.last_request().querystring['name'] == ['release_*'] + assert httpretty.last_request().querystring["name"] == ["release_*"] assert len(definitions) == 5 assert isinstance(definitions[0], Definition) with pytest.raises(Exception): @@ -242,57 +271,65 @@ def test_get_definitions_by_name(self, tfsapi): @pytest.mark.httpretty def test_definition_remove_attr(self, tfsapi): definition = tfsapi.definition(29) - definition.deleteAttrs('triggers', 'comment') + definition.deleteAttrs("triggers", "comment") assert isinstance(definition, Definition) - assert 'triggers' not in definition.data - assert 'comment' not in definition.data - assert not hasattr(definition, 'triggers') - assert not hasattr(definition, 'comment') + assert "triggers" not in definition.data + assert "comment" not in definition.data + assert not hasattr(definition, "triggers") + assert not hasattr(definition, "comment") @pytest.mark.httpretty def test_definition_clone(self, tfsapi): definition = tfsapi.definition(29) - data = {'comment': 'we need a clone'} + data = {"comment": "we need a clone"} clone = definition.clone(data) - assert clone.name == definition.name + '_clone' - assert clone.comment == data['comment'] + assert clone.name == definition.name + "_clone" + assert clone.comment == data["comment"] @pytest.mark.httpretty def test_definition_update(self, tfsapi): definition = tfsapi.definition(29) - data = {'repository': {'defaultBranch': "refs/heads/featureBranch"}} + data = {"repository": {"defaultBranch": "refs/heads/featureBranch"}} definition.update(data) - assert definition.repository.defaultBranch == data['repository']['defaultBranch'] + assert ( + definition.repository.defaultBranch == data["repository"]["defaultBranch"] + ) assert httpretty.last_request().method == "PUT" - assert httpretty.last_request().headers['Content-Type'] == 'application/json' + assert httpretty.last_request().headers["Content-Type"] == "application/json" @pytest.mark.httpretty def test_definition_create(self, tfsapi): definition = tfsapi.definition(29) - data = {'name': 'new definition name', 'comment': 'save the clone'} + data = {"name": "new definition name", "comment": "save the clone"} clone = definition.clone(data) result = clone.create() - assert result.name == data['name'] - assert result.comment == data['comment'] + assert result.name == data["name"] + assert result.comment == data["comment"] assert httpretty.last_request().method == "POST" class TestHTTPClient: def test__get_collection(self): - collection, project = TFSHTTPClient.get_collection_and_project('DefaultCollection') - assert collection == 'DefaultCollection' + collection, project = TFSHTTPClient.get_collection_and_project( + "DefaultCollection" + ) + assert collection == "DefaultCollection" assert project is None def test__get_collection_and_project(self): - collection, project = TFSHTTPClient.get_collection_and_project('DefaultCollection/Project') - assert collection == 'DefaultCollection' - assert project == 'Project' + collection, project = TFSHTTPClient.get_collection_and_project( + "DefaultCollection/Project" + ) + assert collection == "DefaultCollection" + assert project == "Project" def test__get_collection_and_project_and_team(self): - collection, project = TFSHTTPClient.get_collection_and_project('DefaultCollection/Project/Team') - assert collection == 'DefaultCollection' - assert project == 'Project' + collection, project = TFSHTTPClient.get_collection_and_project( + "DefaultCollection/Project/Team" + ) + assert collection == "DefaultCollection" + assert project == "Project" diff --git a/tests/test_resources.py b/tests/test_resources.py index decfd1d..c1dea91 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- - - import json + import pytest + from tfs.resources import * @@ -132,26 +132,26 @@ def test_workitem_id(self, workitem): assert workitem.id == 100 def test_workitem_fields(self, workitem): - assert workitem['Reason'] == "New" - assert workitem['AreaPath'] == "Test Agile" - assert workitem['Tags'] is None + assert workitem["Reason"] == "New" + assert workitem["AreaPath"] == "Test Agile" + assert workitem["Tags"] is None def test_workitem_fields_with_prefix(self, workitem): - assert workitem['System.Reason'] == "New" - assert workitem['System.AreaPath'] == "Test Agile" - assert workitem['System.Tags'] is None + assert workitem["System.Reason"] == "New" + assert workitem["System.AreaPath"] == "Test Agile" + assert workitem["System.Tags"] is None def test_workitem_fields_custom(self, workitem): - assert workitem['Custom.Bug.Type'] == "Manual Test Case" + assert workitem["Custom.Bug.Type"] == "Manual Test Case" @pytest.mark.httpretty def test_workitem_field_update(self, workitem): - workitem['Reason'] = "Canceled" - assert workitem['Reason'] == "Canceled" + workitem["Reason"] = "Canceled" + assert workitem["Reason"] == "Canceled" def test_workitem_fields_case_ins(self, workitem): - assert workitem['ReaSon'] == "New" - assert workitem['AREAPath'] == "Test Agile" + assert workitem["ReaSon"] == "New" + assert workitem["AREAPath"] == "Test Agile" def test_workitem_parent_id(self, workitem): assert workitem.parent_id == 110 @@ -161,26 +161,35 @@ def test_workitem_parent_with_child_only(self, workitem_with_child_only): assert workitem_with_child_only.child_ids == [10, 11] def test_workitem_field_russia(self, workitem): - assert workitem['russia'] == "Русский язык" + assert workitem["russia"] == "Русский язык" def test_workitem_field_names(self, workitem): - assert 'Russia' in workitem.field_names - assert 'Title' in workitem.field_names + assert "Russia" in workitem.field_names + assert "Title" in workitem.field_names def test_find_in_relation(self, workitem): - assert len(workitem.find_in_relation('Hierarchy-Reverse')) == 1, 'Can not find in relation some link' + assert ( + len(workitem.find_in_relation("Hierarchy-Reverse")) == 1 + ), "Can not find in relation some link" def test_attachment(self, workitem): assert len(workitem.attachments) == 1 attach = workitem.attachments[0] assert isinstance(attach, Attachment) - assert attach.name == '.gitignore' + assert attach.name == ".gitignore" def test_dir_links(self, workitem): - properties_must_be = ['workItemHistory', 'workItemRevisions', 'workItemType', 'workItemUpdates'] + properties_must_be = [ + "workItemHistory", + "workItemRevisions", + "workItemType", + "workItemUpdates", + ] properties = dir(workitem) for property_name in properties_must_be: - assert property_name in properties, "Workitem object must has attribute '{}'".format(property_name) + assert ( + property_name in properties + ), "Workitem object must has attribute '{}'".format(property_name) # Started failing without related changes at # https://travis-ci.org/devopshq/tfs/builds/378607508?utm_source=github_status&utm_medium=notification @@ -248,7 +257,7 @@ def test_changeset_fields(self, changeset): assert changeset.author.displayName == "Chuck Reinhart" def test_changeset_fields_get(self, changeset): - assert changeset.get('comment') == "My Comment" + assert changeset.get("comment") == "My Comment" @pytest.mark.httpretty def test_get_changesets_workitem(self, tfsapi): @@ -443,6 +452,6 @@ def test_wiql_result(self, wiql): class TestUtilities(object): def test_class_for_resource_is_case_insensitive(self): - obj = class_for_resource('bUiLd/DeFiNiTiOnS/123') + obj = class_for_resource("bUiLd/DeFiNiTiOnS/123") assert obj is Definition diff --git a/tfs/connection.py b/tfs/connection.py index 902604e..12832ce 100644 --- a/tfs/connection.py +++ b/tfs/connection.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- +import base64 from urllib.parse import quote -import base64 import requests from requests.auth import HTTPBasicAuth @@ -15,13 +15,22 @@ def batch(iterable, n=1): """ len_ = len(iterable) for ndx in range(0, len_, n): - yield iterable[ndx:min(ndx + n, len_)] + yield iterable[ndx : min(ndx + n, len_)] class TFSAPI: - def __init__(self, server_url, project="DefaultCollection", user=None, password=None, pat=None, verify=False, - auth_type=HTTPBasicAuth, - connect_timeout=20, read_timeout=180): + def __init__( + self, + server_url, + project="DefaultCollection", + user=None, + password=None, + pat=None, + verify=False, + auth_type=HTTPBasicAuth, + connect_timeout=20, + read_timeout=180, + ): """ This class must be used to get first object from TFS @@ -36,27 +45,32 @@ def __init__(self, server_url, project="DefaultCollection", user=None, password= """ if auth_type is HTTPBasicAuth: if (user is None or password is None) and pat is None: - raise ValueError('User name and password or personal access token must be specified!') - self.rest_client = TFSHTTPClient(server_url, - project=project, - user=user, password=password, pat=pat, - verify=verify, - timeout=(connect_timeout, read_timeout), - auth_type=auth_type, - ) + raise ValueError( + "User name and password or personal access token must be specified!" + ) + self.rest_client = TFSHTTPClient( + server_url, + project=project, + user=user, + password=password, + pat=pat, + verify=verify, + timeout=(connect_timeout, read_timeout), + auth_type=auth_type, + ) def get_tfs_resource(self, uri, underProject=None, payload=None): - """ Return any object in TFS by the uri """ + """Return any object in TFS by the uri""" raw = self.get_json(uri=uri, underProject=underProject, payload=payload) # For list results - if 'value' in raw: - raw = raw['value'] - url = raw[0].get('url', '') if raw else '' + if "value" in raw: + raw = raw["value"] + url = raw[0].get("url", "") if raw else "" tfs_class = class_for_resource(url) return [tfs_class(tfs=self, raw=x, listVersion=True) for x in raw] else: - return class_for_resource(raw['url'])(tfs=self, raw=raw, listVersion=False) + return class_for_resource(raw["url"])(tfs=self, raw=raw, listVersion=False) def _find_resource(self, resource_class, ids=None): """ @@ -68,7 +82,7 @@ def _find_resource(self, resource_class, ids=None): return resource def get_json(self, uri, underProject=None, payload=None): - """ Get resource from known location or try both locations + """Get resource from known location or try both locations (under collection and under collection/project) :param uri: uri of he resource @@ -84,7 +98,7 @@ def get_json(self, uri, underProject=None, payload=None): return self.rest_client.send_get(uri, payload=payload, project=False) def substitute_ids(self, uri, ids): - """ Substitute id placeholders in the uri + """Substitute id placeholders in the uri :param uri: uri with id placeholders :type uri: str @@ -96,22 +110,23 @@ def substitute_ids(self, uri, ids): return uri.format(ids) - def __get_workitems(self, work_items_ids, fields=None, expand='all'): - ids_string = ','.join(map(str, work_items_ids)) - expand = '&$expand={}'.format(expand) if expand else '' - fields_string = ('&fields=' + ','.join(fields)) if fields else "" + def __get_workitems(self, work_items_ids, fields=None, expand="all"): + ids_string = ",".join(map(str, work_items_ids)) + expand = "&$expand={}".format(expand) if expand else "" + fields_string = ("&fields=" + ",".join(fields)) if fields else "" workitems = self.get_tfs_resource( - 'wit/workitems?ids={ids}{fields}{expand}&api-version=1.0'.format(ids=ids_string, - fields=fields_string, - expand=expand), - underProject=False) + "wit/workitems?ids={ids}{fields}{expand}&api-version=1.0".format( + ids=ids_string, fields=fields_string, expand=expand + ), + underProject=False, + ) return workitems def get_workitem(self, id_, fields=None): if isinstance(id_, int): return self.get_workitems(id_, fields)[0] - def get_workitems(self, work_items_ids, fields=None, batch_size=50, expand='all'): + def get_workitems(self, work_items_ids, fields=None, batch_size=50, expand="all"): if isinstance(work_items_ids, int): work_items_ids = [work_items_ids] if isinstance(work_items_ids, str): @@ -119,7 +134,9 @@ def get_workitems(self, work_items_ids, fields=None, batch_size=50, expand='all' workitems = [] for work_items_batch in batch(list(work_items_ids), batch_size): - work_items_batch_info = self.__get_workitems(work_items_batch, fields=fields, expand=expand) + work_items_batch_info = self.__get_workitems( + work_items_batch, fields=fields, expand=expand + ) workitems += work_items_batch_info return workitems @@ -127,56 +144,60 @@ def get_changeset(self, id): return self._find_resource(Changeset, ids=id) def get_changesets(self, from_=None, to_=None, item_path=None, top=10000): - payload = {'$top': top} + payload = {"$top": top} if from_: from_ = str(from_) if from_.isdigit(): - payload['searchCriteria.fromId'] = from_ + payload["searchCriteria.fromId"] = from_ else: - raise ValueError('from_ must be valid TFS changeset IDs!') + raise ValueError("from_ must be valid TFS changeset IDs!") if to_: to_ = str(to_) if to_.isdigit(): - payload['searchCriteria.toId'] = to_ + payload["searchCriteria.toId"] = to_ else: - raise ValueError('to_ must be valid TFS changeset IDs!') + raise ValueError("to_ must be valid TFS changeset IDs!") if item_path: - payload['searchCriteria.itemPath'] = item_path - changesets = self.get_tfs_resource('tfvc/changesets', underProject=False, payload=payload) + payload["searchCriteria.itemPath"] = item_path + changesets = self.get_tfs_resource( + "tfvc/changesets", underProject=False, payload=payload + ) return changesets def get_projects(self): - """ Deprecated. Use projects instead """ + """Deprecated. Use projects instead""" return self.projects def get_project(self, name): - """ Deprecated. Use project instead""" + """Deprecated. Use project instead""" return self.project(name) @property def projects(self): - """ List of all projects """ - return self.get_tfs_resource('projects', underProject=False) + """List of all projects""" + return self.get_tfs_resource("projects", underProject=False) def project(self, id): - """ Get project by id """ + """Get project by id""" return self._find_resource(Project, id) def teams(self, projectId): - """ List of teams under the project + """List of teams under the project :param projectId: id of the project """ - return self.get_tfs_resource('projects/{}/teams'.format(projectId), underProject=False) + return self.get_tfs_resource( + "projects/{}/teams".format(projectId), underProject=False + ) def builds(self): - return self.get_tfs_resource('build/builds', underProject=True) + return self.get_tfs_resource("build/builds", underProject=True) def build(self, id): - """ Get build by id + """Get build by id :param id: id of the build :return: Build class @@ -184,17 +205,19 @@ def build(self, id): return self._find_resource(Build, ids=id) def definitions(self, nameFilter=None): - """ List of build definitions + """List of build definitions :param nameFilter: Filters to definitions whose names equal this value. Use ``*`` as a wildcard, ex: 'Release_11.*' or 'Release_*_11.0' :return: list of :class:`Definition` object - """ + """ if nameFilter: - payload = {'name': nameFilter} + payload = {"name": nameFilter} else: payload = None - return self.get_tfs_resource('build/definitions', underProject=True, payload=payload) + return self.get_tfs_resource( + "build/definitions", underProject=True, payload=payload + ) def definition(self, id): return self._find_resource(Definition, ids=id) @@ -202,9 +225,9 @@ def definition(self, id): def runs(self, top=None): if top is None: top = 100 - payload = {'$top': top} + payload = {"$top": top} - return self.get_tfs_resource('test/runs', underProject=True, payload=payload) + return self.get_tfs_resource("test/runs", underProject=True, payload=payload) def run(self, id): return self._find_resource(Run, ids=id) @@ -212,30 +235,34 @@ def run(self, id): def results(self, runId, top=None): if top is None: top = 100 - payload = {'$top': top} - return self.get_tfs_resource('test/runs/{}/results'.format(runId), underProject=True, payload=payload) + payload = {"$top": top} + return self.get_tfs_resource( + "test/runs/{}/results".format(runId), underProject=True, payload=payload + ) def result(self, runId, resultId): return self._find_resource(Result, ids=(runId, resultId)) # not a resource def update_workitem(self, work_item_id, update_data, params=None): - raw = self.rest_client.send_patch('wit/workitems/{id}?api-version=1.0'.format(id=work_item_id), - data=update_data, - headers={'Content-Type': 'application/json-patch+json'}, - payload=params) + raw = self.rest_client.send_patch( + "wit/workitems/{id}?api-version=1.0".format(id=work_item_id), + data=update_data, + headers={"Content-Type": "application/json-patch+json"}, + payload=params, + ) return raw def run_query(self, path): - """ Get query definition by path - and get Wiql of this query results in the self.result + """Get query definition by path + and get Wiql of this query results in the self.result """ - if path and not path.startswith('/'): - path = '/' + quote(path) + if path and not path.startswith("/"): + path = "/" + quote(path) return self._find_resource(TFSQuery, ids=path) def run_saved_query(self, id): - """ Run saved query by query id + """Run saved query by query id :param id: id of the query to run :return: instance of the Wiql object with query results @@ -243,50 +270,69 @@ def run_saved_query(self, id): return self._find_resource(Wiql, ids=id) def run_wiql(self, query, params=None): - data = {"query": query, } + data = { + "query": query, + } if params is None: params = {} - if 'api-version' not in params: - params['api-version'] = '1.0' - wiql = self.rest_client.send_post('wit/wiql', - data=data, - project=True, - payload=params - ) + if "api-version" not in params: + params["api-version"] = "1.0" + wiql = self.rest_client.send_post( + "wit/wiql", data=data, project=True, payload=params + ) return Wiql(self, wiql) def download_file(self, uri, filename): # TODO: Use download in stream, not in memory r = self.rest_client.send_get(uri, json=False) - with open(filename, 'wb') as file: + with open(filename, "wb") as file: file.write(r.content) def get_gitrepositories(self): - return self.get_tfs_resource('git/repositories', underProject=False) + return self.get_tfs_resource("git/repositories", underProject=False) def get_gitrepository(self, name): return self._find_resource(GitRepository, ids=name) - def __create_workitem(self, type_, data=None, validate_only=None, bypass_rules=None, - suppress_notifications=None, - api_version=1.0): + def __create_workitem( + self, + type_, + data=None, + validate_only=None, + bypass_rules=None, + suppress_notifications=None, + api_version=1.0, + ): """ Create work item. Param description: https://docs.microsoft.com/en-us/rest/api/vsts/wit/work%20items/create :param project: Name of the target project. The same project is used by default. :return: Raw JSON of the work item created """ - uri = 'wit/workitems/${type}'.format(type=type_) - params = {'api-version': api_version, 'validateOnly': validate_only, 'bypassRules': bypass_rules, - 'suppressNotifications': suppress_notifications} - - headers = {'Content-Type': 'application/json-patch+json'} - raw = self.rest_client.send_post(uri=uri, data=data, headers=headers, project=True, payload=params) + uri = "wit/workitems/${type}".format(type=type_) + params = { + "api-version": api_version, + "validateOnly": validate_only, + "bypassRules": bypass_rules, + "suppressNotifications": suppress_notifications, + } + + headers = {"Content-Type": "application/json-patch+json"} + raw = self.rest_client.send_post( + uri=uri, data=data, headers=headers, project=True, payload=params + ) return raw - def create_workitem(self, type_, fields=None, relations_raw=None, validate_only=None, bypass_rules=None, - suppress_notifications=None, - api_version=1.0): + def create_workitem( + self, + type_, + fields=None, + relations_raw=None, + validate_only=None, + bypass_rules=None, + suppress_notifications=None, + api_version=1.0, + ): """ Create work item. Doc: https://docs.microsoft.com/en-us/rest/api/vsts/wit/work%20items/create @@ -301,14 +347,31 @@ def create_workitem(self, type_, fields=None, relations_raw=None, validate_only= """ # fields - body = [dict(op="add", path='/fields/{}'.format(name), value=value) for name, value in fields.items()] \ - if fields else [] + body = ( + [ + dict(op="add", path="/fields/{}".format(name), value=value) + for name, value in fields.items() + ] + if fields + else [] + ) # relations if relations_raw: - body.extend([dict(op="add", path='/relations/-', value=relation) for relation in relations_raw]) - - raw = self.__create_workitem(type_, body, validate_only, bypass_rules, suppress_notifications, - api_version) + body.extend( + [ + dict(op="add", path="/relations/-", value=relation) + for relation in relations_raw + ] + ) + + raw = self.__create_workitem( + type_, + body, + validate_only, + bypass_rules, + suppress_notifications, + api_version, + ) return Workitem(self, raw) @@ -319,17 +382,23 @@ def __adjusted_area_iteration(self, value): :param value: Old area/iteration path value. :return: Value with the project part replaced. """ - actual_area = value.split('\\')[1:] + actual_area = value.split("\\")[1:] actual_area.insert(0, self.rest_client.project) - return '\\'.join(actual_area) - - def copy_workitem(self, workitem, with_links_and_attachments=False, from_another_project=False, target_type=None, - target_area=None, - target_iteration=None, - validate_only=None, - bypass_rules=None, - suppress_notifications=None, - api_version=1.0): + return "\\".join(actual_area) + + def copy_workitem( + self, + workitem, + with_links_and_attachments=False, + from_another_project=False, + target_type=None, + target_area=None, + target_iteration=None, + validate_only=None, + bypass_rules=None, + suppress_notifications=None, + api_version=1.0, + ): """ Create a copy of a work item @@ -346,38 +415,44 @@ def copy_workitem(self, workitem, with_links_and_attachments=False, from_another :return: WorkItem instance of a newly created copy """ - fields = workitem.data.get('fields') - type_ = target_type if target_type else fields['System.WorkItemType'] + fields = workitem.data.get("fields") + type_ = target_type if target_type else fields["System.WorkItemType"] - params = {'api-version': api_version, 'validateOnly': validate_only, 'bypassRules': bypass_rules, - 'suppressNotifications': suppress_notifications} + params = { + "api-version": api_version, + "validateOnly": validate_only, + "bypassRules": bypass_rules, + "suppressNotifications": suppress_notifications, + } # When copy from another project, adjust AreaPath and IterationPath and do not copy identifying fields if from_another_project: - no_copy_fields = ['System.TeamProject', - 'System.AreaPath', - 'System.IterationPath', - 'System.Id', - 'System.AreaId', - 'System.NodeName', - 'System.AreaLevel1', - 'System.AreaLevel2', - 'System.AreaLevel3', - 'System.AreaLevel4', - 'System.Rev', - 'System.AutorizedDate', - 'System.RevisedDate', - 'System.IterationId', - 'System.IterationLevel1', - 'System.IterationLevel2', - 'System.IterationLevel4', - 'System.CreatedDate', - 'System.CreatedBy', - 'System.ChangedDate', - 'System.ChangedBy', - 'System.AuthorizedAs', - 'System.AuthorizedDate', - 'System.Watermark'] + no_copy_fields = [ + "System.TeamProject", + "System.AreaPath", + "System.IterationPath", + "System.Id", + "System.AreaId", + "System.NodeName", + "System.AreaLevel1", + "System.AreaLevel2", + "System.AreaLevel3", + "System.AreaLevel4", + "System.Rev", + "System.AutorizedDate", + "System.RevisedDate", + "System.IterationId", + "System.IterationLevel1", + "System.IterationLevel2", + "System.IterationLevel4", + "System.CreatedDate", + "System.CreatedBy", + "System.ChangedDate", + "System.ChangedBy", + "System.AuthorizedAs", + "System.AuthorizedDate", + "System.Watermark", + ] fields = {} for name, value in workitem.fields.items(): @@ -385,19 +460,31 @@ def copy_workitem(self, workitem, with_links_and_attachments=False, from_another continue fields[name] = value - fields['System.AreaPath'] = target_area \ - if target_area else self.__adjusted_area_iteration(workitem['AreaPath']) - fields['System.IterationPath'] = target_iteration \ - if target_iteration else self.__adjusted_area_iteration(workitem['IterationPath']) + fields["System.AreaPath"] = ( + target_area + if target_area + else self.__adjusted_area_iteration(workitem["AreaPath"]) + ) + fields["System.IterationPath"] = ( + target_iteration + if target_iteration + else self.__adjusted_area_iteration(workitem["IterationPath"]) + ) relations = None - wi = self.create_workitem(type_, fields, relations, validate_only, bypass_rules, - suppress_notifications, - api_version) + wi = self.create_workitem( + type_, + fields, + relations, + validate_only, + bypass_rules, + suppress_notifications, + api_version, + ) if with_links_and_attachments: - wi.add_relations_raw(workitem.data.get('relations', {}), params) + wi.add_relations_raw(workitem.data.get("relations", {}), params) return wi @@ -406,28 +493,42 @@ class TFSClientError(Exception): class TFSHTTPClient: - def __init__(self, base_url, project, user, password, pat, verify=False, timeout=None, auth_type=None): - if not base_url.endswith('/'): - base_url += '/' + def __init__( + self, + base_url, + project, + user, + password, + pat, + verify=False, + timeout=None, + auth_type=None, + ): + if not base_url.endswith("/"): + base_url += "/" collection, project = self.get_collection_and_project(project) self.collection = collection self.project = project # Remove part after / in project-name, like DefaultCollection/MyProject => DefaultCollection # API responce only in Project, without subproject - self._url = base_url + '%s/_apis/' % collection + self._url = base_url + "%s/_apis/" % collection if project: - self._url_prj = base_url + '%s/%s/_apis/' % (collection, project) + self._url_prj = base_url + "%s/%s/_apis/" % (collection, project) else: self._url_prj = self._url self.http_session = requests.Session() if pat is not None: pat = ":" + pat - pat_base64 = b'Basic ' + base64.b64encode(pat.encode("utf8")) - self.http_session.headers.update({'Authorization': pat_base64}) + pat_base64 = b"Basic " + base64.b64encode(pat.encode("utf8")) + self.http_session.headers.update({"Authorization": pat_base64}) else: - auth = auth_type() if user is None and password is None else auth_type(user, password) + auth = ( + auth_type() + if user is None and password is None + else auth_type(user, password) + ) self.http_session.auth = auth self.api_version = None @@ -435,11 +536,12 @@ def __init__(self, base_url, project, user, password, pat, verify=False, timeout self._verify = verify if not self._verify: from requests.packages.urllib3.exceptions import InsecureRequestWarning + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) @staticmethod def get_collection_and_project(project): - splitted_project = project.split('/') + splitted_project = project.split("/") collection = splitted_project[0] project = None @@ -447,23 +549,40 @@ def get_collection_and_project(project): project = splitted_project[1] # If not space if project: - project = project.split('/')[0] + project = project.split("/")[0] return collection, project def send_get(self, uri, payload=None, project=False, json=True): - return self.__send_request('GET', uri, None, payload=payload, underProject=project, json=json) + return self.__send_request( + "GET", uri, None, payload=payload, underProject=project, json=json + ) def send_post(self, uri, data, headers=None, payload=None, project=False): - return self.__send_request('POST', uri, data, headers, payload=payload, underProject=project) + return self.__send_request( + "POST", uri, data, headers, payload=payload, underProject=project + ) def send_put(self, uri, data, headers=None, payload=None, project=False): - return self.__send_request('PUT', uri, data, headers, payload=payload, underProject=project) + return self.__send_request( + "PUT", uri, data, headers, payload=payload, underProject=project + ) def send_patch(self, uri, data, headers, payload=None, project=False): - return self.__send_request('PATCH', uri, data, headers, payload=payload, underProject=project) - - def __send_request(self, method, uri, data, headers=None, payload=None, underProject=False, json=True): + return self.__send_request( + "PATCH", uri, data, headers, payload=payload, underProject=project + ) + + def __send_request( + self, + method, + uri, + data, + headers=None, + payload=None, + underProject=False, + json=True, + ): """ Send request @@ -485,31 +604,54 @@ def __send_request(self, method, uri, data, headers=None, payload=None, underPro if payload is None: payload = {} - if self.api_version and payload.get('api-version') is None: - payload['api-version'] = self.api_version + if self.api_version and payload.get("api-version") is None: + payload["api-version"] = self.api_version if headers is None: headers = {} - if headers.get('Content-Type') is None: - headers['Content-Type'] = 'application/json' - - if method == 'POST': - response = self.http_session.post(url, json=data, verify=self._verify, headers=headers, params=payload, - timeout=self.timeout) - elif method == 'PUT': - response = self.http_session.put(url, json=data, verify=self._verify, headers=headers, params=payload, - timeout=self.timeout) - elif method == 'PATCH': - response = self.http_session.patch(url, json=data, verify=self._verify, headers=headers, params=payload, - timeout=self.timeout) + if headers.get("Content-Type") is None: + headers["Content-Type"] = "application/json" + + if method == "POST": + response = self.http_session.post( + url, + json=data, + verify=self._verify, + headers=headers, + params=payload, + timeout=self.timeout, + ) + elif method == "PUT": + response = self.http_session.put( + url, + json=data, + verify=self._verify, + headers=headers, + params=payload, + timeout=self.timeout, + ) + elif method == "PATCH": + response = self.http_session.patch( + url, + json=data, + verify=self._verify, + headers=headers, + params=payload, + timeout=self.timeout, + ) else: - response = self.http_session.get(url, headers=headers, verify=self._verify, params=payload, - timeout=self.timeout) + response = self.http_session.get( + url, + headers=headers, + verify=self._verify, + params=payload, + timeout=self.timeout, + ) response.raise_for_status() if self.api_version is None: - api_type = response.headers['Content-Type'].split('; ')[-1].split('=') - if api_type[0] == 'api-version': + api_type = response.headers["Content-Type"].split("; ")[-1].split("=") + if api_type[0] == "api-version": self.api_version = api_type[1] if json: @@ -517,11 +659,16 @@ def __send_request(self, method, uri, data, headers=None, payload=None, underPro result = response.json() if response.status_code not in (200, 201, 202): - raise TFSClientError('TFS API returned HTTP %s (%s)' % ( - response.status_code, result['error'] if 'error' in result else response.reason)) + raise TFSClientError( + "TFS API returned HTTP %s (%s)" + % ( + response.status_code, + result["error"] if "error" in result else response.reason, + ) + ) return result except ValueError: - raise TFSClientError('Response is not json: {}'.format(response.text)) + raise TFSClientError("Response is not json: {}".format(response.text)) else: return response @@ -533,7 +680,7 @@ def __prepare_uri(self, underProject, uri): :param uri: relative uri or the full http url of the resource :return: full url to the resource """ - if uri.startswith('http'): + if uri.startswith("http"): # If we use URL (full path) url = uri else: diff --git a/tfs/resources.py b/tfs/resources.py index 212450b..189a6ae 100644 --- a/tfs/resources.py +++ b/tfs/resources.py @@ -11,7 +11,7 @@ class TFSObject(object): - def __init__(self, data=None, tfs=None, uri='', underProject=None): + def __init__(self, data=None, tfs=None, uri="", underProject=None): """ Base tfs resource object initialization @@ -45,15 +45,15 @@ def __dir__(self): if not self.data: return original - extend = [x for x in self.data.get('_links', {}) if x in self._links_attrs] + extend = [x for x in self.data.get("_links", {}) if x in self._links_attrs] return original + extend def __get_object_by_links(self, name): """ Dynamically add property for all ``_links`` field in JSON, if exist """ - links = self.data.get('_links', {}) # or emtpy if _links is not exist - url = links[name]['href'] + links = self.data.get("_links", {}) # or emtpy if _links is not exist + url = links[name]["href"] return self.tfs.get_tfs_resource(url) def __getattr__(self, name): @@ -62,9 +62,11 @@ def __getattr__(self, name): :param name: :return: mapped or unknown tfs object """ - if self.data and name in self.data.get('_links', {}): + if self.data and name in self.data.get("_links", {}): return self.__get_object_by_links(name) - raise AttributeError("'{}' object has no attribute '{}'".format(self.__class__.__name__, name)) + raise AttributeError( + "'{}' object has no attribute '{}'".format(self.__class__.__name__, name) + ) # TODO: implement better repr # def __repr__(self): @@ -90,9 +92,11 @@ def get(self, key, default=None): class UnknownTfsObject(TFSObject): - """ Not yet known Resource from TFS. """ + """Not yet known Resource from TFS.""" - def __init__(self, tfs, raw=None, uri='unknownResource', underProject=None, listVersion=True): + def __init__( + self, tfs, raw=None, uri="unknownResource", underProject=None, listVersion=True + ): """ :param tfs: instance of :class:`TFSAPI` @@ -105,8 +109,8 @@ def __init__(self, tfs, raw=None, uri='unknownResource', underProject=None, list :param listVersion: indicates list version of the object """ # list of fields to exclude from resource translation and assign as a raw json value - self.raw_attrs = ['_links'] - self._clone_delete = ['id', '_links'] + self.raw_attrs = ["_links"] + self._clone_delete = ["id", "_links"] # indicates object is a brief version of the full one (commonly in the list result) # so to operate on it you need to get a full one self._listVersion = listVersion @@ -123,7 +127,7 @@ def update(self, values=None): :param values: json structure to be merged with object's current state """ if self._listVersion: - raise Exception('You can\'t use list version of the object to update') + raise Exception("You can't use list version of the object to update") if not isinstance(values, dict): return self @@ -136,7 +140,7 @@ def update(self, values=None): return self def deleteAttrs(self, *attrs): - """ Delete attribute from both data and object + """Delete attribute from both data and object :param attrName: name of the attribute :type attrName: str @@ -151,8 +155,7 @@ def deleteAttrs(self, *attrs): return self def clone(self, values): - """ Clone resource - """ + """Clone resource""" data = updateDict(deepcopy(self.data), values) clone = self.__class__(self.tfs, data) @@ -162,19 +165,20 @@ def clone(self, values): return clone def create(self, uri=None): - """ Create new instance of the object from the current self.data state - """ + """Create new instance of the object from the current self.data state""" if uri is None: uri = self._uri - pos = uri.rfind('/{') + pos = uri.rfind("/{") if pos > -1: uri = uri[:pos] - result = self.tfs.rest_client.send_post(uri, data=self.data, project=self._underProject) + result = self.tfs.rest_client.send_post( + uri, data=self.data, project=self._underProject + ) self._parse_raw(result) return self def _find(self, ids): - """ Fills up the resource based on the resource's id. + """Fills up the resource based on the resource's id. :param ids: ids to replace id placeholders in the uri :type ids: Union[Tuple[str, str], int, str] @@ -183,7 +187,7 @@ def _find(self, ids): self._load(uri) def _load(self, uri): - """ Load a resource. + """Load a resource. :param uri: uri of the resource to get :type uri: str @@ -208,17 +212,21 @@ def __init__(self, tfs=None, raw=None, listVersion=True): self.fields = None # Use prefix in automatically lookup. # We don't need use wi['System.History'], we use simple wi['History'] - self._system_prefix = 'System.' + self._system_prefix = "System." - super().__init__(tfs, raw, 'wit/workItems/{0}', underProject=False, listVersion=listVersion) - self._links_attrs.extend(['workItemHistory', 'workItemRevisions', 'workItemType', 'workItemUpdates']) + super().__init__( + tfs, raw, "wit/workItems/{0}", underProject=False, listVersion=listVersion + ) + self._links_attrs.extend( + ["workItemHistory", "workItemRevisions", "workItemType", "workItemUpdates"] + ) def _parse_raw(self, raw): - self.raw_attrs.extend(['fields']) + self.raw_attrs.extend(["fields"]) super()._parse_raw(raw) if not self.id: - self.id = self.url.split('/')[-1] + self.id = self.url.split("/")[-1] if self.fields: self.fields = CaseInsensitiveDict(self.fields) self._fields = self.fields @@ -253,7 +261,7 @@ def _add_prefix(self, key): def _remove_prefix(self, key): if key.startswith(self._system_prefix): - return key[len(self._system_prefix):] + return key[len(self._system_prefix) :] else: return key @@ -277,11 +285,12 @@ def find_in_relation(self, relation_type): :return: """ found = [] - for relation in self.data.get('relations', []): + for relation in self.data.get("relations", []): # Find as is, e.g. 'AttachedFile' or more smartly. # Found 'Hierarchy-Forward' in 'System.LinkTypes.Hierarchy-Forward' - if relation_type == relation.get('rel', '') \ - or relation.get('rel', '').endswith(relation_type): + if relation_type == relation.get("rel", "") or relation.get( + "rel", "" + ).endswith(relation_type): found.append(relation) return found @@ -293,7 +302,7 @@ def _find_in_relation(self, relation_type, return_one=True): ids = [] relations = self.find_in_relation(relation_type) for relation in relations: - id_ = relation['url'].split('/')[-1] + id_ = relation["url"].split("/")[-1] id_ = int(id_) ids.append(id_) if return_one: @@ -303,7 +312,7 @@ def _find_in_relation(self, relation_type, return_one=True): @property def parent_id(self): - return self._find_in_relation('Hierarchy-Reverse', return_one=True) + return self._find_in_relation("Hierarchy-Reverse", return_one=True) @property def parent(self): @@ -313,7 +322,7 @@ def parent(self): @property def child_ids(self): - return self._find_in_relation('Hierarchy-Forward', return_one=False) + return self._find_in_relation("Hierarchy-Forward", return_one=False) @property def childs(self): @@ -335,12 +344,14 @@ def add_relations_raw(self, relations_raw, params=None): # remove ID from attributes as it has to be unique copy_raw = [deepcopy(relation) for relation in relations_raw] for relation in copy_raw: - if 'attributes' in relation: - if 'id' in relation['attributes']: - del relation['attributes']['id'] - - path = '/relations/-' - update_data = [dict(op="add", path=path, value=relation) for relation in copy_raw] + if "attributes" in relation: + if "id" in relation["attributes"]: + del relation["attributes"]["id"] + + path = "/relations/-" + update_data = [ + dict(op="add", path=path, value=relation) for relation in copy_raw + ] if update_data: raw = self.tfs.update_workitem(self.id, update_data, params) self.__init__(raw=raw, tfs=self.tfs) @@ -348,22 +359,30 @@ def add_relations_raw(self, relations_raw, params=None): class Attachment(UnknownTfsObject): def __init__(self, tfs=None, raw=None, listVersion=True): - super().__init__(tfs, raw, 'wit/attachments/{0}', underProject=False, listVersion=listVersion) + super().__init__( + tfs, raw, "wit/attachments/{0}", underProject=False, listVersion=listVersion + ) def _parse_raw(self, raw): super()._parse_raw(raw) - self.id = self.url.split('/')[-1] # Get UUID from url + self.id = self.url.split("/")[-1] # Get UUID from url self.name = self.attributes.name - def download(self, path='.'): + def download(self, path="."): path = os.path.join(path, self.name) self.tfs.download_file(self.url, path) class Changeset(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, uri="tfvc/changesets/{0}", underProject=False, listVersion=listVersion) + super().__init__( + tfs, + raw, + uri="tfvc/changesets/{0}", + underProject=False, + listVersion=listVersion, + ) def _parse_raw(self, raw): super()._parse_raw(raw) @@ -371,8 +390,9 @@ def _parse_raw(self, raw): @property def workitems(self): - wi_links = self.tfs.get_tfs_resource('tfvc/changesets/{}/workItems'.format(self.id), - underProject=False) + wi_links = self.tfs.get_tfs_resource( + "tfvc/changesets/{}/workItems".format(self.id), underProject=False + ) ids = [x.id for x in wi_links] workitems = self.tfs.get_workitems(ids) return workitems @@ -380,7 +400,13 @@ def workitems(self): class TFSQuery(UnknownTfsObject): def __init__(self, tfs=None, raw=None, listVersion=False): - super().__init__(tfs, raw, uri='wit/queries{0}?api-version=2.2', underProject=True, listVersion=listVersion) + super().__init__( + tfs, + raw, + uri="wit/queries{0}?api-version=2.2", + underProject=True, + listVersion=listVersion, + ) self._workitems = None def _parse_raw(self, raw): @@ -388,13 +414,15 @@ def _parse_raw(self, raw): # run saved query using WIQL self.result = self.tfs.run_saved_query(self.id) - self.columns = tuple(i['referenceName'] for i in self.result['columns']) - self.column_names = tuple(i['name'] for i in self.result['columns']) + self.columns = tuple(i["referenceName"] for i in self.result["columns"]) + self.column_names = tuple(i["name"] for i in self.result["columns"]) @property def workitems(self): if not self._workitems: - self._workitems = self.tfs.get_workitems((i['id'] for i in self.result['workItems'])) + self._workitems = self.tfs.get_workitems( + (i["id"] for i in self.result["workItems"]) + ) return self._workitems @@ -404,12 +432,18 @@ class Wiql(UnknownTfsObject): """ def __init__(self, tfs=None, raw=None, listVersion=True): - super().__init__(tfs, raw, 'wit/wiql/{0}?api-version=2.2', underProject=True, listVersion=listVersion) + super().__init__( + tfs, + raw, + "wit/wiql/{0}?api-version=2.2", + underProject=True, + listVersion=listVersion, + ) self.result = self.data @property def workitem_ids(self): - ids = [x['id'] for x in self.data['workItems']] + ids = [x["id"] for x in self.data["workItems"]] return ids @property @@ -419,12 +453,20 @@ def workitems(self): class GitRepository(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, "git/repositories/{0}", underProject=False, listVersion=listVersion) + super().__init__( + tfs, + raw, + "git/repositories/{0}", + underProject=False, + listVersion=listVersion, + ) class Project(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, "projects/{0}", underProject=False, listVersion=listVersion) + super().__init__( + tfs, raw, "projects/{0}", underProject=False, listVersion=listVersion + ) @property def teams(self): @@ -433,35 +475,53 @@ def teams(self): class Team(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, "projects/{0}/teams/{1}", underProject=False, listVersion=listVersion) + super().__init__( + tfs, + raw, + "projects/{0}/teams/{1}", + underProject=False, + listVersion=listVersion, + ) class Build(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, "build/builds/{0}", underProject=True, listVersion=listVersion) + super().__init__( + tfs, raw, "build/builds/{0}", underProject=True, listVersion=listVersion + ) class Definition(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, "build/definitions/{0}", underProject=True, listVersion=listVersion) - self._clone_delete.extend(['authoredBy', 'createdDate', 'comment', 'revision']) + super().__init__( + tfs, + raw, + "build/definitions/{0}", + underProject=True, + listVersion=listVersion, + ) + self._clone_delete.extend(["authoredBy", "createdDate", "comment", "revision"]) def clone(self, data=None): if data is None: data = {} - if 'name' not in data: - data['name'] = self.name + '_clone' + if "name" not in data: + data["name"] = self.name + "_clone" return super().clone(data) class Identity(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, 'identities/{0}', underProject=False, listVersion=listVersion) + super().__init__( + tfs, raw, "identities/{0}", underProject=False, listVersion=listVersion + ) class Run(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, 'test/runs/{}', underProject=True, listVersion=listVersion) + super().__init__( + tfs, raw, "test/runs/{}", underProject=True, listVersion=listVersion + ) @property def results(self): @@ -473,7 +533,13 @@ def result(self, resultId): class Result(UnknownTfsObject): def __init__(self, tfs, raw=None, listVersion=False): - super().__init__(tfs, raw, 'test/runs/{}/results/{}', underProject=True, listVersion=listVersion) + super().__init__( + tfs, + raw, + "test/runs/{}/results/{}", + underProject=True, + listVersion=listVersion, + ) ################################################################################# @@ -482,7 +548,7 @@ def __init__(self, tfs, raw=None, listVersion=False): def raw2resource(raw, top=None, tfs=None): - """ Convert a raw valie into a TFTObject object. + """Convert a raw valie into a TFTObject object. Recursively walks a dict structure, transforming the properties into attributes on a new ``TfsObject`` object of the appropriate type @@ -497,23 +563,24 @@ def raw2resource(raw, top=None, tfs=None): if isinstance(j, dict): if isinstance(top, UnknownTfsObject) and i in top.raw_attrs: setattr(top, i, j) - elif 'url' in j: - resource = class_for_resource(j['url'])(tfs=tfs, raw=j, listVersion=False) + elif "url" in j: + resource = class_for_resource(j["url"])( + tfs=tfs, raw=j, listVersion=False + ) setattr(top, i, resource) else: - setattr( - top, i, raw2resource(j, tfs=tfs)) + setattr(top, i, raw2resource(j, tfs=tfs)) elif isinstance(j, seqs): seq_list = [] for seq_elem in j: if isinstance(seq_elem, dict): - if 'url' in seq_elem: - resource = class_for_resource(seq_elem['url'])( - tfs=tfs, raw=seq_elem, listVersion=True) + if "url" in seq_elem: + resource = class_for_resource(seq_elem["url"])( + tfs=tfs, raw=seq_elem, listVersion=True + ) seq_list.append(resource) else: - seq_list.append( - raw2resource(seq_elem, tfs=tfs)) + seq_list.append(raw2resource(seq_elem, tfs=tfs)) else: seq_list.append(seq_elem) setattr(top, i, seq_list) @@ -523,19 +590,19 @@ def raw2resource(raw, top=None, tfs=None): resource_class_map = { - r'build/builds/[^/]+$': Build, - r'build/definitions/[^/]+$': Definition, - r'git/repositories/[^/]+$': GitRepository, - r'identities/[^/]+$': Identity, - r'projects/[^/]+$': Project, - r'projects/[^/]+/teams/[^/]+$': Team, - r'test/runs/[^/]+$': Run, - r'test/runs/[^/]+/results/[^/]+$': Result, - r'tfvc/changesets/[^/]+$': Changeset, - r'wit/attachments/[^/]+$': Attachment, - r'wit/queries/.+$': TFSQuery, - r'wit/wiql/[^/]+': Wiql, - r'wit/workItems/[^/]+$': Workitem + r"build/builds/[^/]+$": Build, + r"build/definitions/[^/]+$": Definition, + r"git/repositories/[^/]+$": GitRepository, + r"identities/[^/]+$": Identity, + r"projects/[^/]+$": Project, + r"projects/[^/]+/teams/[^/]+$": Team, + r"test/runs/[^/]+$": Run, + r"test/runs/[^/]+/results/[^/]+$": Result, + r"tfvc/changesets/[^/]+$": Changeset, + r"wit/attachments/[^/]+$": Attachment, + r"wit/queries/.+$": TFSQuery, + r"wit/wiql/[^/]+": Wiql, + r"wit/workItems/[^/]+$": Workitem, } From e203fefefbd65d781c750c8635baab63f7d10c0c Mon Sep 17 00:00:00 2001 From: allburov Date: Wed, 18 May 2022 17:18:41 +0700 Subject: [PATCH 2/6] Add six to install_requires --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e41561..501608a 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,11 @@ "HTTPretty", "pytest_httpretty", ], - install_requires=["requests", "requests_ntlm"], + install_requires=[ + "requests", + "requests_ntlm", + "six", + ], package_data={ "": [ "../LICENSE", From b5c68c739a75aefeca473a5e383167e4ddd6390d Mon Sep 17 00:00:00 2001 From: allburov Date: Wed, 18 May 2022 17:21:27 +0700 Subject: [PATCH 3/6] Use fixed version in setup.py - 1.1.1 --- setup.py | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/setup.py b/setup.py index 501608a..b5b5ed1 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os - from setuptools import setup -__version__ = "1.0" # identify main version of dohq-tfs tool -devStatus = "4 - Beta" # default build status, see: https://pypi.python.org/pypi?%3Aaction=list_classifiers - -if "TRAVIS_BUILD_NUMBER" in os.environ and "TRAVIS_BRANCH" in os.environ: - print("This is TRAVIS-CI build") - print("TRAVIS_BUILD_NUMBER = {}".format(os.environ["TRAVIS_BUILD_NUMBER"])) - print("TRAVIS_BRANCH = {}".format(os.environ["TRAVIS_BRANCH"])) - - __version__ += ".{}{}".format( - "" - if "release" in os.environ["TRAVIS_BRANCH"] - or os.environ["TRAVIS_BRANCH"] == "master" - else "dev", - os.environ["TRAVIS_BUILD_NUMBER"], - ) - - devStatus = ( - "5 - Production/Stable" - if "release" in os.environ["TRAVIS_BRANCH"] - or os.environ["TRAVIS_BRANCH"] == "master" - else devStatus - ) - -else: - print("This is local build") - __version__ += ".dev0" # set version as major.minor.localbuild if local build: python setup.py install - -print("dohq-tfs build version = {}".format(__version__)) - setup( name="dohq-tfs", - version=__version__, + version="1.1.1", description="Microsoft TFS Python Library (TFS API Python client) that can work with TFS workflow and workitems.", long_description="You can see detailed user manual here: https://devopshq.github.io/tfs/", license="MIT", @@ -45,8 +14,7 @@ download_url="https://github.com/devopshq/tfs.git", entry_points={}, classifiers=[ - "Development Status :: {}".format(devStatus), - "Environment :: Console", + "Development Status :: 5 - Production/Stable" "Environment :: Console", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", From 857fdbdeab222a5c08e31b531174554966eceb5a Mon Sep 17 00:00:00 2001 From: allburov Date: Wed, 18 May 2022 17:22:17 +0700 Subject: [PATCH 4/6] Remove travis files --- .travis.yml | 41 ----------------------------------------- requirements-travis.txt | 8 -------- 2 files changed, 49 deletions(-) delete mode 100644 .travis.yml delete mode 100644 requirements-travis.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b6a1a1f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: python -python: '3.4' -branches: - only: - - master - - develop -install: - - pip install -r requirements-travis.txt -script: - - set -e - - printenv - - flake8 tfs - - coverage run -m py.test tests - - coverage xml - - if [[ ! $CODACY_PROJECT_TOKEN ]]; then echo "Variable CODACY_PROJECT_TOKEN not set. Step is skipped."; exit 0; else python-codacy-coverage -r coverage.xml; fi - - set +e - -deploy: -- provider: pypi - user: devopshq - password: - secure: "u7kqhFT32wLvGvdXLRg3RtTdfGcSzfFWTQb0p1cdRHWBm/771Q2H4bfvwv8GTVmSz+0txtW4O20/VMuMSULqAgW9ChLoNnpppsiEuwOL/KgFknOMgegM+QqdBU7OyB4c0XBxCEfoe8/+Z8lNlB1+UShwh6aJHjiUvR9U537ebtniMWyj0Nb93f53YQGsh29gbuqDNzIWPePFuSDBhKTRhKEHdBfol11YCgyNs2MXkdTUQ60G+DMtdAOM32mJEXi3n2yQWtnWdnsxApqZ4LLlEO1J7FJ0/6sXEd5Y8aQBoYPdE5RdGjo1gjxpp0tL3WxXisG1TlHdgNShKzO5CT4pUFD5qWqhbzlwDM8eI3xFS39I75bVeoPrQ9jn0MlBs8JQJIEFbn6HDW7G/hezMoQPwH2Sn2ET9/XH9axt3U7Wrqf9I10FuFMdBZ4hD62kml94WfxiC40sCNMgYW/zPYGSUQLXfzWImyFz32K8MTQErWVoiaDz8bx5rjLa+zmmwchhZCX4YdBh4BMIOy793YYx6/jLGUBhKuIyHGYREUF/ZlZg5rYssbKUP9G4TdzPix3dGCBgdtrsYwUZ79Odu/SjwV8uzOfcU2p9E0j1GjEK5BnfwY7b5ZpXeagBze34kDU3U89Jnmwc08eu0S+olv7/4iGn+gvQZOvM9s0CShhMI7I=" - distributions: sdist bdist_wheel - on: - branch: master - skip_cleanup: true -- provider: pypi - user: devopshq - password: - secure: "u7kqhFT32wLvGvdXLRg3RtTdfGcSzfFWTQb0p1cdRHWBm/771Q2H4bfvwv8GTVmSz+0txtW4O20/VMuMSULqAgW9ChLoNnpppsiEuwOL/KgFknOMgegM+QqdBU7OyB4c0XBxCEfoe8/+Z8lNlB1+UShwh6aJHjiUvR9U537ebtniMWyj0Nb93f53YQGsh29gbuqDNzIWPePFuSDBhKTRhKEHdBfol11YCgyNs2MXkdTUQ60G+DMtdAOM32mJEXi3n2yQWtnWdnsxApqZ4LLlEO1J7FJ0/6sXEd5Y8aQBoYPdE5RdGjo1gjxpp0tL3WxXisG1TlHdgNShKzO5CT4pUFD5qWqhbzlwDM8eI3xFS39I75bVeoPrQ9jn0MlBs8JQJIEFbn6HDW7G/hezMoQPwH2Sn2ET9/XH9axt3U7Wrqf9I10FuFMdBZ4hD62kml94WfxiC40sCNMgYW/zPYGSUQLXfzWImyFz32K8MTQErWVoiaDz8bx5rjLa+zmmwchhZCX4YdBh4BMIOy793YYx6/jLGUBhKuIyHGYREUF/ZlZg5rYssbKUP9G4TdzPix3dGCBgdtrsYwUZ79Odu/SjwV8uzOfcU2p9E0j1GjEK5BnfwY7b5ZpXeagBze34kDU3U89Jnmwc08eu0S+olv7/4iGn+gvQZOvM9s0CShhMI7I=" - distributions: sdist bdist_wheel - on: - branch: develop - skip_cleanup: true - -after_script: - - echo "Deploy to PyPI finished." - -env: - global: - - secure: "d67oNhecPXRl0HiTfcI29PKUPLo657VMaCLAt7BAyPk3LXLSgkz9X0fpz0I2dq3DrtREMrKNcoHjFY5x5FoMemYJfg/93PPk8hRbu3XWKiGVHVdj8xwu2hJSSxl/enmEs25qAs1c3Rss+9CyZ7rP4cY7J0hiVymgBZKWSG/8UL6P2D3iDpvxf3EKapPRrf8FP6l3Nmwk3iVlFrwzq4koBUlC2SEd2Hr1uL+iGHuOdNlH4bksMc0nIeC6f905YJ1x3hHMg46h75RkWBrPmj9nn5eAS3jva8Fg9WrDiieF9JrL6GZ0SpektgDS+SjZuinAgjmDZQOogLZtg8+cyDVSud5x0d1K1OQBatNb1ZWVDqLld2E6/6Pxp1x1Bg52OpQvxL23HZkg7XZtxhZzhOEI4f/TvG4123voygjQXSZXVrdqzLwwAxuGqtxym5lcAe7tRllmnXs/Nty/M5ICQhVU56pxoMvSjwwXySBdP69TC6YBNtLbDY9ySImd3lJpbJcc0ftuo2aU6FJjXMNFTuTjmVfw3g/fKvPcjVsTbHqVWOhyfI/smAJxgJx/ugEm9PFk7i2rIc3JkYtW1AyoIXGJBeytvZRlyDz9ulowYSEPCtKL6Z9tqMGPHUntKwwHKbRTBIGsDhnRLN095MYbb7d7aOagBIfOGSaxY3e/mpyXwQI=" diff --git a/requirements-travis.txt b/requirements-travis.txt deleted file mode 100644 index cd87849..0000000 --- a/requirements-travis.txt +++ /dev/null @@ -1,8 +0,0 @@ -coverage -codacy-coverage -pytest==3.1.2 -HTTPretty -pytest_httpretty -pypandoc -requests_ntlm -flake8 From 6b0e16a640ed581bf901409e402b01ede114f62e Mon Sep 17 00:00:00 2001 From: allburov Date: Wed, 18 May 2022 17:33:23 +0700 Subject: [PATCH 5/6] Run tests on github actions --- .github/workflows/tests.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..21d0f43 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: Tests + +on: + - push + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.7' + - name: Tests + run: | + python ./setup.py test From 7c3c703084fe4f7a9a3b952414e408b64b0a2266 Mon Sep 17 00:00:00 2001 From: allburov Date: Wed, 18 May 2022 17:41:58 +0700 Subject: [PATCH 6/6] Add release workflow - publish the package to pypi --- .github/workflows/release.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..22a9347 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.7' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }}