Skip to content

Commit

Permalink
Fetch upstream & merge Take 2
Browse files Browse the repository at this point in the history
___
Jannik Meinecke (<jannik.meinecke@mercedes-benz.com>) on behalf of MBition GmbH.
https://github.com/mercedes-benz/foss/blob/master/PROVIDER_INFORMATION.md
  • Loading branch information
rynkk committed Jun 29, 2022
2 parents 4332cc1 + 5515bf3 commit 6204865
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 247 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repos:
- id: black
language_version: python3
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
rev: v4.3.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
Expand Down Expand Up @@ -46,15 +46,15 @@ repos:
- id: yamllint
files: \.(yaml|yml)$
- repo: https://github.com/pre-commit/mirrors-mypy.git
rev: v0.950
rev: v0.961
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.32.1
rev: v2.34.0
hooks:
- id: pyupgrade
args: [--py38-plus]
10 changes: 5 additions & 5 deletions constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ babel==2.10.1
# via sphinx
backcall==0.2.0
# via ipython
certifi==2021.10.8
certifi==2022.5.18.1
# via requests
cffi==1.15.0
# via cryptography
Expand All @@ -27,9 +27,9 @@ colorama==0.4.4
# ipython
# pytest
# sphinx
coverage==6.3.2
coverage==6.4.1
# via pytest-cov
cryptography==37.0.1
cryptography==37.0.2
# via
# pyspnego
# requests-kerberos
Expand Down Expand Up @@ -106,7 +106,7 @@ pygments==2.12.0
# via
# ipython
# sphinx
pyjwt==2.3.0
pyjwt==2.4.0
# via
# jira (setup.cfg)
# requests-jwt
Expand Down Expand Up @@ -175,7 +175,7 @@ six==1.16.0
# requests-mock
snowballstemmer==2.2.0
# via sphinx
sphinx==4.5.0
sphinx==5.0.1
# via
# jira (setup.cfg)
# sphinx-rtd-theme
Expand Down
3 changes: 3 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
nitpick_ignore = [
("py:class", "JIRA"), # in jira.resources we only import this class if type
("py:class", "jira.resources.AnyLike"), # Dummy subclass for type checking
("py:meth", "__recoverable"), # ResilientSession, not autogenerated
# From other packages
("py:mod", "filemagic"),
("py:mod", "ipython"),
Expand All @@ -69,6 +70,8 @@
("py:class", "Response"),
("py:mod", "requests-kerberos"),
("py:mod", "requests-oauthlib"),
("py:class", "typing_extensions.TypeGuard"), # Py38 not happy with this typehint
("py:class", "TypeGuard"), # Py38 not happy with 'TypeGuard' in docstring
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
10 changes: 10 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,16 @@ Adding, editing and deleting comments is similarly straightforward::
comment.update(body='updated comment body but no mail notification', notify=False)
comment.delete()

Get all images from a comment::

issue = jira.issue('JRA-1330')
regex_for_png = re.compile(r'\!(\S+?\.(jpg|png|bmp))\|?\S*?\!')
pngs_used_in_comment = regex_for_png.findall(issue.fields.comment.comments[0].body)
for attachment in issue.fields.attachment:
if attachment.filename in pngs_used_in_comment:
with open(attachment.filename, 'wb') as f:
f.write(attachment.get())

Transitions
-----------

Expand Down
144 changes: 77 additions & 67 deletions jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
Type,
TypeVar,
Union,
cast,
no_type_check,
overload,
)
Expand All @@ -48,10 +47,11 @@
from requests.auth import AuthBase
from requests.structures import CaseInsensitiveDict
from requests.utils import get_netrc_auth
from requests_toolbelt import MultipartEncoder

from jira import __version__
from jira.exceptions import JIRAError
from jira.resilientsession import ResilientSession, raise_on_error
from jira.resilientsession import PrepareRequestForRetry, ResilientSession
from jira.resources import (
AgileResource,
Attachment,
Expand Down Expand Up @@ -93,12 +93,6 @@
)
from jira.utils import json_loads, threaded_requests

try:
# noinspection PyUnresolvedReferences
from requests_toolbelt import MultipartEncoder
except ImportError:
pass

try:
from requests_jwt import JWTAuth
except ImportError:
Expand Down Expand Up @@ -1000,70 +994,68 @@ def add_attachment(
"""
close_attachment = False
if isinstance(attachment, str):
attachment: BufferedReader = open(attachment, "rb") # type: ignore
attachment = cast(BufferedReader, attachment)
attachment_io = open(attachment, "rb") # type: ignore
close_attachment = True
elif isinstance(attachment, BufferedReader) and attachment.mode != "rb":
self.log.warning(
"%s was not opened in 'rb' mode, attaching file may fail."
% attachment.name
)

url = self._get_url("issue/" + str(issue) + "/attachments")
else:
attachment_io = attachment
if isinstance(attachment, BufferedReader) and attachment.mode != "rb":
self.log.warning(
"%s was not opened in 'rb' mode, attaching file may fail."
% attachment.name
)

fname = filename
if not fname and isinstance(attachment, BufferedReader):
fname = os.path.basename(attachment.name)

if "MultipartEncoder" not in globals():
method = "old"
try:
r = self._session.post(
url,
files={"file": (fname, attachment, "application/octet-stream")},
headers=CaseInsensitiveDict(
{"content-type": None, "X-Atlassian-Token": "no-check"}
),
)
finally:
if close_attachment:
attachment.close()
else:
method = "MultipartEncoder"

def file_stream() -> MultipartEncoder:
"""Returns files stream of attachment."""
return MultipartEncoder(
fields={"file": (fname, attachment, "application/octet-stream")}
)

m = file_stream()
try:
r = self._session.post(
url,
data=m,
headers=CaseInsensitiveDict(
{
"content-type": m.content_type,
"X-Atlassian-Token": "no-check",
}
),
retry_data=file_stream,
)
finally:
if close_attachment:
attachment.close()
def generate_multipartencoded_request_args() -> Tuple[
MultipartEncoder, CaseInsensitiveDict
]:
"""Returns MultipartEncoder stream of attachment, and the header."""
attachment_io.seek(0)
encoded_data = MultipartEncoder(
fields={"file": (fname, attachment_io, "application/octet-stream")}
)
request_headers = CaseInsensitiveDict(
{
"content-type": encoded_data.content_type,
"X-Atlassian-Token": "no-check",
}
)
return encoded_data, request_headers

class RetryableMultipartEncoder(PrepareRequestForRetry):
def prepare(
self, original_request_kwargs: CaseInsensitiveDict
) -> CaseInsensitiveDict:
encoded_data, request_headers = generate_multipartencoded_request_args()
original_request_kwargs["data"] = encoded_data
original_request_kwargs["headers"] = request_headers
return super().prepare(original_request_kwargs)

url = self._get_url(f"issue/{issue}/attachments")
try:
encoded_data, request_headers = generate_multipartencoded_request_args()
r = self._session.post(
url,
data=encoded_data,
headers=request_headers,
_prepare_retry_class=RetryableMultipartEncoder(), # type: ignore[call-arg] # ResilientSession handles
)
finally:
if close_attachment:
attachment_io.close()

js: Union[Dict[str, Any], List[Dict[str, Any]]] = json_loads(r)
if not js or not isinstance(js, Iterable):
raise JIRAError(f"Unable to parse JSON: {js}")
raise JIRAError(f"Unable to parse JSON: {js}. Failed to add attachment?")
jira_attachment = Attachment(
self._options, self._session, js[0] if isinstance(js, List) else js
)
if jira_attachment.size == 0:
raise JIRAError(
"Added empty attachment via %s method?!: r: %s\nattachment: %s"
% (method, r, jira_attachment)
"Added empty attachment?!: "
+ f"Response: {r}\nAttachment: {jira_attachment}"
)
return jira_attachment

Expand Down Expand Up @@ -1836,8 +1828,7 @@ def assign_issue(self, issue: Union[int, str], assignee: Optional[str]) -> bool:
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))
raise_on_error(r)
self._session.put(url, data=json.dumps(payload))
return True

@translate_resource_args
Expand Down Expand Up @@ -2717,7 +2708,7 @@ def create_temp_project_avatar(
if size != size_from_file:
size = size_from_file

params = {"filename": filename, "size": size}
params: Dict[str, Union[int, str]] = {"filename": filename, "size": size}

headers: Dict[str, Any] = {"X-Atlassian-Token": "no-check"}
if contentType is not None:
Expand Down Expand Up @@ -3227,7 +3218,11 @@ def create_temp_user_avatar(
# remove path from filename
filename = os.path.split(filename)[1]

params = {"username": user, "filename": filename, "size": size}
params: Dict[str, Union[str, int]] = {
"username": user,
"filename": filename,
"size": size,
}

headers: Dict[str, Any]
headers = {"X-Atlassian-Token": "no-check"}
Expand Down Expand Up @@ -3791,8 +3786,7 @@ def rename_user(self, old_user: str, new_user: str):
# raw displayName
self.log.debug(f"renaming {self.user(old_user).emailAddress}")

r = self._session.put(url, params=params, data=json.dumps(payload))
raise_on_error(r)
self._session.put(url, params=params, data=json.dumps(payload))
else:
raise NotImplementedError(
"Support for renaming users in Jira " "< 6.0.0 has been removed."
Expand Down Expand Up @@ -4612,13 +4606,29 @@ def sprints(
self.AGILE_BASE_URL,
)

def sprints_by_name(self, id, extended=False):
def sprints_by_name(
self, id: Union[str, int], extended: bool = False, state: str = None
) -> Dict[str, Dict[str, Any]]:
"""Get a dictionary of sprint Resources where the name of the sprint is the key.
Args:
board_id (int): the board to get sprints from
extended (bool): Deprecated.
state (str): Filters results to sprints in specified states. Valid values: `future`, `active`, `closed`.
You can define multiple states separated by commas
Returns:
Dict[str, Dict[str, Any]]: dictionary of sprints with the sprint name as key
"""
sprints = {}
for s in self.sprints(id, extended=extended):
for s in self.sprints(id, extended=extended, state=state):
if s.name not in sprints:
sprints[s.name] = s.raw
else:
raise Exception
raise JIRAError(
f"There are multiple sprints defined with the name {s.name} on board id {id},\n"
f"returning a dict with sprint names as a key, assumes unique names for each sprint"
)
return sprints

def update_sprint(self, id, name=None, startDate=None, endDate=None, state=None):
Expand Down
Loading

0 comments on commit 6204865

Please sign in to comment.