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 2024-04-03 17:20:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jira (Old)
 and      /work/SRC/openSUSE:Factory/.python-jira.new.1905 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jira"

Wed Apr  3 17:20:48 2024 rev:14 rq:1164275 version:3.8.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-jira/python-jira.changes  2024-03-25 
21:14:06.702210511 +0100
+++ /work/SRC/openSUSE:Factory/.python-jira.new.1905/python-jira.changes        
2024-04-03 17:22:05.309197282 +0200
@@ -1,0 +2,13 @@
+Wed Apr  3 07:24:10 UTC 2024 - Dirk Müller <[email protected]>
+
+- update to 3.8.0:
+  * Add goal field to update/create sprint (#1806) @zbarahal
+  * add backward compatibility for createmeta_issuetypes &
+    createmeta_fieldtypes (#1838) @paminov
+- update to 3.7.0:
+  * add Release Process doc
+  * Improve handling of Jira's retry-after handling
+  * ISSUE-1836: Add `Dashboard` Support
+  * Update createmeta warning with new method names
+
+-------------------------------------------------------------------

Old:
----
  jira-3.6.0.tar.gz

New:
----
  jira-3.8.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jira.spec ++++++
--- /var/tmp/diff_new_pack.ONEBNM/_old  2024-04-03 17:22:05.893218830 +0200
+++ /var/tmp/diff_new_pack.ONEBNM/_new  2024-04-03 17:22:05.893218830 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-jira
-Version:        3.6.0
+Version:        3.8.0
 Release:        0
 Summary:        Python library for interacting with JIRA via REST APIs
 License:        BSD-3-Clause

++++++ jira-3.6.0.tar.gz -> jira-3.8.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/.readthedocs.yml 
new/jira-3.8.0/.readthedocs.yml
--- old/jira-3.6.0/.readthedocs.yml     2024-01-05 18:17:52.000000000 +0100
+++ new/jira-3.8.0/.readthedocs.yml     2024-03-25 13:16:36.000000000 +0100
@@ -7,16 +7,13 @@
   os: ubuntu-22.04
   tools:
     python: "3.11"
-  jobs:
-    # Work-around to actually constrain dependencies
-    # 
https://github.com/readthedocs/readthedocs.org/issues/7258#issuecomment-1094978683
-    post_install:
-      - python -m pip install --upgrade --upgrade-strategy eager 
--no-cache-dir .[docs,cli] -c constraints.txt
+  apt_packages:
+    - libkrb5-dev
 
 python:
   install:
-    - method: pip
-      path: .
+    - requirements: constraints.txt
+    - path: .
       extra_requirements:
         - docs
         # to autodoc jirashell
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/PKG-INFO new/jira-3.8.0/PKG-INFO
--- old/jira-3.6.0/PKG-INFO     2024-01-05 18:18:04.238345900 +0100
+++ new/jira-3.8.0/PKG-INFO     2024-03-25 13:16:45.864503400 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: jira
-Version: 3.6.0
+Version: 3.8.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.6.0/RELEASE.md new/jira-3.8.0/RELEASE.md
--- old/jira-3.6.0/RELEASE.md   1970-01-01 01:00:00.000000000 +0100
+++ new/jira-3.8.0/RELEASE.md   2024-03-25 13:16:36.000000000 +0100
@@ -0,0 +1,25 @@
+# Documenting the Release Process
+
+## Scope
+
+The scope and purpose of these instructions are for maintainers who are 
looking to make
+a release.
+
+It forms a checklist of what needs to be done, together with sections for 
guidance.
+
+- [ ] [Draft a New Release](#drafting-a-new-release)
+- [ ] [Publishing the Release](#publishing-the-release)
+
+
+## Drafting a New Release
+
+1. Go to the https://github.com/pycontribs/jira/releases page and **"Draft a 
New Release"** using the button. Note: We currently use the 'Release Drafter' 
GHA that auto populates a draft release
+2. Under the **"Choose a tag"**, make sure we follow the repository convention 
of a tag that IS NOT PREFIXED with a `v` e.g. `1.2.3` instead of `v1.2.3`
+3. The tag **"Target"** should be `main`, the main branch.
+3. The contents of the release should reference the PR reference and the 
individual who contributed. In some cases where maintainers take over a 
previous PR it is better practice to reference the name of the original 
submitter of the PR. e.g. The maintainer re-makes a PR based on a stale PR, the 
GHA would mention the maintainer by default as they created the PR, so the 
originator should be used.
+
+## Publishing the Release
+
+1. Pressing the **Edit** button of the latest draft release and pressing the 
**'Publish release'** button will trigger the release process.
+2. The release process will request an approver from the list of release 
approvers. These are the maintainers specifically added here: 
https://github.com/pycontribs/jira/settings/environments/333132378/edit. This 
release environment also limits the branches that can be deployed. To follow 
the tag version convention mentioned earlier.
+3. Finally this will automatically trigger the release CI action (as defined 
in our repository), this uses the relevant repository secrets to publish to 
PyPI.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/constraints.txt 
new/jira-3.8.0/constraints.txt
--- old/jira-3.6.0/constraints.txt      2024-01-05 18:17:52.000000000 +0100
+++ new/jira-3.8.0/constraints.txt      2024-03-25 13:16:36.000000000 +0100
@@ -12,7 +12,7 @@
     # via sphinx
 backcall==0.2.0
     # via ipython
-beautifulsoup4==4.12.2
+beautifulsoup4==4.12.3
     # via furo
 certifi==2023.11.17
     # via requests
@@ -71,7 +71,7 @@
     # via keyring
 jedi==0.19.1
     # via ipython
-jinja2==3.1.2
+jinja2==3.1.3
     # via sphinx
 keyring==24.3.0
     # via jira (setup.cfg)
@@ -192,7 +192,7 @@
     # via jira (setup.cfg)
 sphinxcontrib-applehelp==1.0.4
     # via sphinx
-sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-devhelp==1.0.6
     # via sphinx
 sphinxcontrib-htmlhelp==2.0.4
     # via sphinx
@@ -200,7 +200,7 @@
     # via sphinx
 sphinxcontrib-qthelp==1.0.6
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.9
+sphinxcontrib-serializinghtml==1.1.10
     # via sphinx
 sspilib==0.1.0
     # via pyspnego
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/jira/client.py 
new/jira-3.8.0/jira/client.py
--- old/jira-3.6.0/jira/client.py       2024-01-05 18:17:52.000000000 +0100
+++ new/jira-3.8.0/jira/client.py       2024-03-25 13:16:36.000000000 +0100
@@ -5,6 +5,7 @@
 will construct a JIRA object as described below. Full API documentation can be 
found
 at: https://jira.readthedocs.io/en/latest/.
 """
+
 from __future__ import annotations
 
 import calendar
@@ -49,7 +50,7 @@
 from requests_toolbelt import MultipartEncoder
 
 from jira import __version__
-from jira.exceptions import JIRAError
+from jira.exceptions import JIRAError, NotJIRAInstanceError
 from jira.resilientsession import PrepareRequestForRetry, ResilientSession
 from jira.resources import (
     AgileResource,
@@ -60,6 +61,9 @@
     Customer,
     CustomFieldOption,
     Dashboard,
+    DashboardGadget,
+    DashboardItemProperty,
+    DashboardItemPropertyKey,
     Field,
     Filter,
     Group,
@@ -92,7 +96,7 @@
     WorkflowScheme,
     Worklog,
 )
-from jira.utils import json_loads, threaded_requests
+from jira.utils import json_loads, remove_empty_attributes, threaded_requests
 
 try:
     from requests_jwt import JWTAuth
@@ -104,6 +108,82 @@
 LOG.addHandler(_logging.NullHandler())
 
 
+def cloud_api(client_method: Callable) -> Callable:
+    """A convenience decorator to check if the Jira instance is cloud.
+
+    Checks if the client instance is talking to Cloud Jira. If it is, return
+    the result of the called client method. If not, return None and log a
+    warning.
+
+    Args:
+      client_method: The method that is being called by the client.
+
+    Returns:
+      Either the result of the wrapped function or None.
+
+    Raises:
+      JIRAError: In the case the error is not an HTTP error with a status code.
+      NotJIRAInstanceError: In the case that the first argument to this method
+       is not a `client.JIRA` instance.
+    """
+    wraps(client_method)
+
+    def check_if_cloud(*args, **kwargs):
+        # The first argument of any class instance is a `self`
+        # reference. Avoiding magic numbers here.
+        instance = next(arg for arg in args)
+        if not isinstance(instance, JIRA):
+            raise NotJIRAInstanceError(instance)
+
+        if instance._is_cloud:
+            return client_method(*args, **kwargs)
+
+        instance.log.warning(
+            "This functionality is not available on Jira Data Center (Server) 
version."
+        )
+        return None
+
+    return check_if_cloud
+
+
+def experimental_atlassian_api(client_method: Callable) -> Callable:
+    """A convenience decorator to inform if a client method is experimental.
+
+    Indicates the path covered by the client method is experimental. If the 
path
+    disappears or the method becomes disallowed, this logs an error and returns
+    None. If another kind of exception is raised, this reraises.
+
+    Raises:
+      JIRAError: In the case the error is not an HTTP error with a status code.
+      NotJIRAInstanceError: In the case that the first argument to this method 
is
+       is not a `client.JIRA` instance.
+
+    Returns:
+      Either the result of the wrapped function or None.
+    """
+    wraps(client_method)
+
+    def is_experimental(*args, **kwargs):
+        instance = next(arg for arg in args)
+        if not isinstance(instance, JIRA):
+            raise NotJIRAInstanceError(instance)
+
+        try:
+            return client_method(*args, **kwargs)
+        except JIRAError as e:
+            response = getattr(e, "response", None)
+            if response is not None and response.status_code in [405, 404]:
+                instance.log.warning(
+                    f"Functionality at path {response.url} is/was 
experimental. "
+                    f"Status Code: {response.status_code}"
+                )
+                return None
+            else:
+                raise
+
+    return is_experimental
+
+
 def translate_resource_args(func: Callable):
     """Decorator that converts Issue and Project resources to their keys when 
used as arguments.
 
@@ -1180,7 +1260,7 @@
             maxResults (int): maximum number of dashboards to return. If 
maxResults set to False, it will try to get all items in batches. (Default: 
``20``)
 
         Returns:
-            ResultList
+            ResultList[Dashboard]
         """
         params = {}
         if filter is not None:
@@ -1203,7 +1283,253 @@
         Returns:
             Dashboard
         """
-        return self._find_for_resource(Dashboard, id)
+        dashboard = self._find_for_resource(Dashboard, id)
+        dashboard.gadgets.extend(self.dashboard_gadgets(id) or [])
+        return dashboard
+
+    @cloud_api
+    @experimental_atlassian_api
+    def create_dashboard(
+        self,
+        name: str,
+        description: str | None = None,
+        edit_permissions: list[dict] | list | None = None,
+        share_permissions: list[dict] | list | None = None,
+    ) -> Dashboard:
+        """Create a new dashboard and return a dashboard resource for it.
+
+        Args:
+            name (str): Name of the new dashboard `required`.
+            description (Optional[str]): Useful human-readable description of 
the new dashboard.
+            edit_permissions (list | list[dict]): A list of permissions dicts 
`required`
+                though can be an empty list.
+            share_permissions (list | list[dict]): A list of permissions dicts 
`required`
+                though can be an empty list.
+
+        Returns:
+            Dashboard
+        """
+        data: dict[str, Any] = remove_empty_attributes(
+            {
+                "name": name,
+                "editPermissions": edit_permissions or [],
+                "sharePermissions": share_permissions or [],
+                "description": description,
+            }
+        )
+        url = self._get_url("dashboard")
+        r = self._session.post(url, data=json.dumps(data))
+
+        raw_dashboard_json: dict[str, Any] = json_loads(r)
+        return Dashboard(self._options, self._session, raw=raw_dashboard_json)
+
+    @cloud_api
+    @experimental_atlassian_api
+    def copy_dashboard(
+        self,
+        id: str,
+        name: str,
+        description: str | None = None,
+        edit_permissions: list[dict] | list | None = None,
+        share_permissions: list[dict] | list | None = None,
+    ) -> Dashboard:
+        """Copy an existing dashboard.
+
+        Args:
+          id (str): The ``id`` of the ``Dashboard`` to copy.
+          name (str): Name of the new dashboard `required`.
+          description (Optional[str]): Useful human-readable description of 
the new dashboard.
+          edit_permissions (list | list[dict]): A list of permissions dicts 
`required`
+            though can be an empty list.
+          share_permissions (list | list[dict]): A list of permissions dicts 
`required`
+            though can be an empty list.
+
+        Returns:
+          Dashboard
+        """
+        data: dict[str, Any] = remove_empty_attributes(
+            {
+                "name": name,
+                "editPermissions": edit_permissions or [],
+                "sharePermissions": share_permissions or [],
+                "description": description,
+            }
+        )
+        url = self._get_url("dashboard")
+        url = f"{url}/{id}/copy"
+        r = self._session.post(url, json=data)
+
+        raw_dashboard_json: dict[str, Any] = json_loads(r)
+        return Dashboard(self._options, self._session, raw=raw_dashboard_json)
+
+    @cloud_api
+    @experimental_atlassian_api
+    def update_dashboard_automatic_refresh_minutes(
+        self, id: str, minutes: int
+    ) -> Response:
+        """Update the automatic refresh interval of a dashboard.
+
+        Args:
+          id (str): The ``id`` of the ``Dashboard`` to copy.
+          minutes (int): The frequency of the dashboard automatic refresh in 
minutes.
+
+        Returns:
+          Response
+        """
+        # The payload expects milliseconds, we are doing a conversion
+        # here as a convenience. Additionally, if the value is `0` then we are 
setting
+        # to `None` which will serialize to `null` in `json` which is what is
+        # expected if the user wants to turn it off.
+
+        value = minutes * 60000 if minutes else None
+        data = {"automaticRefreshMs": value}
+
+        url = self._get_internal_url(f"dashboards/{id}/automatic-refresh-ms")
+        return self._session.put(url, json=data)
+
+    def dashboard_item_property_keys(
+        self, dashboard_id: str, item_id: str
+    ) -> ResultList[DashboardItemPropertyKey]:
+        """Return a ResultList of a Dashboard gadget's property keys.
+
+        Args:
+          dashboard_id (str): ID of dashboard.
+          item_id (str): ID of dashboard item (``DashboardGadget``).
+
+        Returns:
+            ResultList[DashboardItemPropertyKey]
+        """
+        return self._fetch_pages(
+            DashboardItemPropertyKey,
+            "keys",
+            f"dashboard/{dashboard_id}/items/{item_id}/properties",
+        )
+
+    def dashboard_item_property(
+        self, dashboard_id: str, item_id: str, property_key: str
+    ) -> DashboardItemProperty:
+        """Get the item property for a specific dashboard item 
(DashboardGadget).
+
+        Args:
+          dashboard_id (str):  of the dashboard.
+          item_id (str): ID of the item (``DashboardGadget``) on the dashboard.
+          property_key (str): KEY of the gadget property.
+
+        Returns:
+            DashboardItemProperty
+        """
+        dashboard_item_property = self._find_for_resource(
+            DashboardItemProperty, (dashboard_id, item_id, property_key)
+        )
+        return dashboard_item_property
+
+    def set_dashboard_item_property(
+        self, dashboard_id: str, item_id: str, property_key: str, value: 
dict[str, Any]
+    ) -> DashboardItemProperty:
+        """Set a dashboard item property.
+
+        Args:
+          dashboard_id (str): Dashboard id.
+          item_id (str): ID of dashboard item (``DashboardGadget``) to add 
property_key to.
+          property_key (str): The key of the property to set.
+          value (dict[str, Any]): The dictionary containing the value of the 
property key.
+
+        Returns:
+          DashboardItemProperty
+        """
+        url = self._get_url(
+            
f"dashboard/{dashboard_id}/items/{item_id}/properties/{property_key}"
+        )
+        r = self._session.put(url, json=value)
+
+        if not r.ok:
+            raise JIRAError(status_code=r.status_code, request=r)
+        return self.dashboard_item_property(dashboard_id, item_id, 
property_key)
+
+    @cloud_api
+    @experimental_atlassian_api
+    def dashboard_gadgets(self, dashboard_id: str) -> list[DashboardGadget]:
+        """Return a list of DashboardGadget resources for the specified 
dashboard.
+
+        Args:
+          dashboard_id (str): the ``dashboard_id`` of the dashboard to get 
gadgets for
+
+        Returns:
+          list[DashboardGadget]
+        """
+        gadgets: list[DashboardGadget] = []
+        gadgets = self._fetch_pages(
+            DashboardGadget, "gadgets", f"dashboard/{dashboard_id}/gadget"
+        )
+        for gadget in gadgets:
+            for dashboard_item_key in self.dashboard_item_property_keys(
+                dashboard_id, gadget.id
+            ):
+                gadget.item_properties.append(
+                    self.dashboard_item_property(
+                        dashboard_id, gadget.id, dashboard_item_key.key
+                    )
+                )
+
+        return gadgets
+
+    @cloud_api
+    @experimental_atlassian_api
+    def all_dashboard_gadgets(self) -> ResultList[DashboardGadget]:
+        """Return a ResultList of available DashboardGadget resources and a 
``total`` count.
+
+        Returns:
+            ResultList[DashboardGadget]
+        """
+        return self._fetch_pages(DashboardGadget, "gadgets", 
"dashboard/gadgets")
+
+    @cloud_api
+    @experimental_atlassian_api
+    def add_gadget_to_dashboard(
+        self,
+        dashboard_id: str | Dashboard,
+        color: str | None = None,
+        ignore_uri_and_module_key_validation: bool | None = None,
+        module_key: str | None = None,
+        position: dict[str, int] | None = None,
+        title: str | None = None,
+        uri: str | None = None,
+    ) -> DashboardGadget:
+        """Add a gadget to a dashboard and return a ``DashboardGadget`` 
resource.
+
+        Args:
+          dashboard_id (str): The ``dashboard_id`` of the dashboard to add the 
gadget to `required`.
+          color (str): The color of the gadget, should be one of: blue, red, 
yellow,
+              green, cyan, purple, gray, or white.
+          ignore_uri_and_module_key_validation (bool): Whether to ignore the
+              validation of the module key and URI. For example, when a gadget 
is created
+              that is part of an application that is not installed.
+          module_key (str): The module to use in the gadget. Mutually 
exclusive with
+              `uri`.
+          position (dict[str, int]): A dictionary containing position 
information like -
+              `{"column": 0, "row", 1}`.
+          title (str): The title of the gadget.
+          uri (str): The uri to the module to use in the gadget. Mutually 
exclusive
+              with `module_key`.
+
+        Returns:
+          DashboardGadget
+        """
+        data = remove_empty_attributes(
+            {
+                "color": color,
+                "ignoreUriAndModuleKeyValidation": 
ignore_uri_and_module_key_validation,
+                "module_key": module_key,
+                "position": position,
+                "title": title,
+                "uri": uri,
+            }
+        )
+        url = self._get_url(f"dashboard/{dashboard_id}/gadget")
+        r = self._session.post(url, json=data)
+
+        raw_gadget_json: dict[str, Any] = json_loads(r)
+        return DashboardGadget(self._options, self._session, 
raw=raw_gadget_json)
 
     # Fields
 
@@ -1392,11 +1718,13 @@
             hasId = user.get("id") is not None and user.get("id") != ""
             hasName = user.get("name") is not None and user.get("name") != ""
             result[
-                user["id"]
-                if hasId
-                else user.get("name")
-                if hasName
-                else user.get("accountId")
+                (
+                    user["id"]
+                    if hasId
+                    else user.get("name")
+                    if hasName
+                    else user.get("accountId")
+                )
             ] = {
                 "name": user.get("name"),
                 "id": user.get("id"),
@@ -1748,6 +2076,86 @@
                 "Use 'createmeta' instead."
             )
 
+    def createmeta_issuetypes(
+        self,
+        projectIdOrKey: str | int,
+        startAt: int = 0,
+        maxResults: int = 50,
+    ) -> dict[str, Any]:
+        """Get the issue types metadata for a given project, required to 
create issues.
+
+        .. deprecated:: 3.6.0
+            Use :func:`project_issue_types` instead.
+
+        This API was introduced in JIRA Server / DC 8.4 as a replacement for 
the more general purpose API 'createmeta'.
+        For details see: 
https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
+
+        Args:
+            projectIdOrKey (Union[str, int]): id or key of the project for 
which to get the metadata.
+            startAt (int): Index of the first issue to return. (Default: ``0``)
+            maxResults (int): Maximum number of issues to return.
+              Total number of results is available in the ``total`` attribute 
of the returned :class:`ResultList`.
+              If maxResults evaluates to False, it will try to get all issues 
in batches. (Default: ``50``)
+
+        Returns:
+            Dict[str, Any]
+        """
+        warnings.warn(
+            "'createmeta_issuetypes' is deprecated and will be removed in 
future releases."
+            "Use 'project_issue_types' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        self._check_createmeta_issuetypes()
+        return self._get_json(
+            f"issue/createmeta/{projectIdOrKey}/issuetypes",
+            params={
+                "startAt": startAt,
+                "maxResults": maxResults,
+            },
+        )
+
+    def createmeta_fieldtypes(
+        self,
+        projectIdOrKey: str | int,
+        issueTypeId: str | int,
+        startAt: int = 0,
+        maxResults: int = 50,
+    ) -> dict[str, Any]:
+        """Get the field metadata for a given project and issue type, required 
to create issues.
+
+        .. deprecated:: 3.6.0
+            Use :func:`project_issue_fields` instead.
+
+        This API was introduced in JIRA Server / DC 8.4 as a replacement for 
the more general purpose API 'createmeta'.
+        For details see: 
https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
+
+        Args:
+            projectIdOrKey (Union[str, int]): id or key of the project for 
which to get the metadata.
+            issueTypeId (Union[str, int]): id of the issue type for which to 
get the metadata.
+            startAt (int): Index of the first issue to return. (Default: ``0``)
+            maxResults (int): Maximum number of issues to return.
+              Total number of results is available in the ``total`` attribute 
of the returned :class:`ResultList`.
+              If maxResults evaluates to False, it will try to get all issues 
in batches. (Default: ``50``)
+
+        Returns:
+            Dict[str, Any]
+        """
+        warnings.warn(
+            "'createmeta_fieldtypes' is deprecated and will be removed in 
future releases."
+            "Use 'project_issue_fields' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        self._check_createmeta_issuetypes()
+        return self._get_json(
+            f"issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}",
+            params={
+                "startAt": startAt,
+                "maxResults": maxResults,
+            },
+        )
+
     def createmeta(
         self,
         projectKeys: tuple[str, str] | str | None = None,
@@ -1776,12 +2184,12 @@
             if self._version >= (9, 0, 0):
                 raise JIRAError(
                     f"Unsupported JIRA version: {self._version}. "
-                    "Use 'createmeta_issuetypes' and 'createmeta_fieldtypes' 
instead."
+                    "Use 'project_issue_types' and 'project_issue_fields' 
instead."
                 )
             elif self._version >= (8, 4, 0):
                 warnings.warn(
                     "This API have been deprecated in JIRA 8.4 and is removed 
in JIRA 9.0. "
-                    "Use 'createmeta_issuetypes' and 'createmeta_fieldtypes' 
instead.",
+                    "Use 'project_issue_types' and 'project_issue_fields' 
instead.",
                     DeprecationWarning,
                     stacklevel=2,
                 )
@@ -2377,15 +2785,15 @@
     def add_worklog(
         self,
         issue: str | int,
-        timeSpent: (str | None) = None,
-        timeSpentSeconds: (str | None) = None,
-        adjustEstimate: (str | None) = None,
-        newEstimate: (str | None) = None,
-        reduceBy: (str | None) = None,
-        comment: (str | None) = None,
-        started: (datetime.datetime | None) = None,
-        user: (str | None) = None,
-        visibility: (dict[str, Any] | None) = None,
+        timeSpent: str | None = None,
+        timeSpentSeconds: str | None = None,
+        adjustEstimate: str | None = None,
+        newEstimate: str | None = None,
+        reduceBy: str | None = None,
+        comment: str | None = None,
+        started: datetime.datetime | None = None,
+        user: str | None = None,
+        visibility: dict[str, Any] | None = None,
     ) -> Worklog:
         """Add a new worklog entry on an issue and return a Resource for it.
 
@@ -3875,6 +4283,24 @@
         data = {"id": avatar}
         return self._session.put(url, params=params, data=json.dumps(data))
 
+    def _get_internal_url(self, path: str, base: str = JIRA_BASE_URL) -> str:
+        """Returns the full internal api url based on Jira base url and the 
path provided.
+
+        Using the API version specified during the __init__.
+
+        Args:
+          path (str): The subpath desired.
+          base (Optional[str]): The base url which should be prepended to the 
path
+
+        Returns:
+          str: Fully qualified URL
+        """
+        options = self._options.copy()
+        options.update(
+            {"path": path, "rest_api_version": "latest", "rest_path": 
"internal"}
+        )
+        return base.format(**options)
+
     def _get_url(self, path: str, base: str = JIRA_BASE_URL) -> str:
         """Returns the full url based on Jira base url and the path provided.
 
@@ -3941,7 +4367,7 @@
     def _find_for_resource(
         self,
         resource_cls: Any,
-        ids: tuple[str, str] | tuple[str | int, str] | int | str,
+        ids: tuple[str, ...] | tuple[str | int, str] | int | str,
         expand=None,
     ) -> Any:
         """Uses the find method of the provided Resource class.
@@ -4896,6 +5322,7 @@
         startDate: Any | None = None,
         endDate: Any | None = None,
         state: str | None = None,
+        goal: str | None = None,
     ) -> dict[str, Any]:
         """Updates the sprint with the given values.
 
@@ -4904,7 +5331,8 @@
             name (Optional[str]): The name to update your sprint to
             startDate (Optional[Any]): The start date for the sprint
             endDate (Optional[Any]): The start date for the sprint
-            state: (Optional[str]): The start date for the sprint
+            state: (Optional[str]): The state of the sprint
+            goal: (Optional[str]): The goal of the sprint
 
         Returns:
             Dict[str, Any]
@@ -4918,6 +5346,8 @@
             payload["endDate"] = endDate
         if state:
             payload["state"] = state
+        if goal:
+            payload["goal"] = goal
 
         url = self._get_url(f"sprint/{id}", base=self.AGILE_BASE_URL)
         r = self._session.put(url, data=json.dumps(payload))
@@ -5047,6 +5477,7 @@
         board_id: int,
         startDate: Any | None = None,
         endDate: Any | None = None,
+        goal: str | None = None,
     ) -> Sprint:
         """Create a new sprint for the ``board_id``.
 
@@ -5055,6 +5486,7 @@
             board_id (int): Which board the sprint should be assigned.
             startDate (Optional[Any]): Start date for the sprint.
             endDate (Optional[Any]): End date for the sprint.
+            goal (Optional[str]): Goal for the sprint.
 
         Returns:
             Sprint: The newly created Sprint
@@ -5064,14 +5496,16 @@
             payload["startDate"] = startDate
         if endDate:
             payload["endDate"] = endDate
+        if goal:
+            payload["goal"] = goal
 
-        raw_issue_json: dict[str, Any]
+        raw_sprint_json: dict[str, Any]
         url = self._get_url("sprint", base=self.AGILE_BASE_URL)
         payload["originBoardId"] = board_id
         r = self._session.post(url, data=json.dumps(payload))
-        raw_issue_json = json_loads(r)
+        raw_sprint_json = json_loads(r)
 
-        return Sprint(self._options, self._session, raw=raw_issue_json)
+        return Sprint(self._options, self._session, raw=raw_sprint_json)
 
     def add_issues_to_sprint(self, sprint_id: int, issue_keys: list[str]) -> 
Response:
         """Add the issues in ``issue_keys`` to the ``sprint_id``.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/jira/exceptions.py 
new/jira-3.8.0/jira/exceptions.py
--- old/jira-3.6.0/jira/exceptions.py   2024-01-05 18:17:52.000000000 +0100
+++ new/jira-3.8.0/jira/exceptions.py   2024-03-25 13:16:36.000000000 +0100
@@ -2,6 +2,7 @@
 
 import os
 import tempfile
+from typing import Any
 
 from requests import Response
 
@@ -69,3 +70,14 @@
             t += f"\n\t{details}"
 
         return t
+
+
+class NotJIRAInstanceError(Exception):
+    """Raised in the case an object is not a JIRA instance."""
+
+    def __init__(self, instance: Any):
+        msg = (
+            "The first argument of this function must be an instance of type "
+            f"JIRA. Instance Type: {instance.__class__.__name__}"
+        )
+        super().__init__(msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/jira/resilientsession.py 
new/jira-3.8.0/jira/resilientsession.py
--- old/jira-3.6.0/jira/resilientsession.py     2024-01-05 18:17:52.000000000 
+0100
+++ new/jira-3.8.0/jira/resilientsession.py     2024-03-25 13:16:36.000000000 
+0100
@@ -315,7 +315,9 @@
             if response.status_code in recoverable_error_codes:
                 retry_after = response.headers.get("Retry-After")
                 if retry_after:
-                    suggested_delay = int(retry_after)  # Do as told
+                    suggested_delay = 2 * max(
+                        int(retry_after), 1
+                    )  # Do as told but always wait at least a little
                 elif response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
                     suggested_delay = 10 * 2**counter  # Exponential backoff
 
@@ -326,7 +328,9 @@
         is_recoverable = suggested_delay > 0
         if is_recoverable:
             # Apply jitter to prevent thundering herd
-            delay = min(self.max_retry_delay, suggested_delay) * 
random.random()
+            delay = min(self.max_retry_delay, suggested_delay) * 
random.uniform(
+                0.5, 1.0
+            )
             LOG.warning(
                 f"Got recoverable error from {request_method} {url}, will 
retry [{counter}/{self.max_retries}] in {delay}s. Err: {msg}"  # type: 
ignore[str-bytes-safe]
             )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/jira/resources.py 
new/jira-3.8.0/jira/resources.py
--- old/jira-3.6.0/jira/resources.py    2024-01-05 18:17:52.000000000 +0100
+++ new/jira-3.8.0/jira/resources.py    2024-03-25 13:16:36.000000000 +0100
@@ -3,6 +3,7 @@
 This module implements the Resource classes that translate JSON from Jira REST
 resources into usable objects.
 """
+
 from __future__ import annotations
 
 import json
@@ -15,7 +16,7 @@
 from requests.structures import CaseInsensitiveDict
 
 from jira.resilientsession import ResilientSession, parse_errors
-from jira.utils import json_loads, threaded_requests
+from jira.utils import json_loads, remove_empty_attributes, threaded_requests
 
 if TYPE_CHECKING:
     from jira.client import JIRA
@@ -37,7 +38,10 @@
     "Attachment",
     "Component",
     "Dashboard",
+    "DashboardItemProperty",
+    "DashboardItemPropertyKey",
     "Filter",
+    "DashboardGadget",
     "Votes",
     "PermissionScheme",
     "Watchers",
@@ -239,7 +243,7 @@
 
     def find(
         self,
-        id: tuple[str, str] | int | str,
+        id: tuple[str, ...] | int | str,
         params: dict[str, str] | None = None,
     ):
         """Finds a resource based on the input parameters.
@@ -552,8 +556,157 @@
         Resource.__init__(self, "dashboard/{0}", options, session)
         if raw:
             self._parse_raw(raw)
+        self.gadgets: list[DashboardGadget] = []
+        self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)
+
+
+class DashboardItemPropertyKey(Resource):
+    """A jira dashboard item property key."""
+
+    def __init__(
+        self,
+        options: dict[str, str],
+        session: ResilientSession,
+        raw: dict[str, Any] = None,
+    ):
+        Resource.__init__(self, "dashboard/{0}/items/{1}/properties", options, 
session)
+        if raw:
+            self._parse_raw(raw)
+        self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)
+
+
+class DashboardItemProperty(Resource):
+    """A jira dashboard item."""
+
+    def __init__(
+        self,
+        options: dict[str, str],
+        session: ResilientSession,
+        raw: dict[str, Any] = None,
+    ):
+        Resource.__init__(
+            self, "dashboard/{0}/items/{1}/properties/{2}", options, session
+        )
+        if raw:
+            self._parse_raw(raw)
+        self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)
+
+    def update(  # type: ignore[override] # incompatible supertype ignored
+        self, dashboard_id: str, item_id: str, value: dict[str, Any]
+    ) -> DashboardItemProperty:
+        """Update this resource on the server.
+
+        Keyword arguments are marshalled into a dict before being sent. If 
this resource doesn't support ``PUT``, a :py:exc:`.JIRAError`
+        will be raised; subclasses that specialize this method will only raise 
errors in case of user error.
+
+        Args:
+          dashboard_id (str): The ``id`` if the dashboard.
+          item_id (str): The id of the dashboard item (``DashboardGadget``) to 
target.
+          value (dict[str, Any]): The value of the targeted property key.
+
+        Returns:
+          DashboardItemProperty
+        """
+        options = self._options.copy()
+        options[
+            "path"
+        ] = f"dashboard/{dashboard_id}/items/{item_id}/properties/{self.key}"
+        self.raw["value"].update(value)
+        self._session.put(self.JIRA_BASE_URL.format(**options), 
self.raw["value"])
+
+        return DashboardItemProperty(self._options, self._session, 
raw=self.raw)
+
+    def delete(self, dashboard_id: str, item_id: str) -> Response:  # type: 
ignore[override] # incompatible supertype ignored
+        """Delete dashboard item property.
+
+        Args:
+          dashboard_id (str): The ``id`` of the dashboard.
+          item_id (str): The ``id`` of the dashboard item 
(``DashboardGadget``).
+
+
+        Returns:
+          Response
+        """
+        options = self._options.copy()
+        options[
+            "path"
+        ] = f"dashboard/{dashboard_id}/items/{item_id}/properties/{self.key}"
+
+        return self._session.delete(self.JIRA_BASE_URL.format(**options))
+
+
+class DashboardGadget(Resource):
+    """A jira dashboard gadget."""
+
+    def __init__(
+        self,
+        options: dict[str, str],
+        session: ResilientSession,
+        raw: dict[str, Any] = None,
+    ):
+        Resource.__init__(self, "dashboard/{0}/gadget/{1}", options, session)
+        if raw:
+            self._parse_raw(raw)
+        self.item_properties: list[DashboardItemProperty] = []
         self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)
 
+    def update(  # type: ignore[override] # incompatible supertype ignored
+        self,
+        dashboard_id: str,
+        color: str | None = None,
+        position: dict[str, Any] | None = None,
+        title: str | None = None,
+    ) -> DashboardGadget:
+        """Update this resource on the server.
+
+        Keyword arguments are marshalled into a dict before being sent. If 
this resource doesn't support ``PUT``, a :py:exc:`.JIRAError`
+        will be raised; subclasses that specialize this method will only raise 
errors in case of user error.
+
+        Args:
+          dashboard_id (str): The ``id`` of the dashboard to add the gadget to 
`required`.
+          color (str): The color of the gadget, should be one of: blue, red, 
yellow,
+              green, cyan, purple, gray, or white.
+          ignore_uri_and_module_key_validation (bool): Whether to ignore the
+              validation of the module key and URI. For example, when a gadget 
is created
+              that is part of an application that is not installed.
+          position (dict[str, int]): A dictionary containing position 
information like -
+              `{"column": 0, "row", 1}`.
+          title (str): The title of the gadget.
+
+        Returns:
+          ``DashboardGadget``
+        """
+        data = remove_empty_attributes(
+            {"color": color, "position": position, "title": title}
+        )
+        options = self._options.copy()
+        options["path"] = f"dashboard/{dashboard_id}/gadget/{self.id}"
+
+        self._session.put(self.JIRA_BASE_URL.format(**options), json=data)
+        options["path"] = f"dashboard/{dashboard_id}/gadget"
+
+        return next(
+            DashboardGadget(self._options, self._session, raw=gadget)
+            for gadget in self._session.get(
+                self.JIRA_BASE_URL.format(**options)
+            ).json()["gadgets"]
+            if gadget["id"] == self.id
+        )
+
+    def delete(self, dashboard_id: str) -> Response:  # type: ignore[override] 
# incompatible supertype ignored
+        """Delete gadget from dashboard.
+
+        Args:
+          dashboard_id (str): The ``id`` of the dashboard.
+
+        Returns:
+          Response
+        """
+        options = self._options.copy()
+        options["path"] = f"dashboard/{dashboard_id}/gadget/{self.id}"
+
+        return self._session.delete(self.JIRA_BASE_URL.format(**options))
+
 
 class Field(Resource):
     """An issue field.
@@ -1492,6 +1645,9 @@
     r"component/[^/]+$": Component,
     r"customFieldOption/[^/]+$": CustomFieldOption,
     r"dashboard/[^/]+$": Dashboard,
+    r"dashboard/[^/]+/items/[^/]+/properties+$": DashboardItemPropertyKey,
+    r"dashboard/[^/]+/items/[^/]+/properties/[^/]+$": DashboardItemProperty,
+    r"dashboard/[^/]+/gadget/[^/]+$": DashboardGadget,
     r"filter/[^/]$": Filter,
     r"issue/[^/]+$": Issue,
     r"issue/[^/]+/comment/[^/]+$": Comment,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/jira/utils/__init__.py 
new/jira-3.8.0/jira/utils/__init__.py
--- old/jira-3.6.0/jira/utils/__init__.py       2024-01-05 18:17:52.000000000 
+0100
+++ new/jira-3.8.0/jira/utils/__init__.py       2024-03-25 13:16:36.000000000 
+0100
@@ -1,4 +1,5 @@
 """Jira utils used internally."""
+
 from __future__ import annotations
 
 import threading
@@ -79,3 +80,15 @@
         if not resp.text:
             return {}
         raise
+
+
+def remove_empty_attributes(data: dict[str, Any]) -> dict[str, Any]:
+    """A convenience function to remove key/value pairs with `None` for a 
value.
+
+    Args:
+      data: A dictionary.
+
+    Returns:
+      Dict[str, Any]: A dictionary with no `None` key/value pairs.
+    """
+    return {key: val for key, val in data.items() if val is not None}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jira-3.6.0/jira.egg-info/PKG-INFO 
new/jira-3.8.0/jira.egg-info/PKG-INFO
--- old/jira-3.6.0/jira.egg-info/PKG-INFO       2024-01-05 18:18:04.000000000 
+0100
+++ new/jira-3.8.0/jira.egg-info/PKG-INFO       2024-03-25 13:16:45.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: jira
-Version: 3.6.0
+Version: 3.8.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.6.0/jira.egg-info/SOURCES.txt 
new/jira-3.8.0/jira.egg-info/SOURCES.txt
--- old/jira-3.6.0/jira.egg-info/SOURCES.txt    2024-01-05 18:18:04.000000000 
+0100
+++ new/jira-3.8.0/jira.egg-info/SOURCES.txt    2024-03-25 13:16:45.000000000 
+0100
@@ -8,6 +8,7 @@
 LICENSE
 MANIFEST.in
 README.rst
+RELEASE.md
 codecov.yml
 constraints.txt
 make_local_jira_user.py

Reply via email to