python-backlog 0.7.4__py3-none-any.whl → 0.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
backlog/attachment.py CHANGED
@@ -20,10 +20,12 @@ class Attachment(object):
20
20
 
21
21
  _uri = "space/attachment"
22
22
  _method = "POST"
23
- _files = {
24
- 'file': (os.path.basename(filename), open(filename, 'rb'), mimetypes.guess_type(filename))
25
- }
23
+ _content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
26
24
 
27
- resp = self.api.invoke_method(_method, _uri, files=_files)
25
+ with open(filename, 'rb') as f:
26
+ _files = {
27
+ 'file': (os.path.basename(filename), f, _content_type)
28
+ }
29
+ resp = self.api.invoke_method(_method, _uri, files=_files)
28
30
 
29
31
  return resp.json()
backlog/base.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- from urllib.parse import urlencode
3
+ import re
4
4
  import requests
5
5
  from backlog.wiki import Wiki
6
6
  from backlog.attachment import Attachment
@@ -13,6 +13,12 @@ from backlog.custom_field import CustomField
13
13
 
14
14
  BACKLOG_BASE_URL = "https://{space}.backlog.jp"
15
15
  BACKLOG_URI_PREFIX = "/api/v2/"
16
+ BACKLOG_API_KEY_HEADER = "Backlog-API-Key"
17
+ DEFAULT_TIMEOUT = 30
18
+
19
+ # Space keys consist of alphanumerics and hyphens. Validating it prevents the
20
+ # base URL (and therefore the API key) from being pointed at an arbitrary host.
21
+ _SPACE_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*$")
16
22
 
17
23
 
18
24
  class BacklogError(Exception):
@@ -20,9 +26,18 @@ class BacklogError(Exception):
20
26
 
21
27
 
22
28
  class BaseAPI(object):
23
- def __init__(self, space, api_key=None):
29
+ def __init__(self, space, api_key=None, timeout=DEFAULT_TIMEOUT):
30
+ """
31
+ :param space: Backlog space key (the "xxx" of https://xxx.backlog.jp)
32
+ :param api_key: API key. It is sent in the "Backlog-API-Key" request header.
33
+ :param timeout: Default timeout (seconds) passed to the HTTP client.
34
+ Can be overridden per request with the "timeout" keyword argument. None disables it.
35
+ """
36
+ if not isinstance(space, str) or not _SPACE_PATTERN.match(space):
37
+ raise ValueError("Invalid space key: {!r}".format(space))
24
38
  self.space = space
25
39
  self.api_key = api_key
40
+ self.timeout = timeout
26
41
  self.base_url = BACKLOG_BASE_URL.format(space=space) + BACKLOG_URI_PREFIX
27
42
  self.api = None
28
43
  self._build_api()
@@ -30,7 +45,18 @@ class BaseAPI(object):
30
45
  def _build_api(self):
31
46
  self.api = None
32
47
 
33
- def invoke_method(self, method, uri, query_param={}, request_param={}, headers=None, **kwargs):
48
+ def _build_headers(self, headers=None):
49
+ """
50
+ Merge authentication headers into the given headers without mutating it.
51
+ """
52
+ _headers = {}
53
+ if self.api_key is not None:
54
+ _headers[BACKLOG_API_KEY_HEADER] = self.api_key
55
+ if headers:
56
+ _headers.update(headers)
57
+ return _headers
58
+
59
+ def invoke_method(self, method, uri, query_param=None, request_param=None, headers=None, **kwargs):
34
60
  """
35
61
  Delegate requests to get/post/delete method.
36
62
  Each method implements by any http client, default client is requests.
@@ -39,10 +65,14 @@ class BaseAPI(object):
39
65
  :param uri: API Endpoint. 'uri' expects a URI excluded '/api/v2/' prefix
40
66
  :param query_param: dict of URL parameter. this will be url-encoded string
41
67
  :param request_param: dict of body parameter. this will use when invoke post request
42
- :param headers: HTTP Header dict. Recommend value is None. if None, then use default implements of http client(requests).
68
+ :param headers: HTTP Header dict. Authentication headers are added automatically.
43
69
  :return: http response object
44
70
  """
45
- query_param.setdefault("apiKey", self.api_key)
71
+ # Copy the arguments so that neither the caller's dicts nor any shared state are mutated.
72
+ query_param = dict(query_param or {})
73
+ request_param = dict(request_param or {})
74
+ headers = self._build_headers(headers)
75
+ kwargs.setdefault("timeout", self.timeout)
46
76
 
47
77
  method = method.lower()
48
78
 
@@ -100,64 +130,31 @@ class BacklogAPI(BaseAPI):
100
130
  self.category = Category(self)
101
131
  self.custom_field = CustomField(self)
102
132
 
103
- def get(self, method, uri, query_param, request_param, headers, **kwargs):
104
- _url = self.base_url + uri
105
- if headers is not None:
106
- resp = requests.get(_url, params=query_param, headers=headers, **kwargs)
107
- else:
108
- resp = requests.get(_url, params=query_param, **kwargs)
109
-
110
- if resp.status_code // 100 == 2:
133
+ @staticmethod
134
+ def _raise_for_status(resp, ok):
135
+ if ok(resp.status_code):
111
136
  return resp
112
-
113
137
  raise BacklogError("Http response {status}: {message}".format(status=resp.status_code, message=resp.text))
114
138
 
115
- def post(self, method, uri, query_param, request_param, headers, **kwargs):
116
- _url = self.base_url + uri + "?" + urlencode(query_param)
117
- if headers is not None:
118
- resp = requests.post(_url, data=request_param, headers=headers, **kwargs)
119
- else:
120
- resp = requests.post(_url, data=request_param, **kwargs)
121
-
122
- if resp.status_code // 100 == 2:
123
- return resp
139
+ def get(self, method, uri, query_param, request_param, headers, **kwargs):
140
+ _url = self.base_url + uri
141
+ resp = requests.get(_url, params=query_param, headers=headers, **kwargs)
142
+ return self._raise_for_status(resp, lambda code: code // 100 == 2)
124
143
 
125
- raise BacklogError("Http response {status}: {message}".format(status=resp.status_code, message=resp.text))
144
+ def post(self, method, uri, query_param, request_param, headers, **kwargs):
145
+ _url = self.base_url + uri
146
+ resp = requests.post(_url, params=query_param, data=request_param, headers=headers, **kwargs)
147
+ return self._raise_for_status(resp, lambda code: code // 100 == 2)
126
148
 
127
149
  def delete(self, method, uri, query_param, request_param, headers, **kwargs):
128
- _url = self.base_url + uri + "?" + urlencode(query_param)
129
- if headers is not None:
130
- resp = requests.delete(_url, headers=headers, **kwargs)
131
- else:
132
- resp = requests.delete(_url, **kwargs)
133
-
134
- if resp.status_code == 200:
135
- return resp
136
-
137
- raise BacklogError("Http response {status}: {message}".format(status=resp.status_code, message=resp.text))
150
+ _url = self.base_url + uri
151
+ resp = requests.delete(_url, params=query_param, data=request_param, headers=headers, **kwargs)
152
+ return self._raise_for_status(resp, lambda code: code // 100 == 2)
138
153
 
139
154
  def patch(self, method, uri, query_param, request_param, headers, **kwargs):
140
- """
141
- TODO: Implementation.
142
-
143
- :param method:
144
- :param uri:
145
- :param query_param:
146
- :param request_param:
147
- :param headers:
148
- :param kwargs:
149
- :return:
150
- """
151
- _url = self.base_url + uri + "?" + urlencode(query_param)
152
- if headers is not None:
153
- resp = requests.ptach(_url, data=request_param, headers=headers, **kwargs)
154
- else:
155
- resp = requests.patch(_url, data=request_param, **kwargs)
156
-
157
- if resp.status_code == 200:
158
- return resp
159
-
160
- raise BacklogError("Http response {status}: {message}".format(status=resp.status_code, message=resp.text))
155
+ _url = self.base_url + uri
156
+ resp = requests.patch(_url, params=query_param, data=request_param, headers=headers, **kwargs)
157
+ return self._raise_for_status(resp, lambda code: code // 100 == 2)
161
158
 
162
159
 
163
160
  class BacklogObject(object):
backlog/issue.py CHANGED
@@ -124,7 +124,7 @@ class Issue(object):
124
124
  if attachmentId is not None:
125
125
  _data.update([("attachmentId[]", attachmentId)])
126
126
 
127
- resp = self.api.invoke_method(_method, _uri, _data)
127
+ resp = self.api.invoke_method(_method, _uri, request_param=_data)
128
128
 
129
129
  return resp.json()
130
130
 
backlog/project.py CHANGED
@@ -72,6 +72,7 @@ class Project(object):
72
72
  useOriginalImageSizeAtWiki: Optional[bool] = None,
73
73
  textFormattingRule: Optional[TextFormattingRule] = None,
74
74
  useDevAttributes: Optional[bool] = None,
75
+ chartEnabled: Optional[bool] = None,
75
76
  ) -> AddProjectResponse:
76
77
  """Add project
77
78
  https://developer.nulab.com/docs/backlog/api/2/add-project/#form-parameters
@@ -80,8 +81,10 @@ class Project(object):
80
81
  :type name: str
81
82
  :param key: Project Key (Uppercase letters (A-Z), numbers (0-9) and underscore (_) can be used.)
82
83
  :type key: str
83
- :param chatEnabled: Enable chart
84
+ :param chatEnabled: Deprecated alias of chartEnabled, kept for backward compatibility
84
85
  :type chatEnabled: bool
86
+ :param chartEnabled: Enable chart
87
+ :type chartEnabled: bool
85
88
  :param useResolvedForChart: Consider Resolved and statuses after as Closed
86
89
  :type useResolvedForChart: bool
87
90
  :param subtaskingEnabled: Enable subtasking
@@ -109,10 +112,14 @@ class Project(object):
109
112
  """
110
113
  _path = "projects"
111
114
  _method = "POST"
115
+ if chartEnabled is not None and chatEnabled is not None and chartEnabled != chatEnabled:
116
+ raise ValueError("chartEnabled and its deprecated alias chatEnabled have conflicting values")
117
+ if chartEnabled is None:
118
+ chartEnabled = chatEnabled
112
119
  request_param = {
113
120
  "name": name,
114
121
  "key": key,
115
- "chartEnabled": chatEnabled,
122
+ "chartEnabled": chartEnabled,
116
123
  "useResolvedForChart": useResolvedForChart,
117
124
  "subtaskingEnabled": subtaskingEnabled,
118
125
  "projectLeaderCanEditProjectLeader": projectLeaderCanEditProjectLeader,
@@ -241,7 +248,7 @@ class Project(object):
241
248
  _uri = "projects/{project_id_or_key}/users".format(project_id_or_key=projectIdOrKey)
242
249
  _method = "GET"
243
250
  _query_param = {
244
- "excludeGroupMembers": "ture" if excludeGroupMembers else "false"
251
+ "excludeGroupMembers": "true" if excludeGroupMembers else "false"
245
252
  }
246
253
 
247
254
  resp = self.api.invoke_method(_method, _uri, query_param=_query_param)
@@ -361,7 +368,7 @@ class Project(object):
361
368
  )
362
369
  _method = "POST"
363
370
 
364
- resp = self.api.invoke_method(_method, _uri)
371
+ resp = self.api.invoke_method(_method, _uri, request_param={"name": name})
365
372
 
366
373
  return resp.json()
367
374
 
@@ -380,7 +387,7 @@ class Project(object):
380
387
  )
381
388
  _method = "PATCH"
382
389
 
383
- resp = self.api.invoke_method(_method, _uri)
390
+ resp = self.api.invoke_method(_method, _uri, request_param={"name": name})
384
391
 
385
392
  return resp.json()
386
393
 
backlog/user.py CHANGED
@@ -51,7 +51,7 @@ class User(object):
51
51
  _method = "POST"
52
52
  params = dict(
53
53
  userId=userId,
54
- passowrd=password,
54
+ password=password,
55
55
  name=name,
56
56
  mailAddress=mailAddress,
57
57
  roleType=roleType
@@ -61,7 +61,7 @@ class User(object):
61
61
 
62
62
  return resp.json()
63
63
 
64
- def update(self, userId, password, name, mailAddress, roleType):
64
+ def update(self, userId, password=None, name=None, mailAddress=None, roleType=None):
65
65
  """
66
66
  https://developer.nulab.com/ja/docs/backlog/api/2/update-user/
67
67
 
@@ -75,12 +75,12 @@ class User(object):
75
75
  _uri = "users/{user_id}".format(user_id=userId)
76
76
  _method = "PATCH"
77
77
  params = dict(
78
- userId=userId,
79
- passowrd=password,
78
+ password=password,
80
79
  name=name,
81
80
  mailAddress=mailAddress,
82
81
  roleType=roleType
83
82
  )
83
+ params = {k: v for k, v in params.items() if v is not None}
84
84
 
85
85
  resp = self.api.invoke_method(_method, _uri, request_param=params)
86
86
 
backlog/wiki.py CHANGED
@@ -1,5 +1,22 @@
1
1
  # coding: utf-8
2
2
 
3
+ from email.message import Message
4
+
5
+
6
+ def _filename_from_response(resp):
7
+ """
8
+ Extract the file name from the Content-Disposition header (supports RFC 6266 filename*).
9
+
10
+ :return: file name, or None if the header does not contain one
11
+ """
12
+ _content_disposition = resp.headers.get("Content-Disposition")
13
+ if not _content_disposition:
14
+ return None
15
+ _message = Message()
16
+ _message["Content-Disposition"] = _content_disposition
17
+ return _message.get_filename()
18
+
19
+
3
20
  class Wiki(object):
4
21
  def __init__(self, api):
5
22
  """
@@ -146,7 +163,7 @@ class Wiki(object):
146
163
 
147
164
  return {
148
165
  "id": attachmentId,
149
- "name": "test",
166
+ "name": _filename_from_response(resp),
150
167
  "data": resp.content
151
168
  }
152
169
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-backlog
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Backlog API v2 wrapper
5
5
  Home-page: https://github.com/hassaku63/pbl
6
6
  Author: hassaku63
@@ -18,7 +18,20 @@ Classifier: Natural Language :: Japanese
18
18
  Requires-Python: >=3.8
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: requests ~=2.32.2
21
+ Requires-Dist: requests~=2.32.2
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: keywords
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: project-url
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
22
35
 
23
36
  # pbl
24
37
 
@@ -44,6 +57,8 @@ def main():
44
57
  """
45
58
  Initialize API Object
46
59
  """
60
+ # The API key is sent in the "Backlog-API-Key" request header.
61
+ # Requests time out after 30 seconds by default (configurable with `timeout=`).
47
62
  api = BacklogAPI("your-space", "your-api-key")
48
63
 
49
64
  """
@@ -0,0 +1,15 @@
1
+ backlog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ backlog/attachment.py,sha256=_Ex_ob7NC0ZMrt_5OM3MqLzIQrswx8SceKhOYNY5Wgs,872
3
+ backlog/base.py,sha256=CnZ0LLGcHb8yJm6v4gXmVUcki_M1U3Wv8kNgZgyLGoc,6120
4
+ backlog/category.py,sha256=DQ06yhOCXxtDbPMZ1Ig1QD8n7Ht_ZFrp8PJzO18IeZk,2530
5
+ backlog/custom_field.py,sha256=dJ-XnrYv0rlpLvPhIkZEgcyNjZB8GR3352Or8HUPKlc,12243
6
+ backlog/git.py,sha256=mazfvwn1Ekga0eeigk22Xf3vhXEo-P5Irfyq6r_QbaA,3056
7
+ backlog/issue.py,sha256=E7bhNLLQd1KkSEilGcrmKYlkfi_w8rVfRF6xI-PGEtc,6650
8
+ backlog/project.py,sha256=SCNbxjpzKwK5eijN58su9jHnBnIT8XcLqU8kv_vuE3s,14348
9
+ backlog/user.py,sha256=kSEkInekQ8aGgYs5ygCdA5tsUYK3cxO9_Aay-sfaC2A,2890
10
+ backlog/wiki.py,sha256=ThfWC-OlnHDHMYeARYJega92fd3QhZ817mhWF1OegT4,5943
11
+ python_backlog-0.7.6.dist-info/licenses/LICENSE,sha256=1teWq4411yGkUz_9lD1V74mcHFRix2IqOAoFaghKYwo,1073
12
+ python_backlog-0.7.6.dist-info/METADATA,sha256=zQYcB_HEpAJsShHe0HjN1GjWMWay4WlfCv_fxc7n9a0,2591
13
+ python_backlog-0.7.6.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
14
+ python_backlog-0.7.6.dist-info/top_level.txt,sha256=qzHRU2P6F24ntkXGiIBl4Qf_AlV0BRebSc5tuLlcOIE,8
15
+ python_backlog-0.7.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,15 +0,0 @@
1
- backlog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- backlog/attachment.py,sha256=RpaAcTnr_2CsJUDeeJHT1YSH0TwPImH5C0uwUOpDlHs,764
3
- backlog/base.py,sha256=-EUjAZr20vq3i8ekPe9LwbGvwJZ6PGhGCV8TsqZqIrs,5632
4
- backlog/category.py,sha256=DQ06yhOCXxtDbPMZ1Ig1QD8n7Ht_ZFrp8PJzO18IeZk,2530
5
- backlog/custom_field.py,sha256=dJ-XnrYv0rlpLvPhIkZEgcyNjZB8GR3352Or8HUPKlc,12243
6
- backlog/git.py,sha256=mazfvwn1Ekga0eeigk22Xf3vhXEo-P5Irfyq6r_QbaA,3056
7
- backlog/issue.py,sha256=of0DQ_1fsBPJ2aU6TS7IWlw0DzZIJRzo4fCGuKv2JY8,6636
8
- backlog/project.py,sha256=TOBDLV6eSpeQRxcjZ1ArrEE8lHRE7JIyaCJUQALSdSg,13839
9
- backlog/user.py,sha256=fgd1X1fom1mTPKDU6DFqBspPauq6UuhxrbFd9CCrptU,2829
10
- backlog/wiki.py,sha256=NjMkHopNb-9oic1yziiypzzpIHSdXcRfePZhOBKtDok,5431
11
- python_backlog-0.7.4.dist-info/LICENSE,sha256=1teWq4411yGkUz_9lD1V74mcHFRix2IqOAoFaghKYwo,1073
12
- python_backlog-0.7.4.dist-info/METADATA,sha256=mo6ZGBZstyo9QaRCpGCKbP8Nff3Mo0Nci7-rVaERduw,2166
13
- python_backlog-0.7.4.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
14
- python_backlog-0.7.4.dist-info/top_level.txt,sha256=qzHRU2P6F24ntkXGiIBl4Qf_AlV0BRebSc5tuLlcOIE,8
15
- python_backlog-0.7.4.dist-info/RECORD,,