kci-confluence-cli 0.1.0__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.
Files changed (27) hide show
  1. kci_confluence_cli-0.1.0/PKG-INFO +87 -0
  2. kci_confluence_cli-0.1.0/README.md +58 -0
  3. kci_confluence_cli-0.1.0/confluence/__init__.py +5 -0
  4. kci_confluence_cli-0.1.0/confluence/__main__.py +54 -0
  5. kci_confluence_cli-0.1.0/confluence/__version__.py +1 -0
  6. kci_confluence_cli-0.1.0/confluence/attachment/__init__.py +0 -0
  7. kci_confluence_cli-0.1.0/confluence/attachment/create_attachment.py +103 -0
  8. kci_confluence_cli-0.1.0/confluence/attachment/delete_attachment.py +86 -0
  9. kci_confluence_cli-0.1.0/confluence/attachment/get_attachment.py +55 -0
  10. kci_confluence_cli-0.1.0/confluence/attachment/subcommand.py +27 -0
  11. kci_confluence_cli-0.1.0/confluence/common/__init__.py +0 -0
  12. kci_confluence_cli-0.1.0/confluence/common/api.py +164 -0
  13. kci_confluence_cli-0.1.0/confluence/common/cli.py +241 -0
  14. kci_confluence_cli-0.1.0/confluence/common/utils.py +78 -0
  15. kci_confluence_cli-0.1.0/confluence/content/__init__.py +0 -0
  16. kci_confluence_cli-0.1.0/confluence/content/get_content_by_id.py +37 -0
  17. kci_confluence_cli-0.1.0/confluence/content/subcommand.py +23 -0
  18. kci_confluence_cli-0.1.0/confluence/data/logging.yaml +19 -0
  19. kci_confluence_cli-0.1.0/confluence/local/__init__.py +0 -0
  20. kci_confluence_cli-0.1.0/confluence/local/convert_html_to_xml.py +110 -0
  21. kci_confluence_cli-0.1.0/confluence/local/subcommand.py +24 -0
  22. kci_confluence_cli-0.1.0/confluence/page/__init__.py +0 -0
  23. kci_confluence_cli-0.1.0/confluence/page/get_page_body.py +52 -0
  24. kci_confluence_cli-0.1.0/confluence/page/subcommand.py +26 -0
  25. kci_confluence_cli-0.1.0/confluence/page/update_page.py +49 -0
  26. kci_confluence_cli-0.1.0/confluence/py.typed +0 -0
  27. kci_confluence_cli-0.1.0/pyproject.toml +137 -0
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.1
2
+ Name: kci-confluence-cli
3
+ Version: 0.1.0
4
+ Summary: 来栖川電算が利用しているConfluence v6.15.7を操作するためのCLIです。
5
+ Home-page: https://github.com/kurusugawa-computer/confluence-cli
6
+ License: MIT
7
+ Author: Kurusugawa Computer Inc.
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Utilities
20
+ Requires-Dist: backoff
21
+ Requires-Dist: more_itertools
22
+ Requires-Dist: pyquery (>=2,<3)
23
+ Requires-Dist: pyyaml
24
+ Requires-Dist: requests
25
+ Requires-Dist: requests-toolbelt (>=1,<2)
26
+ Project-URL: Repository, https://github.com/kurusugawa-computer/confluence-cli
27
+ Description-Content-Type: text/markdown
28
+
29
+ # confluence-cli
30
+ 来栖川電算が利用しているConfluence v6.15.7を操作するためのCLIです。
31
+
32
+ # Requirements
33
+ Python 3.9+
34
+
35
+ # Install
36
+
37
+ ```
38
+ $ pip install kci-confluence-cli
39
+ ```
40
+
41
+ # 使い方
42
+
43
+ ## 認証情報の指定
44
+ 以下のいずれかの方法で指定できます。
45
+
46
+ * 環境変数 `CONFLUENCE_USER_NAME` , `CONFLUENCE_USER_PASSWORD` に指定する
47
+ * コマンドライン引数 `--confluence_user_name` , `--confluence_user_password`に指定する
48
+
49
+ 上記の方法で認証情報が指定されない場合は、標準入力から認証情報を入力できます。
50
+
51
+
52
+ ## ConfluenceのURLの指定
53
+ アクセスするConfluenceのURLを、以下のいずれかの方法で指定できます。
54
+
55
+ * 環境変数 `CONFLUENCE_BASE_URL`に指定する
56
+ * コマンドライン引数 `--confluence_base_url`に指定する
57
+
58
+ 上記の方法で指定されない場合は、標準入力から入力できます。
59
+
60
+ 来栖川電算のConfluneceにアクセスする場合は、`https://kurusugawa.jp/confluence`を指定してください。
61
+
62
+
63
+ # Command Reference
64
+ ### attachment get
65
+ 添付ファイル情報の取得
66
+
67
+
68
+ ### attachment create
69
+ 添付ファイルの作成
70
+
71
+
72
+ ### attachment delete
73
+ 添付ファイルを削除します。
74
+
75
+ ※添付ファイルをゴミ箱から完全に削除すると、ページを更新できない場合があります。その場合は、一時的に前のバージョンに戻せば、ページを更新できるようになります。
76
+ https://qiita.com/yuji38kwmt/items/c88f039e02b8508926e6
77
+
78
+
79
+ ### attachment get
80
+ コンテンツ(ページやブログ、添付ファイルなど)の情報を取得します。
81
+
82
+ ### local convert_html
83
+ HTMLをConfluence用のXMLに変換します。
84
+ Google DocsのページをConfluenceに移行するときなどに利用できます。
85
+ 具体的な手順は以下を参照してください。
86
+ https://github.com/kurusugawa-computer/confluence-cli/wiki/Google-Docs%E3%82%92Confluence%E3%81%AB%E7%A7%BB%E8%A1%8C%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95
87
+
@@ -0,0 +1,58 @@
1
+ # confluence-cli
2
+ 来栖川電算が利用しているConfluence v6.15.7を操作するためのCLIです。
3
+
4
+ # Requirements
5
+ Python 3.9+
6
+
7
+ # Install
8
+
9
+ ```
10
+ $ pip install kci-confluence-cli
11
+ ```
12
+
13
+ # 使い方
14
+
15
+ ## 認証情報の指定
16
+ 以下のいずれかの方法で指定できます。
17
+
18
+ * 環境変数 `CONFLUENCE_USER_NAME` , `CONFLUENCE_USER_PASSWORD` に指定する
19
+ * コマンドライン引数 `--confluence_user_name` , `--confluence_user_password`に指定する
20
+
21
+ 上記の方法で認証情報が指定されない場合は、標準入力から認証情報を入力できます。
22
+
23
+
24
+ ## ConfluenceのURLの指定
25
+ アクセスするConfluenceのURLを、以下のいずれかの方法で指定できます。
26
+
27
+ * 環境変数 `CONFLUENCE_BASE_URL`に指定する
28
+ * コマンドライン引数 `--confluence_base_url`に指定する
29
+
30
+ 上記の方法で指定されない場合は、標準入力から入力できます。
31
+
32
+ 来栖川電算のConfluneceにアクセスする場合は、`https://kurusugawa.jp/confluence`を指定してください。
33
+
34
+
35
+ # Command Reference
36
+ ### attachment get
37
+ 添付ファイル情報の取得
38
+
39
+
40
+ ### attachment create
41
+ 添付ファイルの作成
42
+
43
+
44
+ ### attachment delete
45
+ 添付ファイルを削除します。
46
+
47
+ ※添付ファイルをゴミ箱から完全に削除すると、ページを更新できない場合があります。その場合は、一時的に前のバージョンに戻せば、ページを更新できるようになります。
48
+ https://qiita.com/yuji38kwmt/items/c88f039e02b8508926e6
49
+
50
+
51
+ ### attachment get
52
+ コンテンツ(ページやブログ、添付ファイルなど)の情報を取得します。
53
+
54
+ ### local convert_html
55
+ HTMLをConfluence用のXMLに変換します。
56
+ Google DocsのページをConfluenceに移行するときなどに利用できます。
57
+ 具体的な手順は以下を参照してください。
58
+ https://github.com/kurusugawa-computer/confluence-cli/wiki/Google-Docs%E3%82%92Confluence%E3%81%AB%E7%A7%BB%E8%A1%8C%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95
@@ -0,0 +1,5 @@
1
+ from .__version__ import __version__
2
+
3
+ __all__ = [
4
+ "__version__",
5
+ ]
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from collections.abc import Sequence
6
+ from typing import Optional
7
+
8
+ import confluence
9
+ import confluence.attachment.subcommand
10
+ import confluence.content.subcommand
11
+ import confluence.local.subcommand
12
+ import confluence.page.subcommand
13
+ from confluence.common.cli import PrettyHelpFormatter
14
+ from confluence.common.utils import set_logger
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def create_parser() -> argparse.ArgumentParser:
20
+ parser = argparse.ArgumentParser(description="Command Line Interface for Confluence", formatter_class=PrettyHelpFormatter, allow_abbrev=False)
21
+ parser.add_argument("--version", action="version", version=f"confluence {confluence.__version__}")
22
+ parser.set_defaults(command_help=parser.print_help)
23
+
24
+ subparsers = parser.add_subparsers(dest="command_name")
25
+
26
+ confluence.attachment.subcommand.add_parser(subparsers)
27
+ confluence.content.subcommand.add_parser(subparsers)
28
+ confluence.page.subcommand.add_parser(subparsers)
29
+ confluence.local.subcommand.add_parser(subparsers)
30
+ return parser
31
+
32
+
33
+ def main(arguments: Optional[Sequence[str]] = None):
34
+ parser = create_parser()
35
+ if arguments is None:
36
+ args = parser.parse_args()
37
+ else:
38
+ args = parser.parse_args(arguments)
39
+
40
+ if hasattr(args, "subcommand_func"):
41
+ try:
42
+ set_logger(args.debug)
43
+ args.subcommand_func(args)
44
+ except Exception as e:
45
+ logger.exception(e)
46
+ raise e
47
+
48
+ else:
49
+ # 未知のサブコマンドの場合はヘルプを表示
50
+ args.command_help()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import confluence
9
+ from confluence.common.api import Api
10
+ from confluence.common.cli import create_api_instance
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def create_attachments_from_file_list(
16
+ api: Api, content_id: str, query_params: dict[str, Any], files: list[Path], filename_pattern: None | str, mime_type: None | str
17
+ ) -> None:
18
+ logger.info(f"{len(files)}件のファイルをアップロードします。")
19
+ success_count = 0
20
+ total_count = 0
21
+ for file in files:
22
+ if not file.is_file():
23
+ logger.warning(f"'{file}'はファイルでないので、アップロードしません。")
24
+ continue
25
+
26
+ if filename_pattern is not None and not file.glob(filename_pattern):
27
+ continue
28
+
29
+ try:
30
+ total_count += 1
31
+ api.create_attachment(content_id, file, query_params=query_params, mime_type=mime_type)
32
+ logger.debug(f"{total_count+1}件目: '{file}'をアップロードしました。")
33
+ except Exception:
34
+ logger.warning(f"'{file}'のアップロードに失敗しました。", exc_info=True)
35
+ continue
36
+
37
+ logger.debug(f"'{file}'をアップロードしました。")
38
+ success_count += 1
39
+
40
+ logger.info(f"{success_count}/{total_count} 件のファイルをアップロードしました。")
41
+
42
+
43
+ def create_attachments_from_directory(
44
+ api: Api, content_id: str, query_params: dict[str, Any], directory: Path, filename_pattern: None | str, mime_type: None | str
45
+ ) -> None:
46
+ success_count = 0
47
+ total_count = 0
48
+ if not directory.is_dir():
49
+ logger.error(f"'{directory}'はディレクトリでないので、終了します。")
50
+ return
51
+
52
+ logger.info(f"ディレクトリ'{directory}'内のファイルをアップロードします。")
53
+
54
+ for file in directory.iterdir():
55
+ if not file.is_file():
56
+ continue
57
+ if filename_pattern is not None and not file.glob(filename_pattern):
58
+ continue
59
+ try:
60
+ total_count += 1
61
+ api.create_attachment(content_id, file, query_params=query_params, mime_type=mime_type)
62
+ logger.debug(f"{total_count}件目: '{file}'をアップロードしました。")
63
+ except Exception:
64
+ logger.warning(f"'{file}'のアップロードに失敗しました。", exc_info=True)
65
+ continue
66
+ success_count += 1
67
+
68
+ logger.info(f"{success_count}/{total_count}件のファイルをアップロードしました。")
69
+
70
+
71
+ def main(args: argparse.Namespace) -> None:
72
+ api = create_api_instance(args)
73
+ content_id = args.content_id
74
+ query_params = {"allowDuplicated": args.allow_duplicated}
75
+ if args.file is not None:
76
+ create_attachments_from_file_list(api, content_id, query_params, args.file, filename_pattern=args.filename_pattern, mime_type=args.mime_type)
77
+ elif args.dir is not None:
78
+ create_attachments_from_directory(api, content_id, query_params, args.dir, filename_pattern=args.filename_pattern, mime_type=args.mime_type)
79
+
80
+
81
+ def add_arguments_to_parser(parser: argparse.ArgumentParser):
82
+ parser.add_argument("-c", "--content_id", required=True, help="ファイルのアップロード先であるページのcontent_id")
83
+
84
+ file_group = parser.add_mutually_exclusive_group(required=True)
85
+ file_group.add_argument("--file", type=Path, nargs="+", help="アップロードするファイル")
86
+ file_group.add_argument("--dir", type=Path, help="アップロードするディレクトリ")
87
+
88
+ parser.add_argument("--mime_type", type=str, help="ファイル名からMIMEタイプが判別できないときに、この値を添付ファイルのMIMEタイプとします。")
89
+
90
+ parser.add_argument("--allow_duplicated", action="store_true", help="指定した場合は、すでに同じファイルが存在しても上書きします。")
91
+
92
+ parser.add_argument("--filename_pattern", help="glob形式のパターンに一致するファイル名だけアップロードします。(ex) '*.png'")
93
+ parser.set_defaults(subcommand_func=main)
94
+
95
+
96
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
97
+ subcommand_name = "create"
98
+ subcommand_help = "添付ファイルを作成します。"
99
+
100
+ parser = confluence.common.cli.add_parser(subparsers, subcommand_name, subcommand_help)
101
+
102
+ add_arguments_to_parser(parser)
103
+ return parser
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from typing import Any
6
+
7
+ import confluence
8
+ from confluence.common.api import Api
9
+ from confluence.common.cli import create_api_instance, prompt_yesnoall
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def get_attachments(api: Api, content_id: str, *, filename: None | str, media_type: None | str) -> list[dict[str, Any]]:
15
+ limit = 50
16
+ start = 0
17
+ results: list[dict[str, Any]] = []
18
+ while True:
19
+ result = api.get_attachments(content_id, query_params={"filename": filename, "mediaType": media_type, "start": start, "limit": limit})
20
+ results.extend(result["results"])
21
+ if result["size"] < limit:
22
+ break
23
+ start = start + limit
24
+ return results
25
+
26
+
27
+ def main(args: argparse.Namespace) -> None:
28
+ api = create_api_instance(args)
29
+ content_id = args.content_id
30
+
31
+ page = api.get_content_by_id(content_id)
32
+ results: list[dict[str, Any]] = get_attachments(api, content_id, filename=args.filename, media_type=args.media_type)
33
+ if len(results) == 0:
34
+ logger.info(f"page title='{page['title']}'の削除対象の添付ファイルは0件なので、終了します。")
35
+ return
36
+
37
+ logger.info(f"page title='{page['title']}'の添付ファイル{len(results)}件を削除します。")
38
+
39
+ is_purged = args.purge
40
+ success_count = 0
41
+
42
+ all_yes = False
43
+ for index, attachment in enumerate(results):
44
+ attachment_id = attachment["id"]
45
+ attachment_title = attachment["title"]
46
+ try:
47
+ if not all_yes:
48
+ confirm_message = f"id='{attachment_id}', title='{attachment_title}'を削除しますか?"
49
+ if is_purged:
50
+ confirm_message += "ゴミ箱からも完全に削除します。復元できません。"
51
+ else:
52
+ confirm_message += "ゴミ箱に移動します。"
53
+
54
+ yes, all_yes = prompt_yesnoall(confirm_message)
55
+ if yes or all_yes:
56
+ logger.debug(f"{index+1}件目: id='{attachment_id}', title='{attachment_title}'をゴミ箱に移動します。 ")
57
+ api.delete_content(attachment_id, query_params={"status": "current"})
58
+ if is_purged:
59
+ logger.debug(f"{index+1}件目: id='{attachment_id}', title='{attachment_title}'をゴミ箱から完全に削除します。 ")
60
+ api.delete_content(attachment_id, query_params={"status": "trashed"})
61
+ success_count += 1
62
+
63
+ except Exception:
64
+ logger.warning(f"{index+1}件目: id='{attachment_id}', title='{attachment_title}'の削除に失敗しました。", exc_info=True)
65
+ continue
66
+
67
+ logger.info(f"{success_count}/{len(results)} 件の添付ファイルを削除しました。")
68
+
69
+
70
+ def add_arguments_to_parser(parser: argparse.ArgumentParser):
71
+ parser.add_argument("-c", "--content_id", required=True, help="削除したい添付ファイルが存在するページのcontent_id")
72
+
73
+ parser.add_argument("--filename", help="filter parameter to return only the Attachment with the matching file name")
74
+ parser.add_argument("--media_type", help="filter parameter to return only Attachments with a matching Media-Type")
75
+ parser.add_argument("--purge", action="store_true", help="指定すると、ゴミ箱からも完全に削除します。復元できません。")
76
+ parser.set_defaults(subcommand_func=main)
77
+
78
+
79
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
80
+ subcommand_name = "delete"
81
+ subcommand_help = "添付ファイルを削除します。"
82
+
83
+ parser = confluence.common.cli.add_parser(subparsers, subcommand_name, subcommand_help)
84
+
85
+ add_arguments_to_parser(parser)
86
+ return parser
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import confluence
9
+ from confluence.common.cli import create_api_instance
10
+ from confluence.common.utils import print_json
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def main(args: argparse.Namespace) -> None:
16
+ api = create_api_instance(args)
17
+ content_id = args.content_id
18
+ expand = ",".join(args.expand) if args.expand else None
19
+
20
+ limit = 50
21
+ start = 0
22
+ results: list[dict[str, Any]] = []
23
+ while True:
24
+ result = api.get_attachments(
25
+ content_id, query_params={"filename": args.filename, "mediaType": args.media_type, "start": start, "limit": limit, "expand": expand}
26
+ )
27
+ results.extend(result["results"])
28
+ if result["size"] < limit:
29
+ break
30
+ start = start + limit
31
+
32
+ print_json(results, is_pretty=True, output=args.output)
33
+
34
+
35
+ def add_arguments_to_parser(parser: argparse.ArgumentParser):
36
+ parser.add_argument("-c", "--content_id", required=True, help="取得した添付ファイルが存在するページのcontent_id")
37
+
38
+ parser.add_argument("--filename", help="filter parameter to return only the Attachment with the matching file name")
39
+ parser.add_argument("--media_type", help="filter parameter to return only Attachments with a matching Media-Type")
40
+
41
+ parser.add_argument("--expand", nargs="+", help="取得したい情報のプロパティ。指定できる値は出力結果の`_expandable`を参照してください。")
42
+
43
+ parser.add_argument("-o", "--output", type=Path, help="出力先")
44
+
45
+ parser.set_defaults(subcommand_func=main)
46
+
47
+
48
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
49
+ subcommand_name = "get"
50
+ subcommand_help = "添付ファイルの情報を取得します。"
51
+
52
+ parser = confluence.common.cli.add_parser(subparsers, subcommand_name, subcommand_help)
53
+
54
+ add_arguments_to_parser(parser)
55
+ return parser
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Optional
5
+
6
+ import confluence
7
+ import confluence.attachment.create_attachment
8
+ import confluence.attachment.delete_attachment
9
+ import confluence.attachment.get_attachment
10
+
11
+
12
+ def add_arguments_to_parser(parser: argparse.ArgumentParser):
13
+ subparsers = parser.add_subparsers(dest="subcommand_name")
14
+
15
+ # サブコマンドの定義
16
+ confluence.attachment.create_attachment.add_parser(subparsers)
17
+ confluence.attachment.delete_attachment.add_parser(subparsers)
18
+ confluence.attachment.get_attachment.add_parser(subparsers)
19
+
20
+
21
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
22
+ subcommand_name = "attachment"
23
+ subcommand_help = "添付ファイルに関するサブコマンド"
24
+
25
+ parser = confluence.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, is_subcommand=False)
26
+ add_arguments_to_parser(parser)
27
+ return parser
File without changes
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import mimetypes
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from requests_toolbelt import sessions
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ QueryParams = dict[str, Any]
14
+ RequestBody = dict[str, Any]
15
+
16
+
17
+ class Api:
18
+ """
19
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/
20
+
21
+ Args:
22
+ base_url: example: `https://kurusugawa.jp/confluence`
23
+ delay_second: APIを連続で実行する際、何秒以上間隔を空けるか。Confluenceに負荷をかけすぎないようにするため、少なくとも0.3秒以上にすること。
24
+
25
+ """
26
+
27
+ def __init__(self, username: str, password: str, base_url: str, delay_second: float = 1) -> None:
28
+ if delay_second < 0.3:
29
+ raise RuntimeError(f"引数'delay_second'は0.3以上にしてください。 :: {delay_second=}")
30
+
31
+ self.base_url = base_url
32
+ self.session = sessions.BaseUrlSession(base_url=base_url + "/rest/api/")
33
+ self.session.auth = (username, password)
34
+
35
+ self.delay_second = delay_second
36
+ self._previous_timestamp: float = 0
37
+
38
+ def _request(
39
+ self,
40
+ http_method: str,
41
+ url: str,
42
+ *,
43
+ headers: Optional[dict[str, Any]] = None,
44
+ params: Optional[QueryParams] = None,
45
+ data: Any = None, # noqa: ANN401
46
+ **kwargs,
47
+ ) -> Any: # noqa: ANN401
48
+ """
49
+ HTTP Requestを投げて、Responseを返す。
50
+
51
+ Args:
52
+ http_method:
53
+ url_path:
54
+ query_params:
55
+ header_params:
56
+ body:
57
+ log_response_with_error: HTTP Errorが発生したときにレスポンスの中身をログに出力するか否か
58
+
59
+ Returns:
60
+ responseの中身。content_typeにより型が変わる。
61
+ application/jsonならdict型, text/*ならばstr型, それ以外ならばbite型。
62
+
63
+ """
64
+ now = time.time()
65
+ diff_time = now - self._previous_timestamp
66
+ if diff_time < self.delay_second:
67
+ time.sleep(self.delay_second - diff_time)
68
+
69
+ response = self.session.request(http_method, url, params=params, data=data, headers=headers, **kwargs)
70
+ self._previous_timestamp = time.time()
71
+
72
+ logger.debug(
73
+ "Sent a request :: %s",
74
+ {
75
+ "requests": {
76
+ "http_method": http_method,
77
+ "url": url,
78
+ "query_params": params,
79
+ "request_body_json": data,
80
+ "headers": response.request.headers,
81
+ },
82
+ "response": {
83
+ "status_code": response.status_code,
84
+ "content_length": len(response.content),
85
+ },
86
+ },
87
+ )
88
+ response.raise_for_status()
89
+ return response
90
+
91
+ def get_attachments(self, content_id: str, *, query_params: Optional[QueryParams] = None) -> dict[str, Any]:
92
+ url = f"content/{content_id}/child/attachment"
93
+ return self._request("get", url, params=query_params).json()
94
+
95
+ def create_attachment(
96
+ self, content_id: str, file: Path, *, query_params: Optional[QueryParams] = None, mime_type: Optional[str] = None
97
+ ) -> dict[str, Any]:
98
+ """
99
+ Args:
100
+ mime_type: mimetypes.guess_type()で自動判定でMIMEタイプを取得できないときに、この値をMIMEタイプにします。
101
+ """
102
+ headers = {"X-Atlassian-Token": "nocheck"}
103
+ url = f"content/{content_id}/child/attachment"
104
+ new_mime_type, _ = mimetypes.guess_type(file)
105
+ if new_mime_type is None:
106
+ new_mime_type = mime_type
107
+
108
+ with file.open("rb") as f:
109
+ files = {"file": (file.name, f, new_mime_type)}
110
+ return self._request("post", url, params=query_params, files=files, headers=headers).json()
111
+
112
+ def get_content(self, *, query_params: Optional[QueryParams] = None) -> list[dict[str, Any]]:
113
+ """
114
+ Returns a paginated list of Content.
115
+
116
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-getContent
117
+ """
118
+ return self._request("get", "content", params=query_params).json()
119
+
120
+ def get_content_by_id(self, content_id: str, *, query_params: Optional[QueryParams] = None) -> dict[str, Any]:
121
+ """
122
+ Returns a piece of Content.
123
+
124
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-getContentById
125
+ """
126
+ return self._request("get", f"content/{content_id}", params=query_params).json()
127
+
128
+ def update_content(
129
+ self, content_id: str, *, query_params: Optional[QueryParams] = None, request_body: Optional[RequestBody] = None
130
+ ) -> dict[str, Any]:
131
+ """
132
+ Updates a piece of Content, including changes to content status
133
+
134
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-update
135
+ """
136
+ headers = {"Content-Type": "application/json", "X-Atlassian-Token": "nocheck"}
137
+ return self._request("put", f"content/{content_id}", headers=headers, params=query_params, data=request_body).json()
138
+
139
+ def delete_content(self, content_id: str, *, query_params: Optional[QueryParams] = None) -> None:
140
+ """
141
+ Trashes or purges a piece of Content, based on its {@link ContentType} and {@link ContentStatus}.
142
+
143
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-delete
144
+
145
+ Notes:
146
+ クエリパラーメタ`status`に`trashed`を指定すると400エラーが発生した。
147
+ """
148
+ self._request("delete", f"content/{content_id}", params=query_params)
149
+
150
+ def get_content_history(self, content_id: str, *, query_params: Optional[QueryParams] = None):
151
+ """Returns the history of a particular piece of content
152
+
153
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-getHistory
154
+ """
155
+ return self._request("get", f"content/{content_id}/history", params=query_params).json()
156
+
157
+ def search_content(self, *, query_params: Optional[QueryParams] = None) -> dict[str, Any]:
158
+ """
159
+ Fetch a list of content using the Confluence Query Language (CQL)
160
+
161
+ https://docs.atlassian.com/ConfluenceServer/rest/6.15.7/#api/content-search
162
+ """
163
+ response = self.session.get("content/search", params=query_params)
164
+ return response.json()