Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-jira for openSUSE:Factory checked in at 2022-06-04 23:27:31 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-jira (Old) and /work/SRC/openSUSE:Factory/.python-jira.new.1548 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jira" Sat Jun 4 23:27:31 2022 rev:8 rq:980788 version:3.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-jira/python-jira.changes 2022-01-31 22:57:53.413351986 +0100 +++ /work/SRC/openSUSE:Factory/.python-jira.new.1548/python-jira.changes 2022-06-04 23:27:37.476791520 +0200 @@ -1,0 +2,14 @@ +Sat Jun 4 18:05:47 UTC 2022 - Dirk M??ller <dmuel...@suse.com> + +- update to 3.2.0: + * Additional scheme endpoints for projects + * Add subscriptable support for PropertyHolder + * Fixing type hint to match documentation for move_to_backlog + * improve ResultList types + * Allow verify option as path to cert in config + * Bugfixes + * Locate the exact user by key if there are multiple users returned from query + * Fixing type hint to match documentation for move_to_backlog + * Avoid printing parsed json response + +------------------------------------------------------------------- Old: ---- jira-3.1.1.tar.gz New: ---- jira-3.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-jira.spec ++++++ --- /var/tmp/diff_new_pack.gbIcbG/_old 2022-06-04 23:27:37.888791937 +0200 +++ /var/tmp/diff_new_pack.gbIcbG/_new 2022-06-04 23:27:37.892791941 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-jira -Version: 3.1.1 +Version: 3.2.0 Release: 0 Summary: Python library for interacting with JIRA via REST APIs License: BSD-3-Clause @@ -40,8 +40,7 @@ Requires: python-requests >= 2.10.0 Requires: python-requests-oauthlib >= 0.6.1 Requires: python-requests-toolbelt -Requires: python-setuptools >= 20.10.1 -Requires: python-six >= 1.10.0 +Requires: python-typing_extensions >= 3.7.4.2 Requires(post): update-alternatives Requires(postun):update-alternatives BuildArch: noarch ++++++ jira-3.1.1.tar.gz -> jira-3.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/.pre-commit-config.yaml new/jira-3.2.0/.pre-commit-config.yaml --- old/jira-3.1.1/.pre-commit-config.yaml 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/.pre-commit-config.yaml 2022-04-14 01:40:59.000000000 +0200 @@ -1,12 +1,12 @@ --- repos: - repo: https://github.com/psf/black - rev: 21.10b0 + rev: 22.3.0 hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace @@ -46,14 +46,15 @@ - id: yamllint files: \.(yaml|yml)$ - repo: https://github.com/pre-commit/mirrors-mypy.git - rev: v0.910-1 + rev: v0.931 hooks: - id: mypy additional_dependencies: - types-requests - types-pkg_resources + args: [--no-strict-optional, --ignore-missing-imports, --show-error-codes] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.31.0 hooks: - id: pyupgrade args: [ --py36-plus ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/PKG-INFO new/jira-3.2.0/PKG-INFO --- old/jira-3.1.1/PKG-INFO 2021-11-11 11:47:33.324146500 +0100 +++ new/jira-3.2.0/PKG-INFO 2022-04-14 01:41:21.476967000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: jira -Version: 3.1.1 +Version: 3.2.0 Summary: Python library for interacting with JIRA via REST APIs. Home-page: https://github.com/pycontribs/jira Author: Ben Speakmon diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/constraints.txt new/jira-3.2.0/constraints.txt --- old/jira-3.1.1/constraints.txt 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/constraints.txt 2022-04-14 01:40:59.000000000 +0200 @@ -8,7 +8,7 @@ # via sphinx appnope==0.1.2 # via ipython -attrs==21.2.0 +attrs==21.4.0 # via pytest babel==2.9.1 # via sphinx @@ -18,24 +18,24 @@ # via requests cffi==1.15.0 # via cryptography -charset-normalizer==2.0.7 +charset-normalizer==2.0.12 # via requests coverage==6.1.2 # via pytest-cov -cryptography==35.0.0 +cryptography==36.0.1 # via # pyspnego # requests-kerberos dataclasses==0.8 # via pyspnego -decorator==5.1.0 +decorator==5.1.1 # via # gssapi # ipython # traitlets defusedxml==0.7.1 # via jira (setup.cfg) -docutils==0.17.1 +docutils==0.18.1 # via # jira (setup.cfg) # sphinx @@ -48,7 +48,7 @@ # via jira (setup.cfg) flaky==3.7.0 # via jira (setup.cfg) -gssapi==1.7.2 +gssapi==1.7.3 # via pyspnego idna==3.3 # via requests @@ -65,28 +65,28 @@ # via jira (setup.cfg) ipython-genutils==0.2.0 # via traitlets -jedi==0.18.0 +jedi==0.18.1 # via ipython jinja2==3.0.3 # via sphinx keyring==23.2.1 # via jira (setup.cfg) -krb5==0.2.0 +krb5==0.3.0 # via pyspnego markupsafe==2.0.1 # via # jinja2 # jira (setup.cfg) -oauthlib==3.1.1 +oauthlib==3.2.0 # via # jira (setup.cfg) # requests-oauthlib -packaging==21.2 +packaging==21.3 # via # pytest # pytest-sugar # sphinx -parso==0.8.2 +parso==0.8.3 # via jedi pexpect==4.8.0 # via ipython @@ -94,7 +94,7 @@ # via ipython pluggy==1.0.0 # via pytest -prompt-toolkit==3.0.22 +prompt-toolkit==3.0.28 # via ipython ptyprocess==0.7.0 # via pexpect @@ -105,7 +105,7 @@ # pytest-forked pycparser==2.21 # via cffi -pygments==2.10.0 +pygments==2.11.2 # via # ipython # sphinx @@ -115,7 +115,7 @@ # requests-jwt pyparsing==2.4.7 # via packaging -pyspnego==0.3.1 +pyspnego==0.5.0 # via requests-kerberos pytest==6.2.5 # via @@ -131,21 +131,21 @@ # via jira (setup.cfg) pytest-cov==3.0.0 # via jira (setup.cfg) -pytest-forked==1.3.0 +pytest-forked==1.4.0 # via pytest-xdist pytest-instafail==0.4.2 # via jira (setup.cfg) pytest-sugar==0.9.4 # via jira (setup.cfg) -pytest-timeout==2.0.1 +pytest-timeout==2.1.0 # via jira (setup.cfg) -pytest-xdist==2.4.0 +pytest-xdist==2.5.0 # via jira (setup.cfg) pytz==2021.3 # via babel pyyaml==6.0 # via jira (setup.cfg) -requests==2.26.0 +requests==2.27.1 # via # jira (setup.cfg) # requests-futures @@ -158,13 +158,13 @@ # sphinx requests-futures==1.0.0 # via jira (setup.cfg) -requests-jwt==0.5.3 +requests-jwt==0.6.0 # via jira (setup.cfg) -requests-kerberos==0.13.0 +requests-kerberos==0.14.0 # via jira (setup.cfg) requests-mock==1.9.3 # via jira (setup.cfg) -requests-oauthlib==1.3.0 +requests-oauthlib==1.3.1 # via jira (setup.cfg) requests-toolbelt==0.9.1 # via jira (setup.cfg) @@ -174,9 +174,9 @@ # via # requests-mock # traitlets -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 # via sphinx -sphinx==4.3.0 +sphinx==4.4.0 # via # jira (setup.cfg) # sphinx-rtd-theme @@ -200,17 +200,17 @@ # via pytest-sugar toml==0.10.2 # via pytest -tomli==1.2.2 +tomli==1.2.3 # via coverage traitlets==4.3.3 # via ipython -typing-extensions==3.10.0.2 +typing-extensions==4.1.1 # via importlib-metadata -urllib3==1.26.7 +urllib3==1.26.8 # via requests wcwidth==0.2.5 # via prompt-toolkit -wheel==0.37.0 +wheel==0.37.1 # via jira (setup.cfg) xmlrunner==1.7.7 # via jira (setup.cfg) @@ -218,6 +218,3 @@ # via jira (setup.cfg) zipp==3.6.0 # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/docs/examples.rst new/jira-3.2.0/docs/examples.rst --- old/jira-3.1.1/docs/examples.rst 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/docs/examples.rst 2022-04-14 01:40:59.000000000 +0200 @@ -8,9 +8,9 @@ .. literalinclude:: ../examples/basic_use.py -Another example shows how to authenticate with your Jira username and password: +Another example with methods to authenticate with your Jira: -.. literalinclude:: ../examples/basic_auth.py +.. literalinclude:: ../examples/auth.py This example shows how to work with GreenHopper: @@ -23,7 +23,7 @@ Initialization -------------- -Everything goes through the ``JIRA`` object, so make one:: +Everything goes through the :py:class:`jira.client.JIRA` object, so make one:: from jira import JIRA @@ -40,13 +40,18 @@ -------------- At initialization time, jira-python can optionally create an HTTP BASIC or use OAuth 1.0a access tokens for user -authentication. These sessions will apply to all subsequent calls to the ``JIRA`` object. +authentication. These sessions will apply to all subsequent calls to the :py:class:`jira.client.JIRA` object. The library is able to load the credentials from inside the ~/.netrc file, so put them there instead of keeping them in your source code. Cookie Based Authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. warning:: + This method of authentication is no longer supported on Jira Cloud. You can find the deprecation notice `here <https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth>`_. + + For Jira Cloud use the basic_auth= :ref:`basic-auth-api-token` authentication + Pass a tuple of (username, password) to the ``auth`` constructor argument:: auth_jira = JIRA(auth=('username', 'password')) @@ -54,10 +59,6 @@ Using this method, authentication happens during the initialization of the object. If the authentication is successful, the retrieved session cookie will be used in future requests. Upon cookie expiration, authentication will happen again transparently. -.. warning:: - This method of authentication is no longer supported on Jira Cloud. You can find the deprecation notice `here <https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth>`_. - - For Jira Cloud use the basic_auth= :ref:`basic-auth-api-token` authentication HTTP BASIC ^^^^^^^^^^ @@ -65,24 +66,30 @@ (username, password) """""""""""""""""""" -Pass a tuple of (username, password) to the ``basic_auth`` constructor argument:: - - auth_jira = JIRA(basic_auth=('username', 'password')) - .. warning:: This method of authentication is no longer supported on Jira Cloud. You can find the deprecation notice `here <https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth>`_ - For Jira Cloud use the basic_auth= :ref:`basic-auth-api-token` authentication + For Jira Cloud use the basic_auth= :ref:`basic-auth-api-token` authentication. + For Self Hosted Jira (Server, Data Center), consider the `Token Auth`_ authentication. + +Pass a tuple of (username, password) to the ``basic_auth`` constructor argument:: + + auth_jira = JIRA(basic_auth=('username', 'password')) .. _basic-auth-api-token: (username, api_token) """"""""""""""""""""" -Or pass a tuple of (email, api_token) to the ``basic_auth`` constructor argument (JIRA cloud):: + +Or pass a tuple of (email, api_token) to the ``basic_auth`` constructor argument (JIRA Cloud):: auth_jira = JIRA(basic_auth=('email', 'API token')) +.. seealso:: + For Self Hosted Jira (Server, Data Center), refer to the `Token Auth`_ Section. + + OAuth ^^^^^ @@ -112,6 +119,29 @@ See https://confluence.atlassian.com/display/JIRA/Configuring+OAuth+Authentication+for+an+Application+Link for details on configuring an OAuth provider for Jira. +Token Auth +^^^^^^^^^^ + + +Jira Cloud +"""""""""" + +This is also referred to as an API Token in the +`Jira Cloud documentation <https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/>`_ :: + + auth_jira = JIRA(basic_auth=('email', 'API token')) + + +Jira Self Hosted (incl. Jira Server/Data Center) +"""""""""""""""""""""""""""""""""""""""""""""""" + +This is also referred to as Personal Access Tokens (PATs) in the +`Self-Hosted Documentation <https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html>`_. +The is available from Jira Core >= 8.14:: + + auth_jira = JIRA(token_auth='API token') + + Kerberos ^^^^^^^^ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/examples/auth.py new/jira-3.2.0/examples/auth.py --- old/jira-3.1.1/examples/auth.py 1970-01-01 01:00:00.000000000 +0100 +++ new/jira-3.2.0/examples/auth.py 2022-04-14 01:40:59.000000000 +0200 @@ -0,0 +1,32 @@ +"""Some simple authentication examples. +""" + +from collections import Counter +from typing import cast + +from jira import JIRA +from jira.client import ResultList +from jira.resources import Issue + +# Some Authentication Methods +jira = JIRA( + basic_auth=("admin", "admin"), # a username/password tuple [Not recommended] + # basic_auth=("email", "API token"), # Jira Cloud: a username/token tuple + # token_auth="API token", # Self-Hosted Jira (e.g. Server): the PAT token + # auth=("admin", "admin"), # a username/password tuple for cookie auth [Not recommended] +) + +# Who has authenticated +myself = jira.myself() + +# Get the mutable application properties for this server (requires +# jira-system-administrators permission) +props = jira.application_properties() + +# Find all issues reported by the admin +# Note: we cast() for mypy's benefit, as search_issues can also return the raw json ! +# This is if the following argument is used: `json_result=True` +issues = cast(ResultList[Issue], jira.search_issues("assignee=admin")) + +# Find the top three projects containing issues reported by admin +top_three = Counter([issue.fields.project.key for issue in issues]).most_common(3) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/examples/basic_auth.py new/jira-3.2.0/examples/basic_auth.py --- old/jira-3.1.1/examples/basic_auth.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/examples/basic_auth.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,27 +0,0 @@ -# This script shows how to connect to a Jira instance with a -# username and password over HTTP BASIC authentication. - -from collections import Counter -from typing import cast - -from jira import JIRA -from jira.client import ResultList -from jira.resources import Issue - -# By default, the client will connect to a Jira instance started from the Atlassian Plugin SDK. -# See -# https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK -# for details. -jira = JIRA(basic_auth=("admin", "admin")) # a username/password tuple - -# Get the mutable application properties for this server (requires -# jira-system-administrators permission) -props = jira.application_properties() - -# Find all issues reported by the admin -# Note: we cast() for mypy's benefit, as search_issues can also return the raw json ! -# This is if the following argument is used: `json_result=True` -issues = cast(ResultList[Issue], jira.search_issues("assignee=admin")) - -# Find the top three projects containing issues reported by admin -top_three = Counter([issue.fields.project.key for issue in issues]).most_common(3) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/examples/cookie_auth.py new/jira-3.2.0/examples/cookie_auth.py --- old/jira-3.1.1/examples/cookie_auth.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/examples/cookie_auth.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,29 +0,0 @@ -# This script shows how to connect to a Jira instance with a -# username and password over HTTP BASIC authentication. - -from collections import Counter -from typing import cast - -from jira import JIRA -from jira.client import ResultList -from jira.resources import Issue - -# By default, the client will connect to a Jira instance started from the Atlassian Plugin SDK. -# See -# https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK -# for details. -jira = JIRA(auth=("admin", "admin")) # a username/password tuple for cookie auth - -# Get the mutable application properties for this server (requires -# jira-system-administrators permission) -props = jira.application_properties() - -# Find all issues reported by the admin -issues = cast(ResultList[Issue], jira.search_issues("assignee=admin")) - -# Find the top three projects containing issues reported by admin -top_three = Counter([issue.fields.project.key for issue in issues]).most_common(3) - -# import time; time.sleep(65) # Fake cookie expiration - -issues = cast(ResultList[Issue], jira.search_issues("assignee=admin")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/examples/maintenance.py new/jira-3.2.0/examples/maintenance.py --- old/jira-3.1.1/examples/maintenance.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/examples/maintenance.py 2022-04-14 01:40:59.000000000 +0200 @@ -16,11 +16,11 @@ CI_JIRA_URL = os.environ["CI_JIRA_URL"] CI_JIRA_ADMIN = os.environ["CI_JIRA_ADMIN"] -CI_JIRA_ADMIN_PASSWORD = os.environ["CI_JIRA_ADMIN_PASSWORD"] +CI_JIRA_ADMIN_TOKEN = os.environ["CI_JIRA_ADMIN_TOKEN"] j = JIRA( CI_JIRA_URL, - basic_auth=(CI_JIRA_ADMIN, CI_JIRA_ADMIN_PASSWORD), + basic_auth=(CI_JIRA_ADMIN, CI_JIRA_ADMIN_TOKEN), logging=True, validate=True, async_=True, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira/client.py new/jira-3.2.0/jira/client.py --- old/jira-3.1.1/jira/client.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/jira/client.py 2022-04-14 01:40:59.000000000 +0200 @@ -1,4 +1,3 @@ -#!/usr/bin/python """ This module implements a friendly (well, friendlier) interface between the raw JSON responses from Jira and the Resource/dict abstractions provided by this library. Users @@ -28,6 +27,7 @@ Callable, Dict, Generic, + Iterator, List, Optional, Tuple, @@ -36,6 +36,7 @@ Union, cast, no_type_check, + overload, ) from urllib.parse import parse_qs, quote, urlparse @@ -67,9 +68,13 @@ Issue, IssueLink, IssueLinkType, + IssueSecurityLevelScheme, IssueType, + IssueTypeScheme, + NotificationScheme, PermissionScheme, Priority, + PriorityScheme, Project, RemoteLink, RequestType, @@ -85,6 +90,7 @@ Version, Votes, Watchers, + WorkflowScheme, Worklog, ) from jira.utils import json_loads, threaded_requests @@ -100,6 +106,10 @@ except ImportError: pass +try: + from typing import SupportsIndex # type:ignore[attr-defined] # Py38+ +except ImportError: + from typing_extensions import SupportsIndex LOG = _logging.getLogger("jira") LOG.addHandler(_logging.NullHandler()) @@ -162,16 +172,32 @@ self.isLast = _isLast self.total = _total if _total is not None else len(self) - self.iterable: List = list(iterable) if iterable else [] + self.iterable: List[ResourceType] = list(iterable) if iterable else [] self.current = self.startAt - def __next__(self) -> Type[ResourceType]: + def __next__(self) -> ResourceType: # type:ignore[misc] self.current += 1 if self.current > self.total: raise StopIteration else: return self.iterable[self.current - 1] + def __iter__(self) -> Iterator[ResourceType]: + return super().__iter__() + + # fmt: off + # The mypy error we ignore is about returning a contravariant type. + # As this class is a List of a generic 'Resource' class + # this is the right way to specify that the output is the same as which + # the class was initialized with. + @overload + def __getitem__(self, i: SupportsIndex) -> ResourceType: ... # type:ignore[misc] # noqa: E704 + @overload + def __getitem__(self, s: slice) -> List[ResourceType]: ... # type:ignore[misc] # noqa: E704 + def __getitem__(self, slice_or_index): # noqa: E301,E261 + return list.__getitem__(self, slice_or_index) + # fmt: on + class QshGenerator: def __init__(self, context_path): @@ -449,7 +475,7 @@ """ # force a copy of the tuple to be used in __del__() because # sys.version_info could have already been deleted in __del__() - self.sys_version_info = tuple(i for i in sys.version_info) + self.sys_version_info = tuple(sys.version_info) if options is None: options = {} @@ -537,7 +563,7 @@ auth_method = ( oauth or basic_auth or jwt or kerberos or auth or "anonymous" ) - raise JIRAError(f"Can not log in with {str(auth_method)}") + raise JIRAError(f"Can not log in with {auth_method}") self.deploymentType = None if get_server_info: @@ -641,7 +667,6 @@ if "<!-- SecurityTokenMissing -->" in content: self.log.warning("Got SecurityTokenMissing") raise JIRAError(f"SecurityTokenMissing: {content}") - return False return True def _get_sprint_field_id(self): @@ -1435,13 +1460,13 @@ p = data["fields"]["project"] - if isinstance(p, str) or isinstance(p, int): + if isinstance(p, (str, int)): data["fields"]["project"] = {"id": self.project(str(p)).id} p = data["fields"]["issuetype"] if isinstance(p, int): data["fields"]["issuetype"] = {"id": p} - if isinstance(p, str) or isinstance(p, int): + if isinstance(p, (str, int)): data["fields"]["issuetype"] = {"id": self.issue_type_by_name(str(p)).id} url = self._get_url("issue") @@ -1478,7 +1503,7 @@ issue_data: Dict[str, Any] = _field_worker(field_dict) p = issue_data["fields"]["project"] - if isinstance(p, str) or isinstance(p, int): + if isinstance(p, (str, int)): issue_data["fields"]["project"] = {"id": self.project(str(p)).id} p = issue_data["fields"]["issuetype"] @@ -1579,7 +1604,6 @@ 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) projects = [ ServiceDesk(self._options, self._session, raw_project_json) for raw_project_json in r_json["values"] @@ -1628,7 +1652,7 @@ p = data["serviceDeskId"] service_desk = None - if isinstance(p, str) or isinstance(p, int): + if isinstance(p, (str, int)): service_desk = self.service_desk(p) elif isinstance(p, ServiceDesk): service_desk = p @@ -1736,9 +1760,15 @@ try: user_obj: User if self._is_cloud: - user_obj = self.search_users(query=user, maxResults=1)[0] + users = self.search_users(query=user, maxResults=20) else: - user_obj = self.search_users(user=user, maxResults=1)[0] + users = self.search_users(user=user, maxResults=20) + + matches = [] + if len(users) > 1: + matches = [u for u in users if self._get_user_identifier(u) == user] + user_obj = matches[0] if matches else users[0] + except Exception as e: raise JIRAError(str(e)) return self._get_user_identifier(user_obj) @@ -1756,7 +1786,7 @@ Returns: bool """ - url = self._get_latest_url(f"issue/{str(issue)}/assignee") + url = self._get_latest_url(f"issue/{issue}/assignee") user_id = self._get_user_id(assignee) payload = {"accountId": user_id} if self._is_cloud else {"name": user_id} r = self._session.put(url, data=json.dumps(payload)) @@ -1777,7 +1807,7 @@ params = {} if expand is not None: params["expand"] = expand - r_json = self._get_json(f"issue/{str(issue)}/comment", params=params) + r_json = self._get_json(f"issue/{issue}/comment", params=params) comments = [ Comment(self._options, self._session, raw_comment_json) @@ -2109,6 +2139,32 @@ return self._find_for_resource(Votes, issue) @translate_resource_args + def project_issue_security_level_scheme( + self, project: str + ) -> IssueSecurityLevelScheme: + """Get a IssueSecurityLevelScheme Resource from the server. + + Args: + project (str): ID or key of the project to get the IssueSecurityLevelScheme for + + Returns: + IssueSecurityLevelScheme: The issue security level scheme + """ + return self._find_for_resource(IssueSecurityLevelScheme, project) + + @translate_resource_args + def project_notification_scheme(self, project: str) -> NotificationScheme: + """Get a NotificationScheme Resource from the server. + + Args: + project (str): ID or key of the project to get the NotificationScheme for + + Returns: + NotificationScheme: The notification scheme + """ + return self._find_for_resource(NotificationScheme, project) + + @translate_resource_args def project_permissionscheme(self, project: str) -> PermissionScheme: """Get a PermissionScheme Resource from the server. @@ -2122,6 +2178,30 @@ return self._find_for_resource(PermissionScheme, project) @translate_resource_args + def project_priority_scheme(self, project: str) -> PriorityScheme: + """Get a PriorityScheme Resource from the server. + + Args: + project (str): ID or key of the project to get the PriorityScheme for + + Returns: + PriorityScheme: The priority scheme + """ + return self._find_for_resource(PriorityScheme, project) + + @translate_resource_args + def project_workflow_scheme(self, project: str) -> WorkflowScheme: + """Get a WorkflowScheme Resource from the server. + + Args: + project (str): ID or key of the project to get the WorkflowScheme for + + Returns: + WorkflowScheme: The workflow scheme + """ + return self._find_for_resource(WorkflowScheme, project) + + @translate_resource_args def add_vote(self, issue: str) -> Response: """Register a vote for the current authenticated user on an issue. @@ -3608,15 +3688,14 @@ """ if self._magic is not None: return self._magic.id_buffer(buff) - else: - try: - return mimetypes.guess_type("f." + str(imghdr.what(0, buff)))[0] - except (OSError, TypeError): - self.log.warning( - "Couldn't detect content type of avatar image" - ". Specify the 'contentType' parameter explicitly." - ) - return None + try: + return mimetypes.guess_type("f." + str(imghdr.what(0, buff)))[0] + except (OSError, TypeError): + self.log.warning( + "Couldn't detect content type of avatar image" + ". Specify the 'contentType' parameter explicitly." + ) + return None def rename_user(self, old_user: str, new_user: str): """Rename a Jira user. @@ -3657,9 +3736,8 @@ r = self._session.delete(url) if 200 <= r.status_code <= 299: return True - else: - self.log.error(r.status_code) - return False + self.log.error(r.status_code) + return False def deactivate_user(self, username: str) -> Union[str, int]: """Disable/deactivate the user. @@ -3697,11 +3775,10 @@ ) if r.status_code == 200: return True - else: - self.log.warning( - f"Got response from deactivating {username}: {r.status_code}" - ) - return r.status_code + self.log.warning( + f"Got response from deactivating {username}: {r.status_code}" + ) + return r.status_code except Exception as e: self.log.error(f"Error Deactivating {username}: {e}") raise JIRAError(f"Error Deactivating {username}: {e}") @@ -3725,11 +3802,10 @@ ) if r.status_code == 200: return True - else: - self.log.warning( - f"Got response from deactivating {username}: {r.status_code}" - ) - return r.status_code + self.log.warning( + f"Got response from deactivating {username}: {r.status_code}" + ) + return r.status_code except Exception as e: self.log.error(f"Error Deactivating {username}: {e}") raise JIRAError(f"Error Deactivating {username}: {e}") @@ -3748,11 +3824,7 @@ """ # /secure/admin/IndexAdmin.jspa # /secure/admin/jira/IndexProgress.jspa?taskId=1 - if background: - indexingStrategy = "background" - else: - indexingStrategy = "stoptheworld" - + indexingStrategy = "background" if background else "stoptheworld" url = self.server_url + "/secure/admin/jira/IndexReIndex.jspa" r = self._session.get(url, headers=self._options["headers"]) @@ -3762,7 +3834,7 @@ if ( not r.text.find("To perform the re-index now, please go to the") - and force is False + and not force ): return True @@ -3796,9 +3868,8 @@ r = self._session.post(url, headers=self._options["headers"], data=payload) if r.status_code == 200: return True - else: - self.log.warning(f"Got {r.status_code} response from calling backup.") - return r.status_code + self.log.warning(f"Got {r.status_code} response from calling backup.") + return r.status_code except Exception as e: self.log.error("I see %s", e) @@ -3974,6 +4045,21 @@ return data["permissionSchemes"] @lru_cache(maxsize=None) + def issue_type_schemes(self) -> List[IssueTypeScheme]: + """Get all issue type schemes defined (Admin required). + + Returns: + List[IssueTypeScheme]: All the Issue Type Schemes available to the currently logged in user. + """ + + url = self._get_url("issuetypescheme") + + r = self._session.get(url) + data: Dict[str, Any] = json_loads(r) + + return data["schemes"] + + @lru_cache(maxsize=None) def issuesecurityschemes(self): url = self._get_url("issuesecurityschemes") @@ -4059,6 +4145,20 @@ self.permissionschemes.cache_clear() return data + def get_issue_type_scheme_associations(self, id: str) -> List[Project]: + """For the specified issue type scheme, returns all of the associated projects. (Admin required) + + Args: + id (str): The issue type scheme id. + + Returns: + List[Project]: Associated Projects for the Issue Type Scheme. + """ + url = self._get_url(f"issuetypescheme/{id}/associations") + r = self._session.get(url) + data = json_loads(r) + return data + def create_project( self, key: str, @@ -4837,11 +4937,11 @@ ) raise - def move_to_backlog(self, issue_keys: str) -> Response: + def move_to_backlog(self, issue_keys: List[str]) -> Response: """Move issues in ``issue_keys`` to the backlog, removing them from all sprints that have not been completed. Args: - issue_keys (str): the issues to move to the backlog + issue_keys (List[str]): the issues to move to the backlog Raises: JIRAError: If moving issues to backlog fails diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira/config.py new/jira-3.2.0/jira/config.py --- old/jira-3.1.1/jira/config.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/jira/config.py 2022-04-14 01:40:59.000000000 +0200 @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ This module allows people to keep their jira server credentials outside their script, in a configuration file that is not saved in the source control. @@ -9,7 +8,7 @@ import logging import os import sys -from typing import Optional +from typing import Optional, Union from jira.client import JIRA @@ -21,7 +20,7 @@ password: str = "admin", appid=None, autofix=False, - verify: bool = True, + verify: Union[bool, str] = True, ): """Return a JIRA object by loading the connection details from the `config.ini` file. @@ -32,7 +31,9 @@ password (str): password to use for authentication appid: appid autofix: autofix - verify (bool): boolean indicating whether SSL certificates should be verified + verify (Union[bool, str]): boolean indicating whether SSL certificates should be + verified, or path to a CA_BUNDLE file or directory with certificates of + trusted CAs. Returns: JIRA: an instance to a JIRA object. @@ -73,13 +74,18 @@ return possible return None + if isinstance(verify, bool): + verify = "yes" if verify else "no" + else: + verify = verify + config = configparser.ConfigParser( defaults={ "user": None, "pass": None, "appid": appid, "autofix": autofix, - "verify": "yes" if verify else "no", + "verify": verify, }, allow_no_value=True, ) @@ -104,8 +110,10 @@ password = config.get(profile, "pass") appid = config.get(profile, "appid") autofix = config.get(profile, "autofix") - verify = config.getboolean(profile, "verify") - + try: + verify = config.getboolean(profile, "verify") + except ValueError: + verify = config.get(profile, "verify") else: raise OSError( "%s was not able to locate the config.ini file in current directory, user home directory or PYTHONPATH." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira/jirashell.py new/jira-3.2.0/jira/jirashell.py --- old/jira-3.1.1/jira/jirashell.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/jira/jirashell.py 2022-04-14 01:40:59.000000000 +0200 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - """Starts an interactive Jira session in an ipython terminal. Script arguments support changing the server and a persistent authentication diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira/resilientsession.py new/jira-3.2.0/jira/resilientsession.py --- old/jira-3.1.1/jira/resilientsession.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/jira/resilientsession.py 2022-04-14 01:40:59.000000000 +0200 @@ -129,7 +129,7 @@ msg = "Atlassian's bug https://jira.atlassian.com/browse/JRA-41559" # Exponential backoff with full jitter. - delay = min(self.max_retry_delay, 10 * 2 ** counter) * random.random() + delay = min(self.max_retry_delay, 10 * 2**counter) * random.random() logging.warning( "Got recoverable error from %s %s, will retry [%s/%s] in %ss. Err: %s" % (request, url, counter, self.max_retries, delay, msg) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira/resources.py new/jira-3.2.0/jira/resources.py --- old/jira-3.1.1/jira/resources.py 2021-11-11 11:47:14.000000000 +0100 +++ new/jira-3.2.0/jira/resources.py 2022-04-14 01:40:59.000000000 +0200 @@ -42,9 +42,14 @@ "Worklog", "IssueLink", "IssueLinkType", + "IssueSecurityLevelScheme", "IssueType", + "IssueTypeScheme", + "NotificationScheme", "Priority", + "PriorityScheme", "Version", + "WorkflowScheme", "Role", "Resolution", "SecurityLevel", @@ -693,6 +698,27 @@ super().update(async_=async_, jira=jira, notify=notify, fields=data) + def get_field(self, field_name: str) -> Any: + """Obtain the (parsed) value from the Issue's field. + + Args: + field_name (str): The name of the field to get + + Raises: + AttributeError: If the field does not exist or if the field starts with an ``_`` + + Returns: + Any: Returns the parsed data stored in the field. For example, "project" would be of class :py:class:`Project` + """ + + if field_name.startswith("_"): + raise AttributeError( + f"An issue field_name cannot start with underscore (_): {field_name}", + field_name, + ) + else: + return getattr(self.fields, field_name) + def add_field_value(self, field: str, value: str): """Add a value to a field that supports multiple values, without resetting the existing values. @@ -799,6 +825,40 @@ self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) +class IssueTypeScheme(Resource): + """An issue type scheme.""" + + def __init__(self, options, session, raw=None): + Resource.__init__(self, "issuetypescheme", options, session) + if raw: + self._parse_raw(raw) + self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) + + +class IssueSecurityLevelScheme(Resource): + """IssueSecurityLevelScheme information on an project.""" + + def __init__(self, options, session, raw=None): + Resource.__init__( + self, "project/{0}/issuesecuritylevelscheme?expand=user", options, session + ) + if raw: + self._parse_raw(raw) + self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) + + +class NotificationScheme(Resource): + """NotificationScheme information on an project.""" + + def __init__(self, options, session, raw=None): + Resource.__init__( + self, "project/{0}/notificationscheme?expand=user", options, session + ) + if raw: + self._parse_raw(raw) + self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) + + class PermissionScheme(Resource): """Permissionscheme information on an project.""" @@ -811,6 +871,30 @@ self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) +class PriorityScheme(Resource): + """PriorityScheme information on an project.""" + + def __init__(self, options, session, raw=None): + Resource.__init__( + self, "project/{0}/priorityscheme?expand=user", options, session + ) + if raw: + self._parse_raw(raw) + self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) + + +class WorkflowScheme(Resource): + """WorkflowScheme information on an project.""" + + def __init__(self, options, session, raw=None): + Resource.__init__( + self, "project/{0}/workflowscheme?expand=user", options, session + ) + if raw: + self._parse_raw(raw) + self.raw: Dict[str, Any] = cast(Dict[str, Any], self.raw) + + class Watchers(Resource): """Watcher information on an issue.""" @@ -1407,10 +1491,15 @@ r"issueLink/[^/]+$": IssueLink, r"issueLinkType/[^/]+$": IssueLinkType, r"issuetype/[^/]+$": IssueType, + r"issuetypescheme/[^/]+$": IssueTypeScheme, + r"project/[^/]+/issuesecuritylevelscheme[^/]+$": IssueSecurityLevelScheme, + r"project/[^/]+/notificationscheme[^/]+$": NotificationScheme, + r"project/[^/]+/priorityscheme[^/]+$": PriorityScheme, r"priority/[^/]+$": Priority, r"project/[^/]+$": Project, r"project/[^/]+/role/[^/]+$": Role, r"project/[^/]+/permissionscheme[^/]+$": PermissionScheme, + r"project/[^/]+/workflowscheme[^/]+$": WorkflowScheme, r"resolution/[^/]+$": Resolution, r"securitylevel/[^/]+$": SecurityLevel, r"status/[^/]+$": Status, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira.egg-info/PKG-INFO new/jira-3.2.0/jira.egg-info/PKG-INFO --- old/jira-3.1.1/jira.egg-info/PKG-INFO 2021-11-11 11:47:33.000000000 +0100 +++ new/jira-3.2.0/jira.egg-info/PKG-INFO 2022-04-14 01:41:20.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: jira -Version: 3.1.1 +Version: 3.2.0 Summary: Python library for interacting with JIRA via REST APIs. Home-page: https://github.com/pycontribs/jira Author: Ben Speakmon diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira.egg-info/SOURCES.txt new/jira-3.2.0/jira.egg-info/SOURCES.txt --- old/jira-3.1.1/jira.egg-info/SOURCES.txt 2021-11-11 11:47:33.000000000 +0100 +++ new/jira-3.2.0/jira.egg-info/SOURCES.txt 2022-04-14 01:41:21.000000000 +0200 @@ -37,9 +37,8 @@ docs/_static/css/custom_width.css docs/extra/jira.xml docs/templates/.placeholder -examples/basic_auth.py +examples/auth.py examples/basic_use.py -examples/cookie_auth.py examples/greenhopper.py examples/maintenance.py jira/__init__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira.egg-info/entry_points.txt new/jira-3.2.0/jira.egg-info/entry_points.txt --- old/jira-3.1.1/jira.egg-info/entry_points.txt 2021-11-11 11:47:33.000000000 +0100 +++ new/jira-3.2.0/jira.egg-info/entry_points.txt 2022-04-14 01:41:21.000000000 +0200 @@ -1,3 +1,2 @@ [console_scripts] jirashell = jira.jirashell:main - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/jira.egg-info/requires.txt new/jira-3.2.0/jira.egg-info/requires.txt --- old/jira-3.1.1/jira.egg-info/requires.txt 2021-11-11 11:47:33.000000000 +0100 +++ new/jira-3.2.0/jira.egg-info/requires.txt 2022-04-14 01:41:21.000000000 +0200 @@ -3,7 +3,7 @@ requests-oauthlib>=1.1.0 requests>=2.10.0 requests_toolbelt -setuptools>=20.10.1 +typing_extensions>=3.7.4.2 [async] requests-futures>=0.9.7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jira-3.1.1/setup.cfg new/jira-3.2.0/setup.cfg --- old/jira-3.1.1/setup.cfg 2021-11-11 11:47:33.324146500 +0100 +++ new/jira-3.2.0/setup.cfg 2022-04-14 01:41:21.476967000 +0200 @@ -55,7 +55,7 @@ requests-oauthlib>=1.1.0 requests>=2.10.0 requests_toolbelt - setuptools>=20.10.1 + typing_extensions>=3.7.4.2 [options.extras_require] cli =