Skip to content

Commit 6204865

Browse files
committed
Fetch upstream & merge Take 2
___ Jannik Meinecke (<[email protected]>) on behalf of MBition GmbH. https://github.com/mercedes-benz/foss/blob/master/PROVIDER_INFORMATION.md
2 parents 4332cc1 + 5515bf3 commit 6204865

10 files changed

+415
-247
lines changed

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ repos:
66
- id: black
77
language_version: python3
88
- repo: https://github.com/pre-commit/pre-commit-hooks
9-
rev: v4.2.0
9+
rev: v4.3.0
1010
hooks:
1111
- id: end-of-file-fixer
1212
- id: trailing-whitespace
@@ -46,15 +46,15 @@ repos:
4646
- id: yamllint
4747
files: \.(yaml|yml)$
4848
- repo: https://github.com/pre-commit/mirrors-mypy.git
49-
rev: v0.950
49+
rev: v0.961
5050
hooks:
5151
- id: mypy
5252
additional_dependencies:
5353
- types-requests
5454
- types-pkg_resources
5555
args: [--no-strict-optional, --ignore-missing-imports, --show-error-codes]
5656
- repo: https://github.com/asottile/pyupgrade
57-
rev: v2.32.1
57+
rev: v2.34.0
5858
hooks:
5959
- id: pyupgrade
6060
args: [--py38-plus]

constraints.txt

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ babel==2.10.1
1616
# via sphinx
1717
backcall==0.2.0
1818
# via ipython
19-
certifi==2021.10.8
19+
certifi==2022.5.18.1
2020
# via requests
2121
cffi==1.15.0
2222
# via cryptography
@@ -27,9 +27,9 @@ colorama==0.4.4
2727
# ipython
2828
# pytest
2929
# sphinx
30-
coverage==6.3.2
30+
coverage==6.4.1
3131
# via pytest-cov
32-
cryptography==37.0.1
32+
cryptography==37.0.2
3333
# via
3434
# pyspnego
3535
# requests-kerberos
@@ -106,7 +106,7 @@ pygments==2.12.0
106106
# via
107107
# ipython
108108
# sphinx
109-
pyjwt==2.3.0
109+
pyjwt==2.4.0
110110
# via
111111
# jira (setup.cfg)
112112
# requests-jwt
@@ -175,7 +175,7 @@ six==1.16.0
175175
# requests-mock
176176
snowballstemmer==2.2.0
177177
# via sphinx
178-
sphinx==4.5.0
178+
sphinx==5.0.1
179179
# via
180180
# jira (setup.cfg)
181181
# sphinx-rtd-theme

docs/conf.py

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
nitpick_ignore = [
5757
("py:class", "JIRA"), # in jira.resources we only import this class if type
5858
("py:class", "jira.resources.AnyLike"), # Dummy subclass for type checking
59+
("py:meth", "__recoverable"), # ResilientSession, not autogenerated
5960
# From other packages
6061
("py:mod", "filemagic"),
6162
("py:mod", "ipython"),
@@ -69,6 +70,8 @@
6970
("py:class", "Response"),
7071
("py:mod", "requests-kerberos"),
7172
("py:mod", "requests-oauthlib"),
73+
("py:class", "typing_extensions.TypeGuard"), # Py38 not happy with this typehint
74+
("py:class", "TypeGuard"), # Py38 not happy with 'TypeGuard' in docstring
7275
]
7376

7477
# Add any paths that contain templates here, relative to this directory.

docs/examples.rst

+10
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ Adding, editing and deleting comments is similarly straightforward::
336336
comment.update(body='updated comment body but no mail notification', notify=False)
337337
comment.delete()
338338

339+
Get all images from a comment::
340+
341+
issue = jira.issue('JRA-1330')
342+
regex_for_png = re.compile(r'\!(\S+?\.(jpg|png|bmp))\|?\S*?\!')
343+
pngs_used_in_comment = regex_for_png.findall(issue.fields.comment.comments[0].body)
344+
for attachment in issue.fields.attachment:
345+
if attachment.filename in pngs_used_in_comment:
346+
with open(attachment.filename, 'wb') as f:
347+
f.write(attachment.get())
348+
339349
Transitions
340350
-----------
341351

jira/client.py

+77-67
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
Type,
3737
TypeVar,
3838
Union,
39-
cast,
4039
no_type_check,
4140
overload,
4241
)
@@ -48,10 +47,11 @@
4847
from requests.auth import AuthBase
4948
from requests.structures import CaseInsensitiveDict
5049
from requests.utils import get_netrc_auth
50+
from requests_toolbelt import MultipartEncoder
5151

5252
from jira import __version__
5353
from jira.exceptions import JIRAError
54-
from jira.resilientsession import ResilientSession, raise_on_error
54+
from jira.resilientsession import PrepareRequestForRetry, ResilientSession
5555
from jira.resources import (
5656
AgileResource,
5757
Attachment,
@@ -93,12 +93,6 @@
9393
)
9494
from jira.utils import json_loads, threaded_requests
9595

96-
try:
97-
# noinspection PyUnresolvedReferences
98-
from requests_toolbelt import MultipartEncoder
99-
except ImportError:
100-
pass
101-
10296
try:
10397
from requests_jwt import JWTAuth
10498
except ImportError:
@@ -1000,70 +994,68 @@ def add_attachment(
1000994
"""
1001995
close_attachment = False
1002996
if isinstance(attachment, str):
1003-
attachment: BufferedReader = open(attachment, "rb") # type: ignore
1004-
attachment = cast(BufferedReader, attachment)
997+
attachment_io = open(attachment, "rb") # type: ignore
1005998
close_attachment = True
1006-
elif isinstance(attachment, BufferedReader) and attachment.mode != "rb":
1007-
self.log.warning(
1008-
"%s was not opened in 'rb' mode, attaching file may fail."
1009-
% attachment.name
1010-
)
1011-
1012-
url = self._get_url("issue/" + str(issue) + "/attachments")
999+
else:
1000+
attachment_io = attachment
1001+
if isinstance(attachment, BufferedReader) and attachment.mode != "rb":
1002+
self.log.warning(
1003+
"%s was not opened in 'rb' mode, attaching file may fail."
1004+
% attachment.name
1005+
)
10131006

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

1018-
if "MultipartEncoder" not in globals():
1019-
method = "old"
1020-
try:
1021-
r = self._session.post(
1022-
url,
1023-
files={"file": (fname, attachment, "application/octet-stream")},
1024-
headers=CaseInsensitiveDict(
1025-
{"content-type": None, "X-Atlassian-Token": "no-check"}
1026-
),
1027-
)
1028-
finally:
1029-
if close_attachment:
1030-
attachment.close()
1031-
else:
1032-
method = "MultipartEncoder"
1033-
1034-
def file_stream() -> MultipartEncoder:
1035-
"""Returns files stream of attachment."""
1036-
return MultipartEncoder(
1037-
fields={"file": (fname, attachment, "application/octet-stream")}
1038-
)
1039-
1040-
m = file_stream()
1041-
try:
1042-
r = self._session.post(
1043-
url,
1044-
data=m,
1045-
headers=CaseInsensitiveDict(
1046-
{
1047-
"content-type": m.content_type,
1048-
"X-Atlassian-Token": "no-check",
1049-
}
1050-
),
1051-
retry_data=file_stream,
1052-
)
1053-
finally:
1054-
if close_attachment:
1055-
attachment.close()
1011+
def generate_multipartencoded_request_args() -> Tuple[
1012+
MultipartEncoder, CaseInsensitiveDict
1013+
]:
1014+
"""Returns MultipartEncoder stream of attachment, and the header."""
1015+
attachment_io.seek(0)
1016+
encoded_data = MultipartEncoder(
1017+
fields={"file": (fname, attachment_io, "application/octet-stream")}
1018+
)
1019+
request_headers = CaseInsensitiveDict(
1020+
{
1021+
"content-type": encoded_data.content_type,
1022+
"X-Atlassian-Token": "no-check",
1023+
}
1024+
)
1025+
return encoded_data, request_headers
1026+
1027+
class RetryableMultipartEncoder(PrepareRequestForRetry):
1028+
def prepare(
1029+
self, original_request_kwargs: CaseInsensitiveDict
1030+
) -> CaseInsensitiveDict:
1031+
encoded_data, request_headers = generate_multipartencoded_request_args()
1032+
original_request_kwargs["data"] = encoded_data
1033+
original_request_kwargs["headers"] = request_headers
1034+
return super().prepare(original_request_kwargs)
1035+
1036+
url = self._get_url(f"issue/{issue}/attachments")
1037+
try:
1038+
encoded_data, request_headers = generate_multipartencoded_request_args()
1039+
r = self._session.post(
1040+
url,
1041+
data=encoded_data,
1042+
headers=request_headers,
1043+
_prepare_retry_class=RetryableMultipartEncoder(), # type: ignore[call-arg] # ResilientSession handles
1044+
)
1045+
finally:
1046+
if close_attachment:
1047+
attachment_io.close()
10561048

10571049
js: Union[Dict[str, Any], List[Dict[str, Any]]] = json_loads(r)
10581050
if not js or not isinstance(js, Iterable):
1059-
raise JIRAError(f"Unable to parse JSON: {js}")
1051+
raise JIRAError(f"Unable to parse JSON: {js}. Failed to add attachment?")
10601052
jira_attachment = Attachment(
10611053
self._options, self._session, js[0] if isinstance(js, List) else js
10621054
)
10631055
if jira_attachment.size == 0:
10641056
raise JIRAError(
1065-
"Added empty attachment via %s method?!: r: %s\nattachment: %s"
1066-
% (method, r, jira_attachment)
1057+
"Added empty attachment?!: "
1058+
+ f"Response: {r}\nAttachment: {jira_attachment}"
10671059
)
10681060
return jira_attachment
10691061

@@ -1836,8 +1828,7 @@ def assign_issue(self, issue: Union[int, str], assignee: Optional[str]) -> bool:
18361828
url = self._get_latest_url(f"issue/{issue}/assignee")
18371829
user_id = self._get_user_id(assignee)
18381830
payload = {"accountId": user_id} if self._is_cloud else {"name": user_id}
1839-
r = self._session.put(url, data=json.dumps(payload))
1840-
raise_on_error(r)
1831+
self._session.put(url, data=json.dumps(payload))
18411832
return True
18421833

18431834
@translate_resource_args
@@ -2717,7 +2708,7 @@ def create_temp_project_avatar(
27172708
if size != size_from_file:
27182709
size = size_from_file
27192710

2720-
params = {"filename": filename, "size": size}
2711+
params: Dict[str, Union[int, str]] = {"filename": filename, "size": size}
27212712

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

3230-
params = {"username": user, "filename": filename, "size": size}
3221+
params: Dict[str, Union[str, int]] = {
3222+
"username": user,
3223+
"filename": filename,
3224+
"size": size,
3225+
}
32313226

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

3794-
r = self._session.put(url, params=params, data=json.dumps(payload))
3795-
raise_on_error(r)
3789+
self._session.put(url, params=params, data=json.dumps(payload))
37963790
else:
37973791
raise NotImplementedError(
37983792
"Support for renaming users in Jira " "< 6.0.0 has been removed."
@@ -4612,13 +4606,29 @@ def sprints(
46124606
self.AGILE_BASE_URL,
46134607
)
46144608

4615-
def sprints_by_name(self, id, extended=False):
4609+
def sprints_by_name(
4610+
self, id: Union[str, int], extended: bool = False, state: str = None
4611+
) -> Dict[str, Dict[str, Any]]:
4612+
"""Get a dictionary of sprint Resources where the name of the sprint is the key.
4613+
4614+
Args:
4615+
board_id (int): the board to get sprints from
4616+
extended (bool): Deprecated.
4617+
state (str): Filters results to sprints in specified states. Valid values: `future`, `active`, `closed`.
4618+
You can define multiple states separated by commas
4619+
4620+
Returns:
4621+
Dict[str, Dict[str, Any]]: dictionary of sprints with the sprint name as key
4622+
"""
46164623
sprints = {}
4617-
for s in self.sprints(id, extended=extended):
4624+
for s in self.sprints(id, extended=extended, state=state):
46184625
if s.name not in sprints:
46194626
sprints[s.name] = s.raw
46204627
else:
4621-
raise Exception
4628+
raise JIRAError(
4629+
f"There are multiple sprints defined with the name {s.name} on board id {id},\n"
4630+
f"returning a dict with sprint names as a key, assumes unique names for each sprint"
4631+
)
46224632
return sprints
46234633

46244634
def update_sprint(self, id, name=None, startDate=None, endDate=None, state=None):

0 commit comments

Comments
 (0)