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.
- kci_confluence_cli-0.1.0/PKG-INFO +87 -0
- kci_confluence_cli-0.1.0/README.md +58 -0
- kci_confluence_cli-0.1.0/confluence/__init__.py +5 -0
- kci_confluence_cli-0.1.0/confluence/__main__.py +54 -0
- kci_confluence_cli-0.1.0/confluence/__version__.py +1 -0
- kci_confluence_cli-0.1.0/confluence/attachment/__init__.py +0 -0
- kci_confluence_cli-0.1.0/confluence/attachment/create_attachment.py +103 -0
- kci_confluence_cli-0.1.0/confluence/attachment/delete_attachment.py +86 -0
- kci_confluence_cli-0.1.0/confluence/attachment/get_attachment.py +55 -0
- kci_confluence_cli-0.1.0/confluence/attachment/subcommand.py +27 -0
- kci_confluence_cli-0.1.0/confluence/common/__init__.py +0 -0
- kci_confluence_cli-0.1.0/confluence/common/api.py +164 -0
- kci_confluence_cli-0.1.0/confluence/common/cli.py +241 -0
- kci_confluence_cli-0.1.0/confluence/common/utils.py +78 -0
- kci_confluence_cli-0.1.0/confluence/content/__init__.py +0 -0
- kci_confluence_cli-0.1.0/confluence/content/get_content_by_id.py +37 -0
- kci_confluence_cli-0.1.0/confluence/content/subcommand.py +23 -0
- kci_confluence_cli-0.1.0/confluence/data/logging.yaml +19 -0
- kci_confluence_cli-0.1.0/confluence/local/__init__.py +0 -0
- kci_confluence_cli-0.1.0/confluence/local/convert_html_to_xml.py +110 -0
- kci_confluence_cli-0.1.0/confluence/local/subcommand.py +24 -0
- kci_confluence_cli-0.1.0/confluence/page/__init__.py +0 -0
- kci_confluence_cli-0.1.0/confluence/page/get_page_body.py +52 -0
- kci_confluence_cli-0.1.0/confluence/page/subcommand.py +26 -0
- kci_confluence_cli-0.1.0/confluence/page/update_page.py +49 -0
- kci_confluence_cli-0.1.0/confluence/py.typed +0 -0
- 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,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のバージョンタグを取得している。変更不要
|
|
File without changes
|
|
@@ -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()
|