kci-confluence-cli 0.4.2__tar.gz → 0.4.3__tar.gz
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.
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/Makefile +1 -2
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/PKG-INFO +19 -1
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/README.md +18 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/common/api.py +86 -14
- kci_confluence_cli-0.4.3/docs/requirements.txt +4 -0
- kci_confluence_cli-0.4.3/tests/test_common_api.py +134 -0
- kci_confluence_cli-0.4.3/uv.lock +1502 -0
- kci_confluence_cli-0.4.2/docs/requirements.txt +0 -4
- kci_confluence_cli-0.4.2/uv.lock +0 -1425
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.devcontainer/.dockerignore +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.devcontainer/.extensions/.gitignore +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.devcontainer/Dockerfile +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.devcontainer/devcontainer.json +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.github/dependabot.yml +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.github/workflows/lint-test.yml +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.gitignore +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.readthedocs.yaml +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.vscode/cspell.json +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.vscode/extension.json +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/.vscode/settings.json +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/__main__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/__version__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/attachment/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/attachment/create_attachment.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/attachment/delete_attachment.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/attachment/get_attachment.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/attachment/subcommand.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/common/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/common/cli.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/common/utils.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/content/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/content/get_content_by_id.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/content/subcommand.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/data/logging.yaml +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/local/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/local/convert_html_to_xml.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/local/subcommand.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/page/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/page/get_page_body.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/page/subcommand.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/page/update_page.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/confluence/py.typed +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/Makefile +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/_static/.gitkeep +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/_templates/.gitkeep +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/attachment/create.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/attachment/delete.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/attachment/get.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/attachment/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/content/get_by_id.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/content/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/local/convert_html.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/local/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/page/get_body.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/page/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/command_reference/page/update.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/conf.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/user_guide/getting_started.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/user_guide/google_docs_migration.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/user_guide/images/confluence_source_editor.png +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/user_guide/images/google_docs_download.png +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/docs/user_guide/index.rst +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/pyproject.toml +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/pytest.ini +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/tests/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/tests/local/__init__.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/tests/local/test__convert_html_to_xml.py +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/tests/resources/page.html +0 -0
- {kci_confluence_cli-0.4.2 → kci_confluence_cli-0.4.3}/tests/test____main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: kci-confluence-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Confluence v6.15.7を操作するためのCLIです。
|
|
5
5
|
Project-URL: Repository, https://github.com/kurusugawa-computer/confluence-cli
|
|
6
6
|
Author: Kurusugawa Computer Inc.
|
|
@@ -47,6 +47,24 @@ $ pip install kci-confluence-cli
|
|
|
47
47
|
$ uvx --from kci-confluence-cli confluence page get_body --page_id 123456
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
Python clientとして利用する場合は、共通のtimeout、redirect、response size policyを指定できます。
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from confluence.common.api import Api, TransportPolicy
|
|
54
|
+
|
|
55
|
+
api = Api(
|
|
56
|
+
username,
|
|
57
|
+
password,
|
|
58
|
+
base_url,
|
|
59
|
+
transport_policy=TransportPolicy(
|
|
60
|
+
timeout_second=10,
|
|
61
|
+
follow_redirects=False,
|
|
62
|
+
response_byte_bound=2 * 1024 * 1024,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
content = api.get_content_by_id(content_id, query_params={"expand": "space,body.storage,version,ancestors"})
|
|
66
|
+
```
|
|
67
|
+
|
|
50
68
|
|
|
51
69
|
# Quick Start
|
|
52
70
|
|
|
@@ -22,6 +22,24 @@ $ pip install kci-confluence-cli
|
|
|
22
22
|
$ uvx --from kci-confluence-cli confluence page get_body --page_id 123456
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Python clientとして利用する場合は、共通のtimeout、redirect、response size policyを指定できます。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from confluence.common.api import Api, TransportPolicy
|
|
29
|
+
|
|
30
|
+
api = Api(
|
|
31
|
+
username,
|
|
32
|
+
password,
|
|
33
|
+
base_url,
|
|
34
|
+
transport_policy=TransportPolicy(
|
|
35
|
+
timeout_second=10,
|
|
36
|
+
follow_redirects=False,
|
|
37
|
+
response_byte_bound=2 * 1024 * 1024,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
content = api.get_content_by_id(content_id, query_params={"expand": "space,body.storage,version,ancestors"})
|
|
41
|
+
```
|
|
42
|
+
|
|
25
43
|
|
|
26
44
|
# Quick Start
|
|
27
45
|
|
|
@@ -4,9 +4,11 @@ import copy
|
|
|
4
4
|
import logging
|
|
5
5
|
import mimetypes
|
|
6
6
|
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
+
from typing import Any, cast
|
|
9
10
|
|
|
11
|
+
from requests import HTTPError, Response
|
|
10
12
|
from requests_toolbelt import sessions
|
|
11
13
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
@@ -15,6 +17,25 @@ QueryParams = dict[str, Any]
|
|
|
15
17
|
RequestBody = dict[str, Any]
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
class ResponseByteBoundExceeded(RuntimeError):
|
|
21
|
+
"""Response body が configured byte bound を超えたことを表す。"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class TransportPolicy:
|
|
26
|
+
"""Confluence HTTP transport に共通する bounded execution policy。"""
|
|
27
|
+
|
|
28
|
+
timeout_second: float = 30
|
|
29
|
+
follow_redirects: bool = True
|
|
30
|
+
response_byte_bound: int | None = None
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
if self.timeout_second <= 0:
|
|
34
|
+
raise ValueError("timeout_secondは0より大きい値にしてください。")
|
|
35
|
+
if self.response_byte_bound is not None and self.response_byte_bound < 1:
|
|
36
|
+
raise ValueError("response_byte_boundは1以上の値にしてください。")
|
|
37
|
+
|
|
38
|
+
|
|
18
39
|
class Api:
|
|
19
40
|
"""
|
|
20
41
|
https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/
|
|
@@ -22,10 +43,19 @@ class Api:
|
|
|
22
43
|
Args:
|
|
23
44
|
base_url: example: `https://kurusugawa.jp/confluence`
|
|
24
45
|
delay_second: APIを連続で実行する際、何秒以上間隔を空けるか。Confluenceに負荷をかけすぎないようにするため、少なくとも0.3秒以上にすること。
|
|
46
|
+
transport_policy: timeout, redirect, response size の共通policy。
|
|
25
47
|
|
|
26
48
|
"""
|
|
27
49
|
|
|
28
|
-
def __init__(
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
username: str,
|
|
53
|
+
password: str,
|
|
54
|
+
base_url: str,
|
|
55
|
+
delay_second: float = 1,
|
|
56
|
+
*,
|
|
57
|
+
transport_policy: TransportPolicy | None = None,
|
|
58
|
+
) -> None:
|
|
29
59
|
if delay_second < 0.3:
|
|
30
60
|
raise RuntimeError(f"引数'delay_second'は0.3以上にしてください。 :: {delay_second=}")
|
|
31
61
|
|
|
@@ -34,6 +64,7 @@ class Api:
|
|
|
34
64
|
self.session.auth = (username, password)
|
|
35
65
|
|
|
36
66
|
self.delay_second = delay_second
|
|
67
|
+
self.transport_policy = transport_policy or TransportPolicy()
|
|
37
68
|
self._previous_timestamp: float = 0
|
|
38
69
|
|
|
39
70
|
@staticmethod
|
|
@@ -57,21 +88,24 @@ class Api:
|
|
|
57
88
|
params: QueryParams | None = None,
|
|
58
89
|
data: Any = None, # noqa: ANN401
|
|
59
90
|
**kwargs,
|
|
60
|
-
) ->
|
|
91
|
+
) -> Response:
|
|
61
92
|
"""
|
|
62
93
|
HTTP Requestを投げて、Responseを返す。
|
|
63
94
|
|
|
64
95
|
Args:
|
|
65
96
|
http_method:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
url:
|
|
98
|
+
headers:
|
|
99
|
+
params:
|
|
100
|
+
data:
|
|
101
|
+
kwargs:
|
|
71
102
|
|
|
72
103
|
Returns:
|
|
73
|
-
response
|
|
74
|
-
|
|
104
|
+
HTTP response。response byte bound が指定されている場合は、bodyをbound内で読み込んだresponseを返す。
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
HTTPError: HTTP status がerrorを表す場合。
|
|
108
|
+
ResponseByteBoundExceeded: Response body がconfigured byte boundを超えた場合。
|
|
75
109
|
|
|
76
110
|
"""
|
|
77
111
|
now = time.time()
|
|
@@ -79,9 +113,22 @@ class Api:
|
|
|
79
113
|
if diff_time < self.delay_second:
|
|
80
114
|
time.sleep(self.delay_second - diff_time)
|
|
81
115
|
|
|
116
|
+
kwargs.setdefault("timeout", self.transport_policy.timeout_second)
|
|
117
|
+
kwargs.setdefault("allow_redirects", self.transport_policy.follow_redirects)
|
|
118
|
+
if self.transport_policy.response_byte_bound is not None:
|
|
119
|
+
kwargs["stream"] = True
|
|
120
|
+
|
|
82
121
|
response = self.session.request(http_method, url, params=params, data=data, headers=headers, **kwargs)
|
|
83
122
|
self._previous_timestamp = time.time()
|
|
84
123
|
|
|
124
|
+
try:
|
|
125
|
+
response.raise_for_status()
|
|
126
|
+
except HTTPError:
|
|
127
|
+
response.close()
|
|
128
|
+
raise
|
|
129
|
+
response_content = self._response_content(response)
|
|
130
|
+
|
|
131
|
+
# `kwargs["json"]` は credential やcontentを含みうるため、arbitrary JSON payloadは意図的にlogへ複製しない。
|
|
85
132
|
logger.debug(
|
|
86
133
|
"Sent a request :: %s",
|
|
87
134
|
{
|
|
@@ -94,13 +141,39 @@ class Api:
|
|
|
94
141
|
},
|
|
95
142
|
"response": {
|
|
96
143
|
"status_code": response.status_code,
|
|
97
|
-
"content_length": len(
|
|
144
|
+
"content_length": len(response_content),
|
|
98
145
|
},
|
|
99
146
|
},
|
|
100
147
|
)
|
|
101
|
-
response.raise_for_status()
|
|
102
148
|
return response
|
|
103
149
|
|
|
150
|
+
def _response_content(self, response: Response) -> bytes:
|
|
151
|
+
byte_bound = self.transport_policy.response_byte_bound
|
|
152
|
+
if byte_bound is None:
|
|
153
|
+
return response.content
|
|
154
|
+
|
|
155
|
+
body = bytearray()
|
|
156
|
+
try:
|
|
157
|
+
content_length = response.headers.get("Content-Length")
|
|
158
|
+
if content_length is not None:
|
|
159
|
+
try:
|
|
160
|
+
if int(content_length) > byte_bound:
|
|
161
|
+
raise ResponseByteBoundExceeded(f"Response bodyがbyte boundを超えました。 :: {byte_bound=}")
|
|
162
|
+
except ValueError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
for chunk in response.iter_content(chunk_size=min(64 * 1024, byte_bound + 1)):
|
|
166
|
+
body.extend(chunk)
|
|
167
|
+
if len(body) > byte_bound:
|
|
168
|
+
raise ResponseByteBoundExceeded(f"Response bodyがbyte boundを超えました。 :: {byte_bound=}")
|
|
169
|
+
finally:
|
|
170
|
+
response.close()
|
|
171
|
+
|
|
172
|
+
response_content = bytes(body)
|
|
173
|
+
response._content = response_content
|
|
174
|
+
cast(Any, response)._content_consumed = True
|
|
175
|
+
return response_content
|
|
176
|
+
|
|
104
177
|
def get_attachments(self, content_id: str, *, query_params: QueryParams | None = None) -> dict[str, Any]:
|
|
105
178
|
url = f"content/{content_id}/child/attachment"
|
|
106
179
|
return self._request("get", url, params=query_params).json()
|
|
@@ -170,5 +243,4 @@ class Api:
|
|
|
170
243
|
|
|
171
244
|
https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-search
|
|
172
245
|
"""
|
|
173
|
-
|
|
174
|
-
return response.json()
|
|
246
|
+
return self._request("get", "content/search", params=query_params).json()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from requests import HTTPError, Response
|
|
7
|
+
|
|
8
|
+
from confluence.common.api import Api, ResponseByteBoundExceeded, TransportPolicy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _json_response(body: bytes = b'{"id":"123456"}') -> Response:
|
|
12
|
+
response = Response()
|
|
13
|
+
response.status_code = 200
|
|
14
|
+
response.url = "https://confluence.example.test/confluence/rest/api/content/123456"
|
|
15
|
+
response.headers["Content-Type"] = "application/json"
|
|
16
|
+
response._content = body # noqa: SLF001
|
|
17
|
+
response._content_consumed = True # noqa: SLF001
|
|
18
|
+
return response
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_transport_policy_is_applied_to_protocol_request(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
22
|
+
api = Api(
|
|
23
|
+
"reader",
|
|
24
|
+
"secret",
|
|
25
|
+
"https://confluence.example.test/confluence",
|
|
26
|
+
transport_policy=TransportPolicy(timeout_second=7, follow_redirects=False, response_byte_bound=1024),
|
|
27
|
+
)
|
|
28
|
+
request_argument: dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
def fake_request(method: str, url: str, **kwargs: Any) -> Response: # noqa: ANN401
|
|
31
|
+
request_argument.update(method=method, url=url, **kwargs)
|
|
32
|
+
return _json_response()
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(api.session, "request", fake_request)
|
|
35
|
+
|
|
36
|
+
result = api.get_content_by_id("123456", query_params={"expand": "space,body.storage"})
|
|
37
|
+
|
|
38
|
+
assert result == {"id": "123456"}
|
|
39
|
+
assert request_argument == {
|
|
40
|
+
"method": "get",
|
|
41
|
+
"url": "content/123456",
|
|
42
|
+
"params": {"expand": "space,body.storage"},
|
|
43
|
+
"data": None,
|
|
44
|
+
"headers": None,
|
|
45
|
+
"timeout": 7,
|
|
46
|
+
"allow_redirects": False,
|
|
47
|
+
"stream": True,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize(
|
|
52
|
+
("policy", "message"),
|
|
53
|
+
[
|
|
54
|
+
(TransportPolicy(timeout_second=1, response_byte_bound=4), "byte bound"),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
def test_response_byte_bound_is_enforced(
|
|
58
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
59
|
+
policy: TransportPolicy,
|
|
60
|
+
message: str,
|
|
61
|
+
) -> None:
|
|
62
|
+
api = Api("reader", "secret", "https://confluence.example.test/confluence", transport_policy=policy)
|
|
63
|
+
response = _json_response(b"12345")
|
|
64
|
+
monkeypatch.setattr(api.session, "request", lambda *_args, **_kwargs: response)
|
|
65
|
+
|
|
66
|
+
with pytest.raises(ResponseByteBoundExceeded, match=message):
|
|
67
|
+
api.get_content_by_id("123456")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_streamed_error_response_is_closed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
71
|
+
api = Api(
|
|
72
|
+
"reader",
|
|
73
|
+
"secret",
|
|
74
|
+
"https://confluence.example.test/confluence",
|
|
75
|
+
transport_policy=TransportPolicy(response_byte_bound=1024),
|
|
76
|
+
)
|
|
77
|
+
response = _json_response()
|
|
78
|
+
response.status_code = 500
|
|
79
|
+
closed = False
|
|
80
|
+
|
|
81
|
+
def close() -> None:
|
|
82
|
+
nonlocal closed
|
|
83
|
+
closed = True
|
|
84
|
+
|
|
85
|
+
monkeypatch.setattr(response, "close", close)
|
|
86
|
+
monkeypatch.setattr(api.session, "request", lambda *_args, **_kwargs: response)
|
|
87
|
+
|
|
88
|
+
with pytest.raises(HTTPError):
|
|
89
|
+
api.get_content_by_id("123456")
|
|
90
|
+
|
|
91
|
+
assert closed is True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_streamed_success_response_is_closed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
95
|
+
api = Api(
|
|
96
|
+
"reader",
|
|
97
|
+
"secret",
|
|
98
|
+
"https://confluence.example.test/confluence",
|
|
99
|
+
transport_policy=TransportPolicy(response_byte_bound=1024),
|
|
100
|
+
)
|
|
101
|
+
response = _json_response()
|
|
102
|
+
closed = False
|
|
103
|
+
|
|
104
|
+
def close() -> None:
|
|
105
|
+
nonlocal closed
|
|
106
|
+
closed = True
|
|
107
|
+
|
|
108
|
+
monkeypatch.setattr(response, "close", close)
|
|
109
|
+
monkeypatch.setattr(api.session, "request", lambda *_args, **_kwargs: response)
|
|
110
|
+
|
|
111
|
+
assert api.get_content_by_id("123456") == {"id": "123456"}
|
|
112
|
+
assert closed is True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_transport_policy_rejects_non_positive_bounds() -> None:
|
|
116
|
+
with pytest.raises(ValueError, match="timeout_second"):
|
|
117
|
+
TransportPolicy(timeout_second=0)
|
|
118
|
+
with pytest.raises(ValueError, match="response_byte_bound"):
|
|
119
|
+
TransportPolicy(response_byte_bound=0)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_search_uses_common_transport_policy(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
123
|
+
api = Api("reader", "secret", "https://confluence.example.test/confluence")
|
|
124
|
+
request_argument: dict[str, Any] = {}
|
|
125
|
+
|
|
126
|
+
def fake_request(method: str, url: str, **kwargs: Any) -> Response: # noqa: ANN401
|
|
127
|
+
request_argument.update(method=method, url=url, **kwargs)
|
|
128
|
+
return _json_response(b"{}")
|
|
129
|
+
|
|
130
|
+
monkeypatch.setattr(api.session, "request", fake_request)
|
|
131
|
+
|
|
132
|
+
assert api.search_content(query_params={"cql": "type=page"}) == {}
|
|
133
|
+
assert request_argument["timeout"] == 30
|
|
134
|
+
assert request_argument["allow_redirects"] is True
|