diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5e06ad2f3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: master diff --git a/.github/workflows/jira_server_ci.yml b/.github/workflows/jira_server_ci.yml new file mode 100644 index 000000000..400cbb1c7 --- /dev/null +++ b/.github/workflows/jira_server_ci.yml @@ -0,0 +1,70 @@ +name: Jira Server CI + +on: + # Trigger the workflow on push or pull request, + # but only for the master branch + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + name: ${{ matrix.os }} / ${{ matrix.python-version }} + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [Ubuntu] + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@master + - name: Start Jira docker instance + run: docker run -dit -p 2990:2990 --name jira addono/jira-software-standalone + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: Setup the Pip cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: >- + ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('tox.ini') }}-${{ + hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install Dependencies + run: | + sudo apt-get update; sudo apt-get install gcc libkrb5-dev + python -m pip install --upgrade pip + python -m pip install --upgrade tox tox-gh-actions + + - name: Lint with tox + run: tox -e lint + + - name: Test with tox + run: tox + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.0.15 + with: + file: ./coverage.xml + name: ${{ runner.os }}-${{ matrix.python-version }} + + - name: Run tox pkg + run: tox -e pkg + + - name: Make docs + run: tox -e docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f59ad2b7..4bfcb0db2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ --- repos: - repo: https://github.com/python/black - rev: 19.3b0 + rev: 21.4b2 hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v3.4.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace @@ -20,21 +20,15 @@ repos: - id: check-yaml files: .*\.(yaml|yml)$ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + rev: 3.9.1 hooks: - id: flake8 - additional_dependencies: - - flake8-black - repo: https://github.com/adrienverge/yamllint.git - rev: v1.17.0 + rev: v1.26.1 hooks: - id: yamllint files: \.(yaml|yml)$ - - repo: https://github.com/openstack-dev/bashate.git - rev: 0.6.0 - hooks: - - id: bashate - repo: https://github.com/pre-commit/mirrors-mypy.git - rev: v0.730 + rev: v0.812 hooks: - id: mypy diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6e90c8aa7..000000000 --- a/.travis.yml +++ /dev/null @@ -1,76 +0,0 @@ ---- -language: python -dist: xenial -# Build only commits on master and release tags for the "Build pushed branches" feature. -# This prevents building twice on PRs originating from our repo ("Build pushed pull requests)". -# See: -# - https://github.com/travis-ci/travis-ci/issues/1147 -# - https://docs.travis-ci.com/user/pull-requests/#double-builds-on-pull-requests -branches: - only: - - master - - /^\d+\.\d+(\.\d+)?(-\S*)?$/ - -cache: - bundler: true - pip: true - directories: - - $HOME/.cache/pre-commit - - $HOME/.pre-commit - - $HOME/.rvm - - $HOME/Library/Caches/Homebrew -os: - - linux -stages: - - maintenance - - test - - deploy -before_install: - - pip install --upgrade tox tox-venv - - rm -rf .tox -notifications: - email: - - pycontribs@googlegroups.com -jobs: - include: - - stage: maintenance - script: - - python -m tox -e maintenance - if: type = cron - - script: - - python -m tox - env: TOXENV="lint,docs,pkg,py37" - python: "3.7" - after_success: - - bash <(curl -s https://codecov.io/bash) -e py37 - - script: python -m tox - python: "3.6" - env: TOXENV=py36 PYTHON='3.6' - after_success: - - bash <(curl -s https://codecov.io/bash) -e TOXENV - - script: python -m tox - python: "3.5" - env: TOXENV=py35 - after_success: - - bash <(curl -s https://codecov.io/bash) -e TOXENV - - stage: deploy - script: - - tox -e upload - if: tag IS present AND type != cron - deploy: - - provider: releases - api_key: - secure: YJGigSNYOzMJqs23gIZLFxiVYRqHdV4WsTZmRVosishD2QIaDlTwJma7k6Y5eMPVNdLpqo7Tq6bt7xkJAz/dcr3UO35T/Y0tiRFFW3sd6IOB6ELwSwPhSeHoyUMvZtKyDTl+9tOfeZusFZuCc+mBLQcG+S2NzEaeyrQ6n5hTT/8FGBP91FOq9l5q2gYbmACZ9MisDIjZkTHNYih36ComnZ9QHC91jHKcSuHmOfWWX3GneDVFtuPhF2vjaLQrz8IFtWGW5Sfe35yDYlVQRH+NFxzSJ2zDuT5j8cRgwXjGout78umtMsqAn+zv1Ws/MUNKMTEtONsACndMpGCkuB6Nifl/KcGj5kD7V4PO/gE0ecr830qAwJxSVB7xk6rl797nMxGbr4w2DWQ/iDdHDTlPAEzbLBMLrMRgPxzKPgg5CNxxjT1cHoBNcFPp6gaf017w4XOVUgp/zxXeCg7iGiNJj7z2t8/m9eYVNNlNRPcodN6BjSjPqkYxC3ZMVCI5KsRXbHmR0zOWbPdcRjrY/IgbiTqX09sHotHw5GThP6YTMbienC4h93cdx6MEfX656W6XMOxpC+MjWtYuV8QlfMEJFlstOnA86MVLcmbl+4A6FHuvlQMdDtP9KsKdKIf/4juGhNEFir32P1rUe8J1abmjwXmDkHVbli0SDqaFtB5gyCc= - file_glob: true - file: - - dist/* - - ChangeLog - skip_cleanup: true - on: - tags: true - repo: pycontribs/jira - branch: master -env: - global: - - secure: "pGQGM5YmHvOgaKihOyzb3k6bdqLQnZQ2OXO9QrfXlXwtop3zvZQi80Q+01l230x2psDWlwvqWTknAjAt1w463fYXPwpoSvKVCsLSSbjrf2l56nrDqnoir+n0CBy288+eIdaGEfzcxDiuULeKjlg08zrqjcjLjW0bDbBrlTXsb5U=" - - PIP_DISABLE_PIP_VERSION_CHECK=1 diff --git a/.yamllint b/.yamllint index 028d683b9..dcb54406e 100644 --- a/.yamllint +++ b/.yamllint @@ -2,15 +2,15 @@ extends: default rules: - braces: {max-spaces-inside: 1, level: error} - brackets: {max-spaces-inside: 1, level: error} - colons: {max-spaces-after: -1, level: error} - commas: {max-spaces-after: -1, level: error} + braces: { max-spaces-inside: 1, level: error } + brackets: { max-spaces-inside: 1, level: error } + colons: { max-spaces-after: -1, level: error } + commas: { max-spaces-after: -1, level: error } comments: disable comments-indentation: disable document-start: disable - empty-lines: {max: 3, level: error} - hyphens: {level: error} + empty-lines: { max: 3, level: error } + hyphens: { level: error } indentation: indent-sequences: consistent # spaces: consistent @@ -23,9 +23,8 @@ rules: allow-non-breakable-words: true allow-non-breakable-inline-mappings: true new-line-at-end-of-file: disable - new-lines: {type: unix} + new-lines: disable trailing-spaces: disable truthy: disable -ignore: - .tox +ignore: .tox diff --git a/MANIFEST.in b/MANIFEST.in index f7afaafa0..3c0f8bf8a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,10 @@ include LICENSE README.rst + +# Exclude what is in these folders prune tests +prune .github + +# Exclude these files +exclude package-lock.json +recursive-exclude * *.py[co] +recursive-exclude * __pycache__ diff --git a/Makefile b/Makefile index b21499b7c..b19faa59e 100644 --- a/Makefile +++ b/Makefile @@ -72,10 +72,9 @@ dist: $(PREFIX)python setup.py sdist bdist_wheel prepare: - @pyenv install -s 3.5.7 @pyenv install -s 3.6.9 @pyenv install -s 3.7.4 - @pyenv local 3.5.7 3.6.9 3.7.4 + @pyenv local 3.6.9 3.7.4 @echo "INFO: === Preparing to run for package:$(PACKAGE_NAME) platform:$(PLATFORM) py:$(PYTHON_VERSION) dir:$(DIR) ===" #if [ -f ${HOME}/testspace/testspace ]; then ${HOME}/testspace/testspace config url ${TESTSPACE_TOKEN}@pycontribs.testspace.com/jira/tests ; fi; diff --git a/README.rst b/README.rst index c1aa693cb..5bcdb02a9 100644 --- a/README.rst +++ b/README.rst @@ -3,41 +3,41 @@ Jira Python Library =================== .. image:: https://img.shields.io/pypi/v/jira.svg - :target: https://pypi.python.org/pypi/jira/ + :target: https://pypi.python.org/pypi/jira/ .. image:: https://img.shields.io/pypi/l/jira.svg - :target: https://pypi.python.org/pypi/jira/ + :target: https://pypi.python.org/pypi/jira/ .. image:: https://img.shields.io/pypi/wheel/jira.svg - :target: https://pypi.python.org/pypi/jira/ + :target: https://pypi.python.org/pypi/jira/ .. image:: https://img.shields.io/github/issues/pycontribs/jira.svg - :target: https://github.com/pycontribs/jira/issues + :target: https://github.com/pycontribs/jira/issues .. image:: https://img.shields.io/badge/irc-%23pycontribs-blue - :target: irc:///#pycontribs + :target: irc:///#pycontribs ------------ .. image:: https://readthedocs.org/projects/jira/badge/?version=master - :target: https://jira.readthedocs.io/ + :target: https://jira.readthedocs.io/ .. image:: https://travis-ci.com/pycontribs/jira.svg?branch=master - :target: https://travis-ci.com/pycontribs/jira + :target: https://travis-ci.com/pycontribs/jira .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black :alt: Python Black Code Style .. image:: https://codecov.io/gh/pycontribs/jira/branch/master/graph/badge.svg - :target: https://codecov.io/gh/pycontribs/jira + :target: https://codecov.io/gh/pycontribs/jira .. image:: https://img.shields.io/bountysource/team/pycontribs/activity.svg - :target: https://www.bountysource.com/teams/pycontribs/issues?tracker_ids=3650997 + :target: https://www.bountysource.com/teams/pycontribs/issues?tracker_ids=3650997 .. image:: https://requires.io/github/pycontribs/jira/requirements.svg?branch=master - :target: https://requires.io/github/pycontribs/jira/requirements/?branch=master - :alt: Requirements Status + :target: https://requires.io/github/pycontribs/jira/requirements/?branch=master + :alt: Requirements Status This library eases the use of the Jira REST API from Python and it has been used in production for years. @@ -54,14 +54,14 @@ Feeling impatient? I like your style. .. code-block:: python - from jira import JIRA + from jira import JIRA - jira = JIRA('https://jira.atlassian.com') + jira = JIRA('https://jira.atlassian.com') - issue = jira.issue('JRA-9') - print(issue.fields.project.key) # 'JRA' - print(issue.fields.issuetype.name) # 'New Feature' - print(issue.fields.reporter.displayName) # 'Mike Cannon-Brookes [Atlassian]' + issue = jira.issue('JRA-9') + print(issue.fields.project.key) # 'JRA' + print(issue.fields.issuetype.name) # 'New Feature' + print(issue.fields.reporter.displayName) # 'Mike Cannon-Brookes [Atlassian]' Installation @@ -102,18 +102,25 @@ Setup * Fork_ repo * Keep it sync_'ed while you are developing * Install pyenv_ -* Install `Atlassian Jira Server`_ for testing - - make install-sdk -* pip install jira[test] -* Start up Jira Server - - atlas-run-standalone -* Test your changes - - make test +* develop and test + * Launch docker jira server + - ``docker run -dit -p 2990:2990 --name jira addono/jira-software-standalone`` + * Lint + - ``tox -e lint`` + - Note: Windows users trying to run locally will need to: + - Comment out the ``npm`` commands in the ``lint`` environment before running the ``lint`` environment + - Run ``npm install`` manually + - Run ``cspell "**" --unique`` manually - this relies on the ``cspell.json`` to check the right files + * Run tests + - ``tox`` + * Run tests for one env only + - ``tox -e py37`` + * Build and publish with TWINE + - ``tox -e upload`` .. _Fork: https://help.github.com/articles/fork-a-repo/ .. _sync: https://help.github.com/articles/syncing-a-fork/ .. _pyenv: https://amaral.northwestern.edu/resources/guides/pyenv-tutorial -.. _`Atlassian Jira Server`: https://www.atlassian.com/software/jira/download Credits diff --git a/cspell.json b/cspell.json index 7bef05387..18ff1b2a8 100644 --- a/cspell.json +++ b/cspell.json @@ -12,6 +12,7 @@ "bspeakmon", "capsys", "categorised", + "Codecov", "conda", "cygwin", "dae", @@ -66,6 +67,7 @@ "k", "ky", "kzh", + "libkrb", "lqqy", "luk", "makotemplate", @@ -182,9 +184,16 @@ "/I18NSPHINXOPTS/" ], "ignorePaths": [ - "docs/build", + "__pycache__", + ".eggs", ".tox", - ".eggs" + "*.egg-info", + "*.egg", + "*.pyc", + "dist/**", + "docs/_build/**", + "node_modules/**", + "package-lock.json" ], "ignoreWords": [ "AACCOUNTID", diff --git a/docs/contributing.rst b/docs/contributing.rst index 1c4e9e635..a07719933 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -63,7 +63,7 @@ Issues and Feature Requests * How to recreate the bug. * If relevant, including the versions of your: - * Python interpreter (3.5, etc) + * Python interpreter (3.6, etc) * jira-python * Operating System and Version (Windows 7, OS X 10.10, Ubuntu 14.04, etc.) * IPython if using jirashell diff --git a/docs/installation.rst b/docs/installation.rst index bd1f1bf61..766998e02 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -28,7 +28,7 @@ Source packages are also available at PyPI: Dependencies ============ -Python 3.5+ is required. +Python >3.5 is required. - :py:mod:`requests` - `python-requests `_ library handles the HTTP business. Usually, the latest version available at time of release is the minimum version required; at this writing, that version is 1.2.0, but any version >= 1.0.0 should work. - :py:mod:`requests-oauthlib` - Used to implement OAuth. The latest version as of this writing is 0.3.3. diff --git a/jira/client.py b/jira/client.py index 57b2d1839..8e0f30d63 100644 --- a/jira/client.py +++ b/jira/client.py @@ -464,7 +464,7 @@ def __init__( if self._options["server"].endswith("/"): self._options["server"] = self._options["server"][:-1] - context_path = urlparse(self._options["server"]).path + context_path = urlparse(self.server_url).path if len(context_path) > 0: self._options["context_path"] = context_path @@ -481,9 +481,8 @@ def __init__( self._create_kerberos_session(timeout, kerberos_options=kerberos_options) elif auth: self._create_cookie_auth(auth, timeout) - validate = ( - True - ) # always log in for cookie based auth, as we need a first request to be logged in + # always log in for cookie based auth, as we need a first request to be logged in + validate = True else: verify = self._options["verify"] self._session = ResilientSession(timeout=timeout) @@ -532,6 +531,11 @@ def __init__( for name in f["clauseNames"]: self._fields[name] = f["id"] + @property + def server_url(self): + """Return the server url""" + return self._options["server"] + def _create_cookie_auth(self, auth, timeout): self._session = ResilientSession(timeout=timeout) self._session.auth = JiraCookieAuth(self._session, self.session, auth) @@ -734,7 +738,7 @@ def _get_items_from_page(self, item_type, items_key, resource): def client_info(self): """Get the server this client is connected to.""" - return self._options["server"] + return self.server_url # Universal resource loading @@ -799,7 +803,7 @@ def set_application_property(self, key, value): :param value: value to assign to the property :type value: str """ - url = self._options["server"] + "/rest/api/latest/application-properties/" + key + url = self._get_latest_url("application-properties/" + key) payload = {"id": key, "value": value} return self._session.put(url, data=json.dumps(payload)) @@ -813,7 +817,7 @@ def applicationlinks(self, cached=True): return self._applicationlinks # url = self._options['server'] + '/rest/applinks/latest/applicationlink' - url = self._options["server"] + "/rest/applinks/latest/listApplicationlinks" + url = self.server_url + "/rest/applinks/latest/listApplicationlinks" r = self._session.get(url) @@ -1223,7 +1227,7 @@ def add_group(self, groupname): :return: Boolean - True if successful. :rtype: bool """ - url = self._options["server"] + "/rest/api/latest/group" + url = self._get_latest_url("group") # implementation based on # https://docs.atlassian.com/jira/REST/ondemand/#d2e5173 @@ -1248,7 +1252,7 @@ def remove_group(self, groupname): """ # implementation based on # https://docs.atlassian.com/jira/REST/ondemand/#d2e5173 - url = self._options["server"] + "/rest/api/latest/group" + url = self._get_latest_url("group") x = {"groupname": groupname} self._session.delete(url, params=x) return True @@ -1403,7 +1407,7 @@ def supports_service_desk(self): :rtype: bool """ - url = self._options["server"] + "/rest/servicedeskapi/info" + url = self.server_url + "/rest/servicedeskapi/info" headers = {"X-ExperimentalApi": "opt-in"} try: r = self._session.get(url, headers=headers) @@ -1421,7 +1425,7 @@ def create_customer(self, email, displayName): :rtype: Customer """ - url = self._options["server"] + "/rest/servicedeskapi/customer" + url = self.server_url + "/rest/servicedeskapi/customer" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post( url, @@ -1441,7 +1445,7 @@ def service_desks(self): :rtype: List[ServiceDesk] """ - url = self._options["server"] + "/rest/servicedeskapi/servicedesk" + url = self.server_url + "/rest/servicedeskapi/servicedesk" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) print(r_json) @@ -1501,7 +1505,7 @@ def create_customer_request(self, fields=None, prefetch=True, **fieldargs): elif isinstance(p, str): data["requestTypeId"] = self.request_type_by_name(service_desk, p).id - url = self._options["server"] + "/rest/servicedeskapi/request" + url = self.server_url + "/rest/servicedeskapi/request" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post(url, headers=headers, data=json.dumps(data)) @@ -1559,13 +1563,13 @@ def createmeta( params["expand"] = expand return self._get_json("issue/createmeta", params) - def _get_user_accountid(self, user): - """Internal method for translating an user to an accountId.""" + def _get_user_key(self, user): + """Internal method for translating an user (str) to an key.""" try: - accountId = self.search_users(user, maxResults=1)[0].accountId + key = self.search_users(user, maxResults=1)[0].key except Exception as e: raise JIRAError(e) - return accountId + return key # non-resource @translate_resource_args @@ -1579,13 +1583,8 @@ def assign_issue(self, issue, assignee): :rtype: bool """ - url = ( - self._options["server"] - + "/rest/api/latest/issue/" - + str(issue) - + "/assignee" - ) - payload = {"accountId": self._get_user_accountid(assignee)} + url = self._get_latest_url("issue/{}/assignee".format(str(issue))) + payload = {"name": self._get_user_key(assignee)} # 'key' and 'name' are deprecated in favor of accountId r = self._session.put(url, data=json.dumps(payload)) raise_on_error(r) @@ -1599,7 +1598,7 @@ def comments(self, issue): :type issue: str :rtype: List[Comment] """ - r_json = self._get_json("issue/" + str(issue) + "/comment") + r_json = self._get_json("issue/{}/comment".format(str(issue))) comments = [ Comment(self._options, self._session, raw_comment_json) @@ -1753,7 +1752,7 @@ def add_remote_link( # check if the link comes from one of the configured application links for x in applicationlinks: - if x["application"]["displayUrl"] == self._options["server"]: + if x["application"]["displayUrl"] == self.server_url: data["globalId"] = "appId=%s&issueId=%s" % ( x["application"]["id"], destination.raw["id"], @@ -1918,7 +1917,7 @@ def add_watcher(self, issue, watcher): """Add a user to an issue's watchers list. :param issue: ID or key of the issue affected - :param watcher: username of the user to add to the watchers list + :param watcher: key of the user to add to the watchers list """ url = self._get_url("issue/" + str(issue) + "/watchers") self._session.post(url, data=json.dumps(watcher)) @@ -1928,11 +1927,12 @@ def remove_watcher(self, issue, watcher): """Remove a user from an issue's watch list. :param issue: ID or key of the issue affected - :param watcher: accountId of the user to remove from the watchers list + :param watcher: key of the user to remove from the watchers list :rtype: Response """ url = self._get_url("issue/" + str(issue) + "/watchers") - params = {"accountId": watcher} + # https://docs.atlassian.com/software/jira/docs/api/REST/8.13.6/#api/2/issue-removeWatcher + params = {"username": watcher} result = self._session.delete(url, params=params) return result @@ -2145,7 +2145,7 @@ def issue_type_by_name(self, name): return issue_type def request_types(self, service_desk): - """ Returns request types supported by a service desk instance. + """Returns request types supported by a service desk instance. :param service_desk: The service desk instance. :type service_desk: ServiceDesk :rtype: List[RequestType] @@ -2153,7 +2153,7 @@ def request_types(self, service_desk): if hasattr(service_desk, "id"): service_desk = service_desk.id url = ( - self._options["server"] + self.server_url + "/rest/servicedeskapi/servicedesk/%s/requesttype" % service_desk ) headers = {"X-ExperimentalApi": "opt-in"} @@ -2585,10 +2585,12 @@ def statuses(self): return statuses def status(self, id): - # type: (str) -> Status """Get a status Resource from the server. :param id: ID of the status resource to get + :type id: str + + :rtype: Status """ return self._find_for_resource(Status, id) @@ -2990,7 +2992,7 @@ def session(self): def kill_session(self): """Destroy the session of the current authenticated user.""" - url = self._options["server"] + "/rest/auth/latest/session" + url = self.server_url + "/rest/auth/latest/session" return self._session.delete(url) # Websudo @@ -3002,12 +3004,12 @@ def kill_websudo(self): :rtype: Optional[Any] """ if self.deploymentType != "Cloud": - url = self._options["server"] + "/rest/auth/1/websudo" + url = self.server_url + "/rest/auth/1/websudo" return self._session.delete(url) # Utilities def _create_http_basic_session(self, username, password, timeout=None): - """ Creates a basic http session. + """Creates a basic http session. :param username: Username for the session :type username: str @@ -3097,7 +3099,8 @@ def _set_avatar(self, params, url, avatar): return self._session.put(url, params=params, data=json.dumps(data)) def _get_url(self, path, base=JIRA_BASE_URL): - """ Returns the full url based on Jira base url and the path provided + """Returns the full url based on Jira base url and the path provided. + Using the API version specified during the __init__. :param path: The subpath desired. :type path: str @@ -3112,6 +3115,23 @@ def _get_url(self, path, base=JIRA_BASE_URL): options.update({"path": path}) return base.format(**options) + def _get_latest_url(self, path, base=JIRA_BASE_URL): + """Returns the full url based on Jira base url and the path provided. + Using the latest API endpoint. + + :param path: The subpath desired. + :type path: str + :param base: The base url which should be prepended to the path + :type base: Optional[str] + + :return Fully qualified URL + :rtype: str + + """ + options = self._options.copy() + options.update({"path": path, "rest_api_version": "latest"}) + return base.format(**options) + def _get_json(self, path, params=None, base=JIRA_BASE_URL): """Get the json for a given path and params. @@ -3195,7 +3215,7 @@ def rename_user(self, old_user, new_user): """ if self._version > (6, 0, 0): - url = self._options["server"] + "/rest/api/latest/user" + url = self._get_latest_url("user") payload = {"name": new_user} params = {"username": old_user} @@ -3220,7 +3240,7 @@ def delete_user(self, username): """ - url = self._options["server"] + "/rest/api/latest/user/?username=%s" % username + url = self._get_latest_url("user/?username=%s" % username) r = self._session.delete(url) if 200 <= r.status_code <= 299: @@ -3273,7 +3293,7 @@ def deactivate_user(self, username): logging.error("Error Deactivating %s: %s" % (username, e)) raise JIRAError("Error Deactivating %s: %s" % (username, e)) else: - url = self._options["server"] + "/secure/admin/user/EditUser.jspa" + url = self.server_url + "/secure/admin/user/EditUser.jspa" self._options["headers"][ "Content-Type" ] = "application/x-www-form-urlencoded; charset=UTF-8" @@ -3317,7 +3337,7 @@ def reindex(self, force=False, background=True): else: indexingStrategy = "stoptheworld" - url = self._options["server"] + "/secure/admin/jira/IndexReIndex.jspa" + url = self.server_url + "/secure/admin/jira/IndexReIndex.jspa" r = self._session.get(url, headers=self._options["headers"]) if r.status_code == 503: @@ -3349,11 +3369,11 @@ def reindex(self, force=False, background=True): def backup(self, filename="backup.zip", attachments=False): """Will call jira export to backup as zipped xml. Returning with success does not mean that the backup process finished.""" if self.deploymentType == "Cloud": - url = self._options["server"] + "/rest/backup/1/export/runbackup" + url = self.server_url + "/rest/backup/1/export/runbackup" payload = json.dumps({"cbAttachments": attachments}) self._options["headers"]["X-Requested-With"] = "XMLHttpRequest" else: - url = self._options["server"] + "/secure/admin/XmlBackup.jspa" + url = self.server_url + "/secure/admin/XmlBackup.jspa" payload = {"filename": filename} try: r = self._session.post(url, headers=self._options["headers"], data=payload) @@ -3372,9 +3392,7 @@ def backup_progress(self): """ epoch_time = int(time.time() * 1000) if self.deploymentType == "Cloud": - url = ( - self._options["server"] + "/rest/obm/1.0/getprogress?_=%i" % epoch_time - ) + url = self.server_url + "/rest/obm/1.0/getprogress?_=%i" % epoch_time else: logging.warning("This functionality is not available in Server version") return None @@ -3417,7 +3435,7 @@ def backup_download(self, filename=None): return None remote_file = self.backup_progress()["fileName"] local_file = filename or remote_file - url = self._options["server"] + "/webdav/backupmanager/" + remote_file + url = self.server_url + "/webdav/backupmanager/" + remote_file try: logging.debug("Writing file to %s" % local_file) with open(local_file, "wb") as file: @@ -3468,7 +3486,7 @@ def delete_project(self, pid): if hasattr(pid, "id"): pid = pid.id - url = self._options["server"] + "/rest/api/2/project/%s" % pid + url = self._get_url("project/%s" % pid) r = self._session.delete(url) if r.status_code == 403: raise JIRAError("Not enough permissions to delete project") @@ -3477,7 +3495,7 @@ def delete_project(self, pid): return r.ok def _gain_sudo_session(self, options, destination): - url = self._options["server"] + "/secure/admin/WebSudoAuthenticate.jspa" + url = self.server_url + "/secure/admin/WebSudoAuthenticate.jspa" if not self._session.auth: self._session.auth = get_netrc_auth(url) @@ -3501,7 +3519,7 @@ def _gain_sudo_session(self, options, destination): @lru_cache(maxsize=None) def templates(self): - url = self._options["server"] + "/rest/project-templates/latest/templates" + url = self.server_url + "/rest/project-templates/latest/templates" r = self._session.get(url) data = json_loads(r) @@ -3517,7 +3535,7 @@ def templates(self): @lru_cache(maxsize=None) def permissionschemes(self): - url = self._options["server"] + "/rest/api/3/permissionscheme" + url = self._get_url("permissionscheme") r = self._session.get(url) data = json_loads(r)["permissionSchemes"] @@ -3527,7 +3545,7 @@ def permissionschemes(self): @lru_cache(maxsize=None) def issuesecurityschemes(self): - url = self._options["server"] + "/rest/api/3/issuesecurityschemes" + url = self._get_url("issuesecurityschemes") r = self._session.get(url) data = json_loads(r)["issueSecuritySchemes"] @@ -3537,7 +3555,7 @@ def issuesecurityschemes(self): @lru_cache(maxsize=None) def projectcategories(self): - url = self._options["server"] + "/rest/api/3/projectCategory" + url = self._get_url("projectCategory") r = self._session.get(url) data = json_loads(r) @@ -3547,7 +3565,7 @@ def projectcategories(self): @lru_cache(maxsize=None) def avatars(self, entity="project"): - url = self._options["server"] + "/rest/api/3/avatar/%s/system" % entity + url = self._get_url("avatar/%s/system" % entity) r = self._session.get(url) data = json_loads(r)["system"] @@ -3557,7 +3575,7 @@ def avatars(self, entity="project"): @lru_cache(maxsize=None) def notificationschemes(self): # TODO(ssbarnea): implement pagination support - url = self._options["server"] + "/rest/api/3/notificationscheme" + url = self._get_url("notificationscheme") r = self._session.get(url) data = json_loads(r) @@ -3566,7 +3584,7 @@ def notificationschemes(self): @lru_cache(maxsize=None) def screens(self): # TODO(ssbarnea): implement pagination support - url = self._options["server"] + "/rest/api/3/screens" + url = self._get_url("screens") r = self._session.get(url) data = json_loads(r) @@ -3575,7 +3593,7 @@ def screens(self): @lru_cache(maxsize=None) def workflowscheme(self): # TODO(ssbarnea): implement pagination support - url = self._options["server"] + "/rest/api/3/workflowschemes" + url = self._get_url("workflowschemes") r = self._session.get(url) data = json_loads(r) @@ -3584,7 +3602,7 @@ def workflowscheme(self): @lru_cache(maxsize=None) def workflows(self): # TODO(ssbarnea): implement pagination support - url = self._options["server"] + "/rest/api/3/workflow" + url = self._get_url("workflow") r = self._session.get(url) data = json_loads(r) @@ -3592,7 +3610,7 @@ def workflows(self): def delete_screen(self, id): - url = self._options["server"] + "/rest/api/3/screens/%s" % id + url = self._get_url("screens/%s" % id) r = self._session.delete(url) data = json_loads(r) @@ -3602,7 +3620,7 @@ def delete_screen(self, id): def delete_permissionscheme(self, id): - url = self._options["server"] + "/rest/api/3/permissionscheme/%s" % id + url = self._get_url("permissionscheme/%s" % id) r = self._session.delete(url) data = json_loads(r) @@ -3631,7 +3649,7 @@ def create_project( :type: str :param name: If not specified it will use the key value. :type name: Optional[str] - :param assignee: accountId of the lead, if not specified it will use current user. + :param assignee: key of the lead, if not specified it will use current user. :type assignee: Optional[str] :param type: Determines the type of project should be created. :type ptype: Optional[str] @@ -3646,7 +3664,7 @@ def create_project( template_key = None if assignee is None: - assignee = self.current_user("accountId") + assignee = self.current_user() if name is None: name = key @@ -3682,8 +3700,12 @@ def create_project( # https://jira.atlassian.com/browse/JRASERVER-59658 # preference list for picking a default template if not template_name: - template_key = "com.pyxis.greenhopper.jira:gh-simplified-basic" + # https://confluence.atlassian.com/jirakb/creating-projects-via-rest-api-in-jira-963651978.html + template_key = ( + "com.pyxis.greenhopper.jira:basic-software-development-template" + ) + # https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-projects/#api-rest-api-2-project-get # template_keys = [ # "com.pyxis.greenhopper.jira:gh-simplified-agility-kanban", # "com.pyxis.greenhopper.jira:gh-simplified-agility-scrum", @@ -3743,7 +3765,8 @@ def create_project( "key": key, "projectTypeKey": ptype, "projectTemplateKey": template_key, - "leadAccountId": assignee, + "lead": assignee, + # "leadAccountId": assignee, "assigneeType": "PROJECT_LEAD", "description": "", # "avatarId": 13946, @@ -3756,7 +3779,7 @@ def create_project( if projectCategory: payload["categoryId"] = int(projectCategory) - url = self._options["server"] + "/rest/api/3/project" + url = self._get_url("project") r = self._session.post(url, data=json.dumps(payload)) r.raise_for_status() @@ -3806,7 +3829,7 @@ def add_user( fullname = username # TODO(ssbarnea): default the directoryID to the first directory in jira instead # of 1 which is the internal one. - url = self._options["server"] + "/rest/api/latest/user" + url = self._get_latest_url("user") # implementation based on # https://docs.atlassian.com/jira/REST/ondemand/#d2e5173 @@ -3847,7 +3870,7 @@ def add_user_to_group(self, username, group): :return: json response from Jira server for success or a value that evaluates as False in case of failure. :rtype: Union[bool,Dict[str,Any]] """ - url = self._options["server"] + "/rest/api/latest/group/user" + url = self._get_latest_url("group/user") x = {"groupname": group} y = {"name": username} @@ -3865,7 +3888,7 @@ def remove_user_from_group(self, username, groupname): :param username: The user to remove from the group. :param groupname: The group that the user will be removed from. """ - url = self._options["server"] + "/rest/api/latest/group/user" + url = self._get_latest_url("group/user") x = {"groupname": groupname, "username": username} self._session.delete(url, params=x) @@ -3881,7 +3904,7 @@ def role(self): """ # https://developer.atlassian.com/cloud/jira/platform/rest/v3/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest%2F&utm_medium=302#api-rest-api-3-role-get - url = self._options["server"] + "/rest/api/latest/role" + url = self._get_latest_url("role") r = self._session.get(url) return json_loads(r) @@ -3890,7 +3913,7 @@ def role(self): # Experimental support for iDalko Grid, expect API to change as it's using private APIs currently # https://support.idalko.com/browse/IGRID-1017 def get_igrid(self, issueid, customfield, schemeid): - url = self._options["server"] + "/rest/idalko-igrid/1.0/datagrid/data" + url = self.server_url + "/rest/idalko-igrid/1.0/datagrid/data" if str(customfield).isdigit(): customfield = "customfield_%s" % customfield params = { diff --git a/jira/exceptions.py b/jira/exceptions.py index 6e1b4ee77..c1be44868 100644 --- a/jira/exceptions.py +++ b/jira/exceptions.py @@ -19,7 +19,7 @@ def __init__( response=None, **kwargs ): - """ Creates a JIRAError. + """Creates a JIRAError. :param status_code: Status code for the error. :type status_code: Optional[int] diff --git a/jira/jirashell.py b/jira/jirashell.py index dc413581e..aa1f0221d 100644 --- a/jira/jirashell.py +++ b/jira/jirashell.py @@ -362,7 +362,7 @@ def main(): from IPython.frontend.terminal.embed import InteractiveShellEmbed ip_shell = InteractiveShellEmbed( - banner1="" + banner1="" ) ip_shell("*** Jira shell active; client is in 'jira'." " Press Ctrl-D to exit.") except Exception as e: diff --git a/jira/resources.py b/jira/resources.py index 6464dd449..3725e4320 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -235,7 +235,7 @@ def find(self, id, params=None): self._load(url, params=params) def _get_url(self, path): - """ Gets the url for the specified path. + """Gets the url for the specified path. :type path: str @@ -377,7 +377,7 @@ def delete(self, params=None): return self._session.delete(url=self.self, params=params) def _load(self, url, headers=CaseInsensitiveDict(), params=None, path=None): - """ Load a resource. + """Load a resource. :type url: str :type headers: CaseInsensitiveDict @@ -1092,7 +1092,7 @@ def dict2resource(raw, top=None, options=None, session=None): r"securitylevel/[^/]+$": SecurityLevel, r"status/[^/]+$": Status, r"statuscategory/[^/]+$": StatusCategory, - r"user\?(username|accountId).+$": User, + r"user\?(username|key).+$": User, r"group\?groupname.+$": Group, r"version/[^/]+$": Version, # GreenHopper specific resources diff --git a/make_local_jira_user.py b/make_local_jira_user.py new file mode 100644 index 000000000..084c2db96 --- /dev/null +++ b/make_local_jira_user.py @@ -0,0 +1,52 @@ +"""Attempts to create a test user, +as the empty JIRA instance isn't provisioned with one. +""" +import time + +import requests + +from jira import JIRA +from os import environ + +CI_JIRA_URL = environ["CI_JIRA_URL"] + + +def add_user_to_jira(): + try: + JIRA( + CI_JIRA_URL, + basic_auth=(environ["CI_JIRA_ADMIN"], environ["CI_JIRA_ADMIN_PASSWORD"]), + ).add_user( + environ["CI_JIRA_USER"], + "user@example.com", + password=environ["CI_JIRA_USER_PASSWORD"], + ) + print("user {}".format(environ["CI_JIRA_USER"])) + except Exception as e: + if "username already exists" not in str(e): + raise e + + +if __name__ == "__main__": + start_time = time.time() + timeout_mins = 15 + print( + "waiting for instance of jira to be running, to add a user for CI system:\n timeout = {} mins".format( + timeout_mins + ) + ) + while True: + try: + requests.get(CI_JIRA_URL + "rest/api/2/permissions") + print("JIRA IS REACHABLE") + add_user_to_jira() + break + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as ex: + print( + "encountered {} while waiting for the JiraServer docker".format(str(ex)) + ) + time.sleep(20) + if start_time + 60 * timeout_mins < time.time(): + raise TimeoutError( + "Jira server wasn't reachable within timeout {}".format(timeout_mins) + ) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..6e67bf45e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,661 @@ +{ + "name": "python-jira", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@cspell/dict-aws": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@cspell/dict-aws/-/dict-aws-1.0.14.tgz", + "integrity": "sha512-K21CfB4ZpKYwwDQiPfic2zJA/uxkbsd4IQGejEvDAhE3z8wBs6g6BwwqdVO767M9NgZqc021yAVpr79N5pWe3w==" + }, + "@cspell/dict-bash": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-1.0.12.tgz", + "integrity": "sha512-BOMHVW/m281mqUSJkZ3oiJiUUItLd7QdzpMjm428V9yBYFwIdbds1CeatS7C6kgpI2eBE4RXmy1Hjk/lR63Jew==" + }, + "@cspell/dict-companies": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-1.0.36.tgz", + "integrity": "sha512-Bk9mMJs9spzrtLxZsxBZIK6ukD9REfQYpuTBNJk/IiTViHVQ6ertHAgw1vRVtJAMxViv8dMLNtDyTpEXeaYm7w==" + }, + "@cspell/dict-cpp": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-1.1.38.tgz", + "integrity": "sha512-QqVMxVNYX9XtxzflpJ/888GSyjPU5VeotltsHql1BeEPxhyV27ud9bRKDrBGzCijCK/+MvCxiMZGDpYZqHTjXw==" + }, + "@cspell/dict-cryptocurrencies": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@cspell/dict-cryptocurrencies/-/dict-cryptocurrencies-1.0.10.tgz", + "integrity": "sha512-47ABvDJOkaST/rXipNMfNvneHUzASvmL6K/CbOFpYKfsd0x23Jc9k1yaOC7JAm82XSC/8a7+3Yu+Fk2jVJNnsA==" + }, + "@cspell/dict-csharp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-csharp/-/dict-csharp-1.0.11.tgz", + "integrity": "sha512-nub+ZCiTgmT87O+swI+FIAzNwaZPWUGckJU4GN402wBq420V+F4ZFqNV7dVALJrGaWH7LvADRtJxi6cZVHJKeA==" + }, + "@cspell/dict-css": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-1.0.11.tgz", + "integrity": "sha512-2Or5oF5ojaXYD8QbO4Z+QdaNXSp+ZyNLJdeyKfejbxLvpL5feSNB0oYtTNrweFPTAvJKQ4DJsdEXy0/s31haRg==" + }, + "@cspell/dict-django": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@cspell/dict-django/-/dict-django-1.0.26.tgz", + "integrity": "sha512-mn9bd7Et1L2zuibc08GVHTiD2Go3/hdjyX5KLukXDklBkq06r+tb0OtKtf1zKodtFDTIaYekGADhNhA6AnKLkg==" + }, + "@cspell/dict-dotnet": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-1.0.25.tgz", + "integrity": "sha512-3BFhdquYqqjeI8Jm1dYepZKGEg+fKFhw7UfPkVdx13C4ETo5VlsS4FAblC0pCY21pDU3QgRZOGL1Bj+KWCGp/w==" + }, + "@cspell/dict-elixir": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/@cspell/dict-elixir/-/dict-elixir-1.0.24.tgz", + "integrity": "sha512-pEX6GYlEx4Teusw/m+XmqoXzcHOqpcn1ZX4H33ONqR81XdPwbaKorBr1IG23Ic76IhwrFlOqs48tcnxrHYpFnA==" + }, + "@cspell/dict-en-gb": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb/-/dict-en-gb-1.1.28.tgz", + "integrity": "sha512-noOH+iv4xFpPxu1agiQgp5LhY/KA0Ir28y1xnC2QTtLvlIid7vIvgixBOz4Zi0P7lo/mPmMjQY+x7//2EKFDgQ==" + }, + "@cspell/dict-en_us": { + "version": "1.2.40", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-1.2.40.tgz", + "integrity": "sha512-e8leCvGAWPWQIw0SoozgEAiMt2YM12rafOuW4aQwgTJD++vp32a9RrnVL8olBfWaA57rRWWndbMSmPTrsO9mpg==" + }, + "@cspell/dict-filetypes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-1.1.5.tgz", + "integrity": "sha512-yfkB37J+hL6W8qa4AknFp7u6CGECrw2ql2/y0lUKruLQYid0ApK+bH+ll+Sqgl2YS5QAOhclskc72aQHAcRJIQ==" + }, + "@cspell/dict-fonts": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-1.0.14.tgz", + "integrity": "sha512-VhIX+FVYAnqQrOuoFEtya6+H72J82cIicz9QddgknsTqZQ3dvgp6lmVnsQXPM3EnzA8n1peTGpLDwHzT7ociLA==" + }, + "@cspell/dict-fullstack": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-1.0.37.tgz", + "integrity": "sha512-ljVzUdIlBENMiyHUV06007hz2FPRt+BQmC9Jgn6iGIEQeAQp37Q6oIDmxv2lD65ScEIbysxXuaUgJ5x0j4a48A==" + }, + "@cspell/dict-golang": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-1.1.24.tgz", + "integrity": "sha512-qq3Cjnx2U1jpeWAGJL1GL0ylEhUMqyaR36Xij6Y6Aq4bViCRp+HRRqk0x5/IHHbOrti45h3yy7ii1itRFo+Xkg==" + }, + "@cspell/dict-haskell": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-haskell/-/dict-haskell-1.0.13.tgz", + "integrity": "sha512-kvl8T84cnYRPpND/P3D86P6WRSqebsbk0FnMfy27zo15L5MLAb3d3MOiT1kW3vEWfQgzUD7uddX/vUiuroQ8TA==" + }, + "@cspell/dict-html": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-1.1.6.tgz", + "integrity": "sha512-RsZXIrmsnLcUpXfyZdNg7OtO2+e4p7m/qILg03kM6vhSUMY6ryCQNPWKrHqsl8+LBKd54EgFM+O5zcgq6IIsCw==" + }, + "@cspell/dict-html-symbol-entities": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-1.0.23.tgz", + "integrity": "sha512-PV0UBgcBFbBLf/m1wfkVMM8w96kvfHoiCGLWO6BR3Q9v70IXoE4ae0+T+f0CkxcEkacMqEQk/I7vuE9MzrjaNw==" + }, + "@cspell/dict-java": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@cspell/dict-java/-/dict-java-1.0.22.tgz", + "integrity": "sha512-CVAJ29dx1XwwutgsMgaj5eCl1Nc7X7qFhWL2KkAdu78A/NUIaS+1I9KS0hHhdZx/wLke9dH8TR7NyPQGpGxeAw==" + }, + "@cspell/dict-latex": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@cspell/dict-latex/-/dict-latex-1.0.25.tgz", + "integrity": "sha512-cEgg91Migqcp1SdVV7dUeMxbPDhxdNo6Fgq2eygAXQjIOFK520FFvh/qxyBvW90qdZbIRoU2AJpchyHfGuwZFA==" + }, + "@cspell/dict-lorem-ipsum": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@cspell/dict-lorem-ipsum/-/dict-lorem-ipsum-1.0.22.tgz", + "integrity": "sha512-yqzspR+2ADeAGUxLTfZ4pXvPl7FmkENMRcGDECmddkOiuEwBCWMZdMP5fng9B0Q6j91hQ8w9CLvJKBz10TqNYg==" + }, + "@cspell/dict-lua": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-lua/-/dict-lua-1.0.16.tgz", + "integrity": "sha512-YiHDt8kmHJ8nSBy0tHzaxiuitYp+oJ66ffCYuFWTNB3//Y0SI4OGHU3omLsQVeXIfCeVrO4DrVvRDoCls9B5zQ==" + }, + "@cspell/dict-node": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-node/-/dict-node-1.0.11.tgz", + "integrity": "sha512-q66zAqtNmuvZGKt4stRwQPFLsbOjZGGZOZ1HEbqpOkicxvF0BWhR0Di/JBq27PDxeqQP3S5sLeogQTSNQBuTww==" + }, + "@cspell/dict-npm": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-1.0.11.tgz", + "integrity": "sha512-mokmv9/Yk1yliDz97drWyuDWv7eKGEcFhdM43YSPK7GuMLh6i2ULOmORPFhUcjxQjPf0uySMDA2JguiQ4m5Lmg==" + }, + "@cspell/dict-php": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/@cspell/dict-php/-/dict-php-1.0.23.tgz", + "integrity": "sha512-rRLf/09rXDrzs0DJuNXNmFVTw2b2zLmZKNF4LIPrFHYHvdfsMvwVqxkr/SAyhF8C6zi5sW0XYC/J0S/3IE927w==" + }, + "@cspell/dict-powershell": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@cspell/dict-powershell/-/dict-powershell-1.0.14.tgz", + "integrity": "sha512-hisOXXi5PBXB5YKtrJQIis2FIRHgSW1U0/sd4yI36lzb3ZMEvGJwdAdyhXN3IGiqRUNxMzJiXAeXfhnia4xPtQ==" + }, + "@cspell/dict-python": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-1.0.33.tgz", + "integrity": "sha512-tRmE4TzHDFPs7sJ1a3XbfyFrvRHwefVz+z1wkm6tkXK9TPrCbIS+rV/T8xhj205q4lpZQ/TkNB3lT40eLB9O8A==" + }, + "@cspell/dict-ruby": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-1.0.13.tgz", + "integrity": "sha512-YeN1acY38dgMYlEJ6iWPH+8qXB6seLKHm9BszXxaKT/IzGA9Y9XUWPGobeJFD5E/tC6HjvcqRKxEs8vnvakoLQ==" + }, + "@cspell/dict-rust": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@cspell/dict-rust/-/dict-rust-1.0.22.tgz", + "integrity": "sha512-7WOIzv0BPiU+MssZbbMk8K+HR/g9Bcvd0+jXJC3/AKT8L6l0Mx0Tr/oF7cJ4xvCYgA84nBz3PhMZkabGSz/Nkg==" + }, + "@cspell/dict-scala": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@cspell/dict-scala/-/dict-scala-1.0.21.tgz", + "integrity": "sha512-5V/R7PRbbminTpPS3ywgdAalI9BHzcEjEj9ug4kWYvBIGwSnS7T6QCFCiu+e9LvEGUqQC+NHgLY4zs1NaBj2vA==" + }, + "@cspell/dict-software-terms": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-1.0.27.tgz", + "integrity": "sha512-O6wCGuFSnr9G9Sr62zc7/XyruRRPI0/PJ0xZj8/R+hr+vFjDaScQnkqj10gTVoLAshk1TjL5Firnzyz3ibfgdQ==" + }, + "@cspell/dict-typescript": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-1.0.17.tgz", + "integrity": "sha512-CXCuXcrgAc56P3kL9I6gW6bZwTs6t3duyAtHerHg5YAYbPs6/4nXgniQgLgu8kjFHFy07XrqaaBdLU9V2DmMtQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "comment-json": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.1.0.tgz", + "integrity": "sha512-WEghmVYaNq9NlWbrkzQTSsya9ycLyxJxpTQfZEan6a5Jomnjw18zS3Podf8q1Zf9BvonvQd/+Z7Z39L7KKzzdQ==", + "requires": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.2", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "cspell": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-4.2.8.tgz", + "integrity": "sha512-eqan8+lCU9bSp8Tl4+SR/ccBnuPyMmp7evck/RlMdFTjLh/s+3vQ5hQyBzbzK8w2MMqL84CymW7BwIOKjpylSg==", + "requires": { + "chalk": "^4.1.0", + "commander": "^7.0.0", + "comment-json": "^4.0.6", + "cspell-glob": "^0.1.25", + "cspell-lib": "^4.3.12", + "fs-extra": "^9.1.0", + "gensequence": "^3.1.1", + "get-stdin": "^8.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + } + }, + "cspell-glob": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-0.1.25.tgz", + "integrity": "sha512-/XaSHrGBpMJa+duFz3GKOWfrijrfdHT7a/XGgIcq3cymCSpOH+DPho42sl0jLI/hjM+8yv2m8aEoxRT8yVSnlg==", + "requires": { + "micromatch": "^4.0.2" + } + }, + "cspell-io": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-4.1.7.tgz", + "integrity": "sha512-V0/tUu9FnIS3v+vAvDT6NNa14Nc/zUNX8+YUUOfFAiDJJTdqefmvcWjOJBIMYBf3wIk9iWLmLbMM+bNHqr7DSQ==", + "requires": { + "iconv-lite": "^0.6.2", + "iterable-to-stream": "^1.0.1" + } + }, + "cspell-lib": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-4.3.12.tgz", + "integrity": "sha512-yCCb6MoW1K8Tsr/WVEQoO4dfYhH9bCsjQayccb8MlyDaNNuWJHuX+gUGHsZSXSuChSh8PrTWKXJzs13/uM977g==", + "requires": { + "@cspell/dict-aws": "^1.0.13", + "@cspell/dict-bash": "^1.0.11", + "@cspell/dict-companies": "^1.0.35", + "@cspell/dict-cpp": "^1.1.37", + "@cspell/dict-cryptocurrencies": "^1.0.10", + "@cspell/dict-csharp": "^1.0.10", + "@cspell/dict-css": "^1.0.10", + "@cspell/dict-django": "^1.0.25", + "@cspell/dict-dotnet": "^1.0.24", + "@cspell/dict-elixir": "^1.0.23", + "@cspell/dict-en-gb": "^1.1.27", + "@cspell/dict-en_us": "^1.2.39", + "@cspell/dict-filetypes": "^1.1.5", + "@cspell/dict-fonts": "^1.0.13", + "@cspell/dict-fullstack": "^1.0.36", + "@cspell/dict-golang": "^1.1.24", + "@cspell/dict-haskell": "^1.0.12", + "@cspell/dict-html": "^1.1.5", + "@cspell/dict-html-symbol-entities": "^1.0.23", + "@cspell/dict-java": "^1.0.22", + "@cspell/dict-latex": "^1.0.23", + "@cspell/dict-lorem-ipsum": "^1.0.22", + "@cspell/dict-lua": "^1.0.16", + "@cspell/dict-node": "^1.0.10", + "@cspell/dict-npm": "^1.0.10", + "@cspell/dict-php": "^1.0.23", + "@cspell/dict-powershell": "^1.0.14", + "@cspell/dict-python": "^1.0.32", + "@cspell/dict-ruby": "^1.0.12", + "@cspell/dict-rust": "^1.0.22", + "@cspell/dict-scala": "^1.0.21", + "@cspell/dict-software-terms": "^1.0.24", + "@cspell/dict-typescript": "^1.0.16", + "comment-json": "^4.1.0", + "configstore": "^5.0.1", + "cspell-io": "^4.1.7", + "cspell-trie-lib": "^4.2.8", + "cspell-util-bundle": "^4.1.11", + "fs-extra": "^9.1.0", + "gensequence": "^3.1.1", + "minimatch": "^3.0.4", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0", + "vscode-uri": "^3.0.2" + } + }, + "cspell-trie-lib": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-4.2.8.tgz", + "integrity": "sha512-Nt3c0gxOYXIc3/yhALDukpje1BgR6guvlUKWQO2zb0r7qRWpwUw2j2YM4dWbHQeH/3Hx5ei4Braa6cMaiJ5YBw==", + "requires": { + "gensequence": "^3.1.1" + } + }, + "cspell-util-bundle": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/cspell-util-bundle/-/cspell-util-bundle-4.1.11.tgz", + "integrity": "sha512-or3OGKydZs1NwweMIgnA48k8H3F5zK4e5lonjUhpEzLYQZ2nB23decdoqZ8ogFC8pFTA40tZKDsMJ0b+65gX4Q==" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gensequence": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-3.1.1.tgz", + "integrity": "sha512-ys3h0hiteRwmY6BsvSttPmkhC0vEQHPJduANBRtH/dlDPZ0UBIb/dXy80IcckXyuQ6LKg+PloRqvGER9IS7F7g==" + }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "requires": { + "ini": "^1.3.4" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==" + }, + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "iterable-to-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterable-to-stream/-/iterable-to-stream-1.0.1.tgz", + "integrity": "sha512-O62gD5ADMUGtJoOoM9U6LQ7i4byPXUNoHJ6mqsmkQJcom331ZJGDApWgDESWyBMEHEJRjtHozgIiTzYo9RU4UA==" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "requires": { + "global-dirs": "^0.1.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "vscode-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.2.tgz", + "integrity": "sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + } + } +} diff --git a/package.json b/package.json index 070c353cd..2c3f8660c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/pycontribs/jira.git" }, "dependencies": { - "cspell": "^4.0.23", + "cspell": "^4.2.8", "npm": "^6.10.0" } } diff --git a/pyproject.toml b/pyproject.toml index ba41bc7f5..3b9e229bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,5 +5,5 @@ requires = [ "setuptools_scm_git_archive >= 1.0", "wheel", ] -requires-python = ">=3.5" +requires-python = ">3.5" build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 545190166..4445ad8b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,24 +1,24 @@ [metadata] name = jira author = Ben Speakmon -author-email = ben.speakmon@gmail.com +author_email = ben.speakmon@gmail.com maintainer = Sorin Sbarnea -maintainer-email = sorin.sbarnea@gmail.com +maintainer_email = sorin.sbarnea@gmail.com summary = Python library for interacting with Jira via REST APIs. -long-description = file: README.rst +long_description = file: README.rst # Do not include ChangeLog in description-file due to multiple reasons: # - Unicode chars, see https://github.com/pycontribs/jira/issues/512 # - Breaks ability to perform `python setup.py install` -long-description-content-type = text/x-rst; charset=UTF-8 -home-page = https://github.com/pycontribs/jira -project-urls = +long_description_content_type = text/x-rst; charset=UTF-8 +url = https://github.com/pycontribs/jira +project_urls = Bug Tracker = https://github.com/pycontribs/jira/issues Release Management = https://github.com/pycontribs/jira/projects CI: Travis = https://travis-ci.com/pycontribs/jira Source Code = https://github.com/pycontribs/jira.git Documentation = https://jira.readthedocs.io/en/master/ Forum = https://community.atlassian.com/t5/tag/jira-python/tg-p?sort=recent -requires-python = >=3.5 +requires_python = >=3.6 platforms = any license = BSD classifiers = @@ -31,9 +31,10 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Software Development :: Libraries :: Python Modules Topic :: Internet :: WWW/HTTP keywords = api, atlassian, jira, rest, web @@ -44,7 +45,7 @@ packages = [options] use_scm_version = True -python_requires = >=3.5 +python_requires = >=3.6 packages = find: include_package_data = True zip_safe = False @@ -86,8 +87,8 @@ test = pytest-instafail pytest-sugar pytest-timeout>=1.3.1 - pytest-xdist>=1.14 - pytest>=5.0.0,<6.0 # MIT + pytest-xdist>=2.2 + pytest>=6.0.0,<7.0 # MIT PyYAML>=5.1 # MIT requests_mock # Apache-2 requires.io # UNKNOWN!!! @@ -146,4 +147,4 @@ filterwarnings = ignore::pytest.PytestWarning [mypy] -python_version = 3.5 +python_version = 3.6 diff --git a/test.local b/test.local deleted file mode 100755 index ebd061bbc..000000000 --- a/test.local +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Settings for using the Vagrant VM from atlassian -# (see https://developer.atlassian.com/static/connect/docs/latest/developing/developing-locally.html) -# a user "jira_user" with password "jira" needs to be created manually -export CI_JIRA_URL="http://localhost:2990/jira" -export CI_JIRA_ADMIN="admin" -export CI_JIRA_ADMIN_PASSWORD="admin" -export CI_JIRA_USER=jira_user -export CI_JIRA_USER_PASSWORD=jira -export CI_JIRA_ISSUE=Task - -if [ "$1" = "--tox" ] ; then - shift - exec tox "$@" -else - exec python -m pytest --cov-report xml --cov jira --pyargs jira "$@" -fi diff --git a/tests/test_client.py b/tests/test_client.py index 1c49fdbc6..52408f129 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -73,24 +73,15 @@ def test_delete_inexistent_project(cl_admin): def test_templates(cl_admin): - templates = cl_admin.templates() + templates = set(cl_admin.templates()) expected_templates = set( filter( None, """ -Agility -Basic -Bug tracking -Content Management -Customer service -Document Approval -IT Service Desk +Basic software development Kanban software development -Lead Tracking Process management -Procurement Project management -Recruitment Scrum software development Task management """.split( @@ -99,8 +90,7 @@ def test_templates(cl_admin): ) ) - for t in expected_templates: - assert t in templates + assert templates == expected_templates def test_result_list(): diff --git a/tests/tests.py b/tests/tests.py index 32f1d6758..d3ddcebbe 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -295,7 +295,7 @@ def __init__(self): ): break except Exception as e: - if "A project with that name already exists" not in e.text: + if "A project with that name already exists" not in str(e): raise e self.project_a_id = self.jira_admin.project(self.project_a).id self.jira_admin.create_project(self.project_b, self.project_b_name) @@ -453,7 +453,7 @@ def test_application_property(self): clone_prefix = self.jira.application_properties( key="jira.lf.text.headingcolour" ) - self.assertEqual(clone_prefix["value"], "#292929") + self.assertEqual(clone_prefix["value"], "#172b4d") def test_set_application_property(self): prop = "jira.lf.favicon.hires.url" @@ -1133,12 +1133,10 @@ def test_editmeta(self): "comment", "components", "description", - "environment", "fixVersions", "issuelinks", "labels", "summary", - "versions", } for i in (self.issue_1, self.issue_2): meta = self.jira.editmeta(i) @@ -1304,15 +1302,15 @@ def test_votes_with_issue_obj(self): def test_add_remove_watcher(self): # removing it in case it exists, so we know its state - self.jira.remove_watcher(self.issue_1, self.test_manager.user_admin.accountId) + self.jira.remove_watcher(self.issue_1, self.test_manager.user_admin.key) init_watchers = self.jira.watchers(self.issue_1).watchCount # adding a new watcher - self.jira.add_watcher(self.issue_1, self.test_manager.user_admin.accountId) + self.jira.add_watcher(self.issue_1, self.test_manager.user_admin.key) self.assertEqual(self.jira.watchers(self.issue_1).watchCount, init_watchers + 1) # now we verify that remove does indeed remove watchers - self.jira.remove_watcher(self.issue_1, self.test_manager.user_admin.accountId) + self.jira.remove_watcher(self.issue_1, self.test_manager.user_admin.key) new_watchers = self.jira.watchers(self.issue_1).watchCount self.assertEqual(init_watchers, new_watchers) @@ -1598,12 +1596,7 @@ def test_project_versions(self): self.assertEqual(test.name, name) i = self.jira.issue(JiraTestManager().project_b_issue1) - i.update( - fields={ - "versions": [{"id": version.id}], - "fixVersions": [{"id": version.id}], - } - ) + i.update(fields={"fixVersions": [{"id": version.id}]}) version.delete() def test_get_project_version_by_name(self): @@ -1620,12 +1613,7 @@ def test_get_project_version_by_name(self): self.assertEqual(not_found_version, None) i = self.jira.issue(JiraTestManager().project_b_issue1) - i.update( - fields={ - "versions": [{"id": version.id}], - "fixVersions": [{"id": version.id}], - } - ) + i.update(fields={"fixVersions": [{"id": version.id}]}) version.delete() def test_rename_version(self): @@ -1647,12 +1635,7 @@ def test_rename_version(self): self.assertEqual(not_found_version, None) i = self.jira.issue(JiraTestManager().project_b_issue1) - i.update( - fields={ - "versions": [{"id": version.id}], - "fixVersions": [{"id": version.id}], - } - ) + i.update(fields={"fixVersions": [{"id": version.id}]}) version.delete() def test_project_versions_with_project_obj(self): @@ -2142,7 +2125,7 @@ def setUp(self): ) def test_fetch_pages(self): - """Tests that the JIRA._fetch_pages method works as expected. """ + """Tests that the JIRA._fetch_pages method works as expected.""" params = {"startAt": 0} total = 26 expected_results = [] @@ -2176,7 +2159,7 @@ def test_fetch_pages(self): def _create_issue_result_json(issue_id, summary, key, **kwargs): - """Returns a minimal json object for an issue. """ + """Returns a minimal json object for an issue.""" return { "id": "%s" % issue_id, "summary": summary, @@ -2186,7 +2169,7 @@ def _create_issue_result_json(issue_id, summary, key, **kwargs): def _create_issue_search_results_json(issues, **kwargs): - """Returns a minimal json object for Jira issue search results. """ + """Returns a minimal json object for Jira issue search results.""" return { "startAt": kwargs.get("start_at", 0), "maxResults": kwargs.get("max_results", 50), diff --git a/tox.ini b/tox.ini index 590b5a146..9b4945947 100644 --- a/tox.ini +++ b/tox.ini @@ -3,17 +3,21 @@ minversion = 3.8.0 requires = tox-pyenv envlist = - lint - pkg + py39 py38 py37 py36 - py35 - docs ignore_basepython_conflict = True skip_missing_interpreters = True skipdist = True +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + [testenv:docs] extras = docs @@ -23,10 +27,8 @@ skipdist = False setenv = PYTHONHTTPSVERIFY=0 commands = - # pip install "..[docs]" - bash -c "set | grep REQUESTS_CA_BUNDLE" - python -m sphinx \ - -a -n -W \ + sphinx-build \ + -a -n -v -W --keep-going \ -b html --color \ -d "{toxworkdir}/docs_doctree" \ docs/ "{toxworkdir}/docs_out" @@ -47,14 +49,21 @@ extras = test sitepackages=False commands= - bash -c 'find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf' + git clean -xdf jira tests python -m pip check + python make_local_jira_user.py python -m pytest {posargs} setenv = PIP_LOG={envdir}/pip.log PIP_DISABLE_PIP_VERSION_CHECK=1 # Avoid 2020-01-01 warnings: https://github.com/pypa/pip/issues/6207 PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command + CI_JIRA_URL=http://localhost:2990/jira + CI_JIRA_ADMIN=admin + CI_JIRA_ADMIN_PASSWORD=admin + CI_JIRA_USER=jira_user + CI_JIRA_USER_PASSWORD=jira + CI_JIRA_ISSUE=Task passenv = CI CI_JIRA_* @@ -62,44 +71,38 @@ passenv = PIP_* REQUESTS_CA_BUNDLE SSL_CERT_FILE - TRAVIS* TWINE_* XDG_CACHE_HOME + # For Windows users, getpass.get_user() needs USERNAME + USERNAME envars = PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_USER=no whitelist_externals = - bash - echo - find - grep - rm - xargs + git + npm [testenv:pkg] deps = collective.checkdocs>=0.2 - pep517>=0.7.0 + build>=0.3.0 pip>=19.2.3 setuptools>=41.4 twine>=2.0.0 wheel>=0.33.6 commands = - rm -rf {toxinidir}/dist + git clean -xdf dist python setup.py check -m -s # disabled due to errors with older setuptools: # python setup.py sdist bdist_wheel - python -m pep517.build \ - --source \ - --binary \ - --out-dir {toxinidir}/dist/ \ - {toxinidir} - python -m twine check {toxinidir}/dist/* + python -m build --wheel --sdist . + python -m twine check dist/* [testenv:lint] deps = pre-commit>=1.17.0 commands= - bash -c "npm install && npm run spell" + npm install + npm run spell python -m pre_commit run --color=always {posargs:--all} extras = skip_install = true