python-backlog 0.8.0__py3-none-any.whl → 0.9.0__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/_http.py +204 -0
- backlog/base.py +51 -29
- backlog/project.py +22 -0
- {python_backlog-0.8.0.dist-info → python_backlog-0.9.0.dist-info}/METADATA +33 -8
- {python_backlog-0.8.0.dist-info → python_backlog-0.9.0.dist-info}/RECORD +8 -7
- {python_backlog-0.8.0.dist-info → python_backlog-0.9.0.dist-info}/WHEEL +0 -0
- {python_backlog-0.8.0.dist-info → python_backlog-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {python_backlog-0.8.0.dist-info → python_backlog-0.9.0.dist-info}/top_level.txt +0 -0
backlog/_http.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""
|
|
3
|
+
Minimal HTTP client built on the standard library (urllib).
|
|
4
|
+
|
|
5
|
+
python-backlog has no runtime dependencies. This module implements the small subset of HTTP
|
|
6
|
+
features the Backlog API needs: query strings, form-encoded and multipart bodies, custom
|
|
7
|
+
headers, timeouts and JSON responses.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import ssl
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import urllib.request
|
|
16
|
+
import uuid
|
|
17
|
+
from email.message import Message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Response(object):
|
|
21
|
+
"""
|
|
22
|
+
HTTP response returned by :meth:`backlog.base.BaseAPI.invoke_method`.
|
|
23
|
+
|
|
24
|
+
It provides the attributes of ``requests.Response`` that are commonly used with this library.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, status_code, headers, content, url):
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.headers = headers
|
|
30
|
+
self.content = content
|
|
31
|
+
self.url = url
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def ok(self):
|
|
35
|
+
return 200 <= self.status_code < 400
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def encoding(self):
|
|
39
|
+
_message = Message()
|
|
40
|
+
_message["Content-Type"] = self.headers.get("Content-Type", "")
|
|
41
|
+
return _message.get_content_charset() or "utf-8"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def text(self):
|
|
45
|
+
return self.content.decode(self.encoding, errors="replace")
|
|
46
|
+
|
|
47
|
+
def json(self):
|
|
48
|
+
return json.loads(self.text)
|
|
49
|
+
|
|
50
|
+
def __repr__(self):
|
|
51
|
+
return "<Response [{}]>".format(self.status_code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _items(params):
|
|
55
|
+
"""
|
|
56
|
+
Flatten a parameter dict into (key, value) string pairs, like requests does:
|
|
57
|
+
None values are skipped and list/tuple values become repeated keys.
|
|
58
|
+
"""
|
|
59
|
+
pairs = []
|
|
60
|
+
for key, value in (params or {}).items():
|
|
61
|
+
values = value if isinstance(value, (list, tuple)) else [value]
|
|
62
|
+
for v in values:
|
|
63
|
+
if v is None:
|
|
64
|
+
continue
|
|
65
|
+
if isinstance(v, bytes):
|
|
66
|
+
v = v.decode()
|
|
67
|
+
pairs.append((str(key), str(v)))
|
|
68
|
+
return pairs
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def encode_query(url, params):
|
|
72
|
+
pairs = _items(params)
|
|
73
|
+
if not pairs:
|
|
74
|
+
return url
|
|
75
|
+
return url + ("&" if "?" in url else "?") + urllib.parse.urlencode(pairs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def encode_form(data):
|
|
79
|
+
pairs = _items(data)
|
|
80
|
+
if not pairs:
|
|
81
|
+
return None
|
|
82
|
+
return urllib.parse.urlencode(pairs).encode()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def encode_multipart(data, files):
|
|
86
|
+
"""
|
|
87
|
+
Encode form fields and files as multipart/form-data.
|
|
88
|
+
|
|
89
|
+
:param files: dict of field name to ``(filename, fileobj_or_bytes[, content_type])``
|
|
90
|
+
:return: (body, content_type)
|
|
91
|
+
"""
|
|
92
|
+
boundary = uuid.uuid4().hex
|
|
93
|
+
lines = []
|
|
94
|
+
for key, value in _items(data):
|
|
95
|
+
lines += [
|
|
96
|
+
"--{}".format(boundary).encode(),
|
|
97
|
+
'Content-Disposition: form-data; name="{}"'.format(_quote(key)).encode(),
|
|
98
|
+
b"",
|
|
99
|
+
value.encode(),
|
|
100
|
+
]
|
|
101
|
+
for key, spec in files.items():
|
|
102
|
+
filename, content = spec[0], spec[1]
|
|
103
|
+
content_type = (spec[2] if len(spec) > 2 else None) or "application/octet-stream"
|
|
104
|
+
if hasattr(content, "read"):
|
|
105
|
+
content = content.read()
|
|
106
|
+
if isinstance(content, str):
|
|
107
|
+
content = content.encode()
|
|
108
|
+
lines += [
|
|
109
|
+
"--{}".format(boundary).encode(),
|
|
110
|
+
'Content-Disposition: form-data; name="{}"; filename="{}"'.format(_quote(key), _quote(filename)).encode(),
|
|
111
|
+
"Content-Type: {}".format(content_type).encode(),
|
|
112
|
+
b"",
|
|
113
|
+
content,
|
|
114
|
+
]
|
|
115
|
+
lines += ["--{}--".format(boundary).encode(), b""]
|
|
116
|
+
return b"\r\n".join(lines), "multipart/form-data; boundary={}".format(boundary)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _quote(value):
|
|
120
|
+
return str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\r", "").replace("\n", "")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
124
|
+
"""
|
|
125
|
+
Do not follow redirects. The Backlog API does not redirect, and following a redirect would
|
|
126
|
+
forward the API key header to wherever the redirect points.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _ssl_context():
|
|
134
|
+
"""
|
|
135
|
+
Build the TLS context.
|
|
136
|
+
|
|
137
|
+
The system trust store is used as the standard library does (it also honors SSL_CERT_FILE and
|
|
138
|
+
SSL_CERT_DIR, so CAs added to the OS, such as those of corporate proxies, keep working).
|
|
139
|
+
CA bundles that requests users may rely on are added on top of it:
|
|
140
|
+
|
|
141
|
+
- the file in REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE, if set;
|
|
142
|
+
- certifi's bundle, if certifi is installed (helps on systems without a usable trust store).
|
|
143
|
+
"""
|
|
144
|
+
context = ssl.create_default_context()
|
|
145
|
+
for name in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"):
|
|
146
|
+
path = os.environ.get(name)
|
|
147
|
+
if not path:
|
|
148
|
+
continue
|
|
149
|
+
if os.path.isdir(path):
|
|
150
|
+
context.load_verify_locations(capath=path)
|
|
151
|
+
else:
|
|
152
|
+
context.load_verify_locations(cafile=path)
|
|
153
|
+
try:
|
|
154
|
+
import certifi
|
|
155
|
+
except ImportError:
|
|
156
|
+
pass
|
|
157
|
+
else:
|
|
158
|
+
context.load_verify_locations(cafile=certifi.where())
|
|
159
|
+
return context
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_opener = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _get_opener():
|
|
166
|
+
global _opener
|
|
167
|
+
if _opener is None:
|
|
168
|
+
_opener = urllib.request.build_opener(
|
|
169
|
+
urllib.request.HTTPSHandler(context=_ssl_context()),
|
|
170
|
+
_NoRedirectHandler(),
|
|
171
|
+
)
|
|
172
|
+
return _opener
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _open(req, timeout):
|
|
176
|
+
"""
|
|
177
|
+
Send the request. Returns (status, headers, body, url) for every HTTP status code.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
with _get_opener().open(req, timeout=timeout) as resp:
|
|
181
|
+
return resp.status, resp.headers, resp.read(), resp.url
|
|
182
|
+
except urllib.error.HTTPError as e:
|
|
183
|
+
with e:
|
|
184
|
+
return e.code, e.headers, e.read(), e.url
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def request(method, url, params=None, data=None, headers=None, files=None, timeout=None):
|
|
188
|
+
"""
|
|
189
|
+
Send an HTTP request and return a :class:`Response`.
|
|
190
|
+
|
|
191
|
+
Network errors raise ``OSError`` subclasses (``urllib.error.URLError``, ``TimeoutError``, ...).
|
|
192
|
+
"""
|
|
193
|
+
_headers = dict(headers or {})
|
|
194
|
+
body = None
|
|
195
|
+
if files:
|
|
196
|
+
body, _headers["Content-Type"] = encode_multipart(data, files)
|
|
197
|
+
elif method.upper() != "GET":
|
|
198
|
+
body = encode_form(data)
|
|
199
|
+
if body is not None:
|
|
200
|
+
_headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
201
|
+
|
|
202
|
+
req = urllib.request.Request(encode_query(url, params), data=body, headers=_headers, method=method.upper())
|
|
203
|
+
status, response_headers, content, response_url = _open(req, timeout)
|
|
204
|
+
return Response(status, response_headers, content, response_url)
|
backlog/base.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
import
|
|
4
|
+
from backlog import _http
|
|
5
|
+
from backlog._http import Response # noqa: F401 (re-exported for type references)
|
|
5
6
|
from backlog.wiki import Wiki
|
|
6
7
|
from backlog.attachment import Attachment
|
|
7
8
|
from backlog.project import Project
|
|
@@ -25,6 +26,18 @@ class BacklogError(Exception):
|
|
|
25
26
|
pass
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
class BacklogConnectionError(BacklogError):
|
|
30
|
+
"""
|
|
31
|
+
Raised when the request could not be sent or no response was received
|
|
32
|
+
(DNS failure, refused connection, TLS error, timeout, ...).
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Keyword arguments accepted by invoke_method() in addition to its named parameters.
|
|
38
|
+
_SUPPORTED_KWARGS = frozenset(["timeout", "files"])
|
|
39
|
+
|
|
40
|
+
|
|
28
41
|
class BaseAPI(object):
|
|
29
42
|
def __init__(self, space, api_key=None, timeout=DEFAULT_TIMEOUT):
|
|
30
43
|
"""
|
|
@@ -39,11 +52,10 @@ class BaseAPI(object):
|
|
|
39
52
|
self.api_key = api_key
|
|
40
53
|
self.timeout = timeout
|
|
41
54
|
self.base_url = BACKLOG_BASE_URL.format(space=space) + BACKLOG_URI_PREFIX
|
|
42
|
-
self.api = None
|
|
43
55
|
self._build_api()
|
|
44
56
|
|
|
45
57
|
def _build_api(self):
|
|
46
|
-
|
|
58
|
+
pass
|
|
47
59
|
|
|
48
60
|
def _build_headers(self, headers=None):
|
|
49
61
|
"""
|
|
@@ -58,16 +70,24 @@ class BaseAPI(object):
|
|
|
58
70
|
|
|
59
71
|
def invoke_method(self, method, uri, query_param=None, request_param=None, headers=None, **kwargs):
|
|
60
72
|
"""
|
|
61
|
-
Delegate requests to get/post/delete method.
|
|
62
|
-
Each method implements by any http client, default client is requests.
|
|
73
|
+
Delegate requests to get/post/delete/patch method.
|
|
63
74
|
|
|
64
|
-
:param method: HTTP Method. supports GET/POST/DELETE
|
|
75
|
+
:param method: HTTP Method. supports GET/POST/DELETE/PATCH
|
|
65
76
|
:param uri: API Endpoint. 'uri' expects a URI excluded '/api/v2/' prefix
|
|
66
77
|
:param query_param: dict of URL parameter. this will be url-encoded string
|
|
67
78
|
:param request_param: dict of body parameter. this will use when invoke post request
|
|
68
79
|
:param headers: HTTP Header dict. Authentication headers are added automatically.
|
|
69
|
-
:
|
|
80
|
+
:param kwargs: ``timeout`` (seconds, overrides the client default) and ``files``
|
|
81
|
+
(dict of field name to ``(filename, fileobj_or_bytes[, content_type])``).
|
|
82
|
+
:return: :class:`backlog._http.Response`
|
|
70
83
|
"""
|
|
84
|
+
unsupported = set(kwargs) - _SUPPORTED_KWARGS
|
|
85
|
+
if unsupported:
|
|
86
|
+
raise TypeError(
|
|
87
|
+
"Unsupported keyword argument(s): {}. Since 0.9.0 python-backlog no longer uses requests, "
|
|
88
|
+
"so requests-specific options are not available.".format(", ".join(sorted(unsupported)))
|
|
89
|
+
)
|
|
90
|
+
|
|
71
91
|
# Copy the arguments so that neither the caller's dicts nor any shared state are mutated.
|
|
72
92
|
query_param = dict(query_param or {})
|
|
73
93
|
request_param = dict(request_param or {})
|
|
@@ -116,11 +136,9 @@ class BaseAPI(object):
|
|
|
116
136
|
|
|
117
137
|
class BacklogAPI(BaseAPI):
|
|
118
138
|
"""
|
|
119
|
-
|
|
139
|
+
Backlog API client using the standard library HTTP client.
|
|
120
140
|
"""
|
|
121
141
|
def _build_api(self):
|
|
122
|
-
self.api = None
|
|
123
|
-
|
|
124
142
|
self.wiki = Wiki(self)
|
|
125
143
|
self.attachment = Attachment(self)
|
|
126
144
|
self.project = Project(self)
|
|
@@ -131,32 +149,36 @@ class BacklogAPI(BaseAPI):
|
|
|
131
149
|
self.custom_field = CustomField(self)
|
|
132
150
|
|
|
133
151
|
@staticmethod
|
|
134
|
-
def _raise_for_status(resp
|
|
135
|
-
if
|
|
152
|
+
def _raise_for_status(resp):
|
|
153
|
+
if resp.status_code // 100 == 2:
|
|
136
154
|
return resp
|
|
137
155
|
raise BacklogError("Http response {status}: {message}".format(status=resp.status_code, message=resp.text))
|
|
138
156
|
|
|
157
|
+
def _send(self, method, uri, query_param, request_param, headers, timeout=None, files=None):
|
|
158
|
+
try:
|
|
159
|
+
resp = _http.request(
|
|
160
|
+
method,
|
|
161
|
+
self.base_url + uri,
|
|
162
|
+
params=query_param,
|
|
163
|
+
data=request_param,
|
|
164
|
+
headers=headers,
|
|
165
|
+
files=files,
|
|
166
|
+
timeout=timeout,
|
|
167
|
+
)
|
|
168
|
+
except OSError as e:
|
|
169
|
+
raise BacklogConnectionError(
|
|
170
|
+
"Failed to send {method} request to {url}: {error}".format(method=method.upper(), url=self.base_url + uri, error=e)
|
|
171
|
+
) from e
|
|
172
|
+
return self._raise_for_status(resp)
|
|
173
|
+
|
|
139
174
|
def get(self, method, uri, query_param, request_param, headers, **kwargs):
|
|
140
|
-
|
|
141
|
-
resp = requests.get(_url, params=query_param, headers=headers, **kwargs)
|
|
142
|
-
return self._raise_for_status(resp, lambda code: code // 100 == 2)
|
|
175
|
+
return self._send("GET", uri, query_param, None, headers, **kwargs)
|
|
143
176
|
|
|
144
177
|
def post(self, method, uri, query_param, request_param, headers, **kwargs):
|
|
145
|
-
|
|
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)
|
|
178
|
+
return self._send("POST", uri, query_param, request_param, headers, **kwargs)
|
|
148
179
|
|
|
149
180
|
def delete(self, method, uri, query_param, request_param, headers, **kwargs):
|
|
150
|
-
|
|
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)
|
|
181
|
+
return self._send("DELETE", uri, query_param, request_param, headers, **kwargs)
|
|
153
182
|
|
|
154
183
|
def patch(self, method, uri, query_param, request_param, headers, **kwargs):
|
|
155
|
-
|
|
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)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class BacklogObject(object):
|
|
161
|
-
def __init__(self, api):
|
|
162
|
-
self.api = api
|
|
184
|
+
return self._send("PATCH", uri, query_param, request_param, headers, **kwargs)
|
backlog/project.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
2
4
|
from typing import TypedDict, Literal, Optional, NewType
|
|
3
5
|
|
|
4
6
|
|
|
@@ -346,6 +348,11 @@ class Project(object):
|
|
|
346
348
|
:param projectIdOrKey:
|
|
347
349
|
:return:
|
|
348
350
|
"""
|
|
351
|
+
warnings.warn(
|
|
352
|
+
"Project.list_categories() is deprecated and will be removed in a future release; use BacklogAPI.category.list() instead.",
|
|
353
|
+
DeprecationWarning,
|
|
354
|
+
stacklevel=2,
|
|
355
|
+
)
|
|
349
356
|
_uri = "projects/{project_id_or_key}/categories".format(
|
|
350
357
|
project_id_or_key=projectIdOrKey,
|
|
351
358
|
)
|
|
@@ -363,6 +370,11 @@ class Project(object):
|
|
|
363
370
|
:param name: (str) Category name
|
|
364
371
|
:return:
|
|
365
372
|
"""
|
|
373
|
+
warnings.warn(
|
|
374
|
+
"Project.add_category() is deprecated and will be removed in a future release; use BacklogAPI.category.add() instead.",
|
|
375
|
+
DeprecationWarning,
|
|
376
|
+
stacklevel=2,
|
|
377
|
+
)
|
|
366
378
|
_uri = "projects/{project_id_or_key}/categories".format(
|
|
367
379
|
project_id_or_key=projectIdOrKey,
|
|
368
380
|
)
|
|
@@ -381,6 +393,11 @@ class Project(object):
|
|
|
381
393
|
:param name: (str) will modified
|
|
382
394
|
:return:
|
|
383
395
|
"""
|
|
396
|
+
warnings.warn(
|
|
397
|
+
"Project.update_category() is deprecated and will be removed in a future release; use BacklogAPI.category.update() instead.",
|
|
398
|
+
DeprecationWarning,
|
|
399
|
+
stacklevel=2,
|
|
400
|
+
)
|
|
384
401
|
_uri = "projects/{project_id_or_key}/categories/{id}".format(
|
|
385
402
|
project_id_or_key=projectIdOrKey,
|
|
386
403
|
id=id
|
|
@@ -399,6 +416,11 @@ class Project(object):
|
|
|
399
416
|
:param id:
|
|
400
417
|
:return:
|
|
401
418
|
"""
|
|
419
|
+
warnings.warn(
|
|
420
|
+
"Project.delete_category() is deprecated and will be removed in a future release; use BacklogAPI.category.delete() instead.",
|
|
421
|
+
DeprecationWarning,
|
|
422
|
+
stacklevel=2,
|
|
423
|
+
)
|
|
402
424
|
_uri = "projects/{project_id_or_key}/categories/{id}".format(
|
|
403
425
|
project_id_or_key=projectIdOrKey,
|
|
404
426
|
id=id
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-backlog
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Backlog API v2 wrapper
|
|
5
5
|
Author-email: hassaku63 <hassaku63@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,17 +22,17 @@ Classifier: Natural Language :: English
|
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: requests<3,>=2.32.2
|
|
26
25
|
Provides-Extra: dev
|
|
27
26
|
Requires-Dist: flake8>=7; extra == "dev"
|
|
28
27
|
Requires-Dist: coverage>=7.6; extra == "dev"
|
|
29
|
-
Requires-Dist: httpretty>=1.1.4; extra == "dev"
|
|
30
|
-
Requires-Dist: tox>=4; extra == "dev"
|
|
31
28
|
Dynamic: license-file
|
|
32
29
|
|
|
33
30
|
# pbl
|
|
34
31
|
|
|
35
|
-
A Python package wraps Backlog API
|
|
32
|
+
A Python package that wraps the [Backlog API v2](https://developer.nulab.com/docs/backlog/).
|
|
33
|
+
|
|
34
|
+
- Requires Python 3.10 or later.
|
|
35
|
+
- No runtime dependencies: only the Python standard library is used.
|
|
36
36
|
|
|
37
37
|
## Usage
|
|
38
38
|
|
|
@@ -58,11 +58,15 @@ def main():
|
|
|
58
58
|
# Requests time out after 30 seconds by default (configurable with `timeout=`).
|
|
59
59
|
api = BacklogAPI("your-space", "your-api-key")
|
|
60
60
|
|
|
61
|
+
# Categories
|
|
62
|
+
# https://developer.nulab.com/docs/backlog/api/2/get-category-list/
|
|
63
|
+
print(api.category.list("SampleProject"))
|
|
64
|
+
|
|
61
65
|
"""
|
|
62
66
|
Project API
|
|
63
67
|
"""
|
|
64
68
|
# list project users
|
|
65
|
-
# https://developer.nulab
|
|
69
|
+
# https://developer.nulab.com/docs/backlog/api/2/get-project-list/
|
|
66
70
|
print("# list project users")
|
|
67
71
|
users = api.project.list_users("SampleProject")
|
|
68
72
|
print(json.dumps(users, indent=2))
|
|
@@ -72,13 +76,13 @@ def main():
|
|
|
72
76
|
Wiki API
|
|
73
77
|
"""
|
|
74
78
|
# list wikis
|
|
75
|
-
# https://developer.nulab
|
|
79
|
+
# https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-list/
|
|
76
80
|
print("# list wikis")
|
|
77
81
|
wikis = api.wiki.list("SampleProject")
|
|
78
82
|
print(json.dumps(wikis[0], indent=2))
|
|
79
83
|
|
|
80
84
|
# get attachment
|
|
81
|
-
# https://developer.nulab
|
|
85
|
+
# https://developer.nulab.com/docs/backlog/api/2/get-issue-attachment/
|
|
82
86
|
print("# get attachment")
|
|
83
87
|
wiki = [w for w in api.wiki.list("SampleProject") if len(w["attachments"]) > 0][0]
|
|
84
88
|
attachment = api.wiki.get_attachment(
|
|
@@ -92,10 +96,31 @@ if __name__ == "__main__":
|
|
|
92
96
|
main()
|
|
93
97
|
```
|
|
94
98
|
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
- `backlog.base.BacklogError` is raised when the API returns a non-2xx response.
|
|
102
|
+
- `backlog.base.BacklogConnectionError` (a subclass of `BacklogError`) is raised when the request could not be sent or no response was received, for example on a timeout.
|
|
103
|
+
|
|
104
|
+
## TLS certificates
|
|
105
|
+
|
|
106
|
+
HTTPS certificates are verified against the system trust store (it also honors `SSL_CERT_FILE` and `SSL_CERT_DIR`). If `certifi` is installed, or `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE` is set, those CA certificates are trusted as well.
|
|
107
|
+
|
|
108
|
+
## Upgrading to 0.9
|
|
109
|
+
|
|
110
|
+
0.9.0 removed the dependency on `requests`:
|
|
111
|
+
|
|
112
|
+
- `BaseAPI.invoke_method()` returns a `backlog._http.Response` instead of a `requests.Response`. It provides `status_code`, `ok`, `headers`, `content`, `text` and `json()`.
|
|
113
|
+
- `invoke_method()` only accepts the `timeout` and `files` keyword arguments. Requests-specific options such as `verify` or `proxies` raise `TypeError`. Proxies are still read from the `HTTP(S)_PROXY` and `NO_PROXY` environment variables.
|
|
114
|
+
- Network errors raise `BacklogConnectionError` instead of `requests.exceptions.*`.
|
|
115
|
+
- Redirects are not followed, so the API key is never sent to another host.
|
|
116
|
+
- `Project.list_categories()`, `add_category()`, `update_category()` and `delete_category()` are deprecated. Use `BacklogAPI.category` instead.
|
|
117
|
+
|
|
95
118
|
## See also
|
|
96
119
|
|
|
97
120
|
Qiita https://qiita.com/hassaku_63/items/b9eb2a1c7ecd3c19507d
|
|
98
121
|
|
|
99
122
|
## Contact
|
|
100
123
|
|
|
124
|
+
Please open an issue on [GitHub](https://github.com/hassaku63/pbl/issues).
|
|
125
|
+
|
|
101
126
|
Twitter: [hassaku_63](https://twitter.com/hassaku_63)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
backlog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
backlog/_http.py,sha256=5mAhdQss-eOSgtMQalSATsZGMTxBccNVjMqMGKRxbc4,6385
|
|
2
3
|
backlog/attachment.py,sha256=_Ex_ob7NC0ZMrt_5OM3MqLzIQrswx8SceKhOYNY5Wgs,872
|
|
3
|
-
backlog/base.py,sha256=
|
|
4
|
+
backlog/base.py,sha256=4hEjxogGKAcF3PQ4AetiC7hX-w1OmmE7o-BGP3tKD4Y,7081
|
|
4
5
|
backlog/category.py,sha256=DQ06yhOCXxtDbPMZ1Ig1QD8n7Ht_ZFrp8PJzO18IeZk,2530
|
|
5
6
|
backlog/custom_field.py,sha256=dJ-XnrYv0rlpLvPhIkZEgcyNjZB8GR3352Or8HUPKlc,12243
|
|
6
7
|
backlog/git.py,sha256=mazfvwn1Ekga0eeigk22Xf3vhXEo-P5Irfyq6r_QbaA,3056
|
|
7
8
|
backlog/issue.py,sha256=E7bhNLLQd1KkSEilGcrmKYlkfi_w8rVfRF6xI-PGEtc,6650
|
|
8
|
-
backlog/project.py,sha256=
|
|
9
|
+
backlog/project.py,sha256=9Wdy25M-dhDNuZ4_2iSUz1k3KkE5oLP8Ean7sVppCpU,15273
|
|
9
10
|
backlog/user.py,sha256=kSEkInekQ8aGgYs5ygCdA5tsUYK3cxO9_Aay-sfaC2A,2890
|
|
10
11
|
backlog/wiki.py,sha256=ThfWC-OlnHDHMYeARYJega92fd3QhZ817mhWF1OegT4,5943
|
|
11
|
-
python_backlog-0.
|
|
12
|
-
python_backlog-0.
|
|
13
|
-
python_backlog-0.
|
|
14
|
-
python_backlog-0.
|
|
15
|
-
python_backlog-0.
|
|
12
|
+
python_backlog-0.9.0.dist-info/licenses/LICENSE,sha256=1teWq4411yGkUz_9lD1V74mcHFRix2IqOAoFaghKYwo,1073
|
|
13
|
+
python_backlog-0.9.0.dist-info/METADATA,sha256=YgM4IPdNaO49SM5n3Za7KuWOT_D2wxHxlqNCTC1OJpA,4344
|
|
14
|
+
python_backlog-0.9.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
15
|
+
python_backlog-0.9.0.dist-info/top_level.txt,sha256=qzHRU2P6F24ntkXGiIBl4Qf_AlV0BRebSc5tuLlcOIE,8
|
|
16
|
+
python_backlog-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|