wpctl 1.0.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.
wpctl-1.0.0/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 templates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
wpctl-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: wpctl
3
+ Version: 1.0.0
4
+ Summary: WordPress APIを活用して記事を投稿・管理するCLIツール
5
+ Author-email: "Ryoh.Ya" <dev.p.ry.yamafuji@gmail.com>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: markdown>=3.7
10
+ Requires-Dist: nh3>=0.2.18
11
+ Requires-Dist: requests>=2.32
12
+ Dynamic: license-file
13
+
14
+ # wpctl
15
+
16
+ WordPress APIを活用して、記事を投稿・管理するCLIツール。
17
+
18
+ ## 概要
19
+
20
+ MarkDown / テキスト / HTMLファイルを読み込み、WordPress REST APIを通じて記事の投稿・更新を行います。
21
+
22
+ ## 機能
23
+
24
+ | 機能 | コマンド | 説明 |
25
+ | -------- | ------------------- | ------------------------------------------ |
26
+ | 記事投稿 | `wpctl post create` | Markdownなどのファイルを記事として投稿する |
27
+ | 記事更新 | `wpctl post update` | 既存の記事をファイルの内容で更新する |
28
+
29
+ ## インストール
30
+
31
+ ```sh
32
+ pip install .
33
+ ```
34
+
35
+ ## 使い方
36
+
37
+ ### 記事を投稿する
38
+
39
+ ```sh
40
+ wpctl post create [-t "記事のタイトル"] <FilePath>
41
+ ```
42
+
43
+ | 引数 | オプション | 必須 | 説明 |
44
+ | ---- | ---------- | ---- | ---- |
45
+ | `FilePath` | | true | 投稿するファイルのパス |
46
+ | `title` | `--title`, `-t` | | 記事タイトル(デフォルト: `タイトル未設定`)|
47
+
48
+ ### 記事を更新する
49
+
50
+ ```sh
51
+ wpctl post update --id <ID> [-t "記事のタイトル"] <FilePath>
52
+ ```
53
+
54
+ | 引数 | オプション | 必須 | 説明 |
55
+ | ---- | ---------- | ---- | ---- |
56
+ | `FilePath` | | true | 投稿するファイルのパス |
57
+ | `id` | `--id` | true | 更新対象の記事ID |
58
+ | `title` | `--title`, `-t` | | 記事タイトル(デフォルト: `タイトル未設定`)|
59
+
60
+ ## 環境変数
61
+
62
+ | 環境変数 | 必須 | 説明 |
63
+ | ----------------- | ---- | --------------------------- |
64
+ | `WP_USER` | true | WordPressのユーザー名 |
65
+ | `WP_APP_PASSWORD` | true | WordPressのアプリパスワード |
66
+
67
+ > `.env` ファイルは非対応です。あらかじめシェルの環境変数に設定してください。
68
+
69
+ ## 対応ファイル形式
70
+
71
+ - `.txt`
72
+ - `.md`
73
+ - `.html`
74
+
75
+ ## Tool
76
+
77
+ ### Test
78
+
79
+ ```sh
80
+ pip install -r requirements-dev.txt
81
+ pytest tests/
82
+ # ログを出力する場合
83
+ # pytest -s tests/
84
+ ```
85
+
86
+ ### Lint
87
+
88
+ ```sh
89
+ pip install -r requirements-dev.txt
90
+ ruff check .
91
+ # 自動修正する場合
92
+ ruff check . --fix
93
+ ```
wpctl-1.0.0/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # wpctl
2
+
3
+ WordPress APIを活用して、記事を投稿・管理するCLIツール。
4
+
5
+ ## 概要
6
+
7
+ MarkDown / テキスト / HTMLファイルを読み込み、WordPress REST APIを通じて記事の投稿・更新を行います。
8
+
9
+ ## 機能
10
+
11
+ | 機能 | コマンド | 説明 |
12
+ | -------- | ------------------- | ------------------------------------------ |
13
+ | 記事投稿 | `wpctl post create` | Markdownなどのファイルを記事として投稿する |
14
+ | 記事更新 | `wpctl post update` | 既存の記事をファイルの内容で更新する |
15
+
16
+ ## インストール
17
+
18
+ ```sh
19
+ pip install .
20
+ ```
21
+
22
+ ## 使い方
23
+
24
+ ### 記事を投稿する
25
+
26
+ ```sh
27
+ wpctl post create [-t "記事のタイトル"] <FilePath>
28
+ ```
29
+
30
+ | 引数 | オプション | 必須 | 説明 |
31
+ | ---- | ---------- | ---- | ---- |
32
+ | `FilePath` | | true | 投稿するファイルのパス |
33
+ | `title` | `--title`, `-t` | | 記事タイトル(デフォルト: `タイトル未設定`)|
34
+
35
+ ### 記事を更新する
36
+
37
+ ```sh
38
+ wpctl post update --id <ID> [-t "記事のタイトル"] <FilePath>
39
+ ```
40
+
41
+ | 引数 | オプション | 必須 | 説明 |
42
+ | ---- | ---------- | ---- | ---- |
43
+ | `FilePath` | | true | 投稿するファイルのパス |
44
+ | `id` | `--id` | true | 更新対象の記事ID |
45
+ | `title` | `--title`, `-t` | | 記事タイトル(デフォルト: `タイトル未設定`)|
46
+
47
+ ## 環境変数
48
+
49
+ | 環境変数 | 必須 | 説明 |
50
+ | ----------------- | ---- | --------------------------- |
51
+ | `WP_USER` | true | WordPressのユーザー名 |
52
+ | `WP_APP_PASSWORD` | true | WordPressのアプリパスワード |
53
+
54
+ > `.env` ファイルは非対応です。あらかじめシェルの環境変数に設定してください。
55
+
56
+ ## 対応ファイル形式
57
+
58
+ - `.txt`
59
+ - `.md`
60
+ - `.html`
61
+
62
+ ## Tool
63
+
64
+ ### Test
65
+
66
+ ```sh
67
+ pip install -r requirements-dev.txt
68
+ pytest tests/
69
+ # ログを出力する場合
70
+ # pytest -s tests/
71
+ ```
72
+
73
+ ### Lint
74
+
75
+ ```sh
76
+ pip install -r requirements-dev.txt
77
+ ruff check .
78
+ # 自動修正する場合
79
+ ruff check . --fix
80
+ ```
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+ [project]
5
+ name = "wpctl"
6
+ version = "1.0.0"
7
+ description = "WordPress APIを活用して記事を投稿・管理するCLIツール"
8
+ authors = [
9
+ { name = "Ryoh.Ya", email = "dev.p.ry.yamafuji@gmail.com" }
10
+ ]
11
+ readme = "README.md"
12
+ requires-python = ">=3.12"
13
+ dependencies = [
14
+ "markdown>=3.7",
15
+ "nh3>=0.2.18",
16
+ "requests>=2.32",
17
+ ]
18
+ [project.scripts]
19
+ wpctl = "wpctl.main:main"
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
wpctl-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,104 @@
1
+ import os
2
+
3
+ import requests
4
+
5
+ from wpctl.utils.custom_logger import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ class WordPressAPIError(Exception):
11
+ """WordPress API 呼び出しエラー。"""
12
+
13
+
14
+ class ApiWordpress:
15
+ """WordPress REST API クライアント。
16
+
17
+ Notes:
18
+ WordPress REST API のエンドポイントは通常 /wp-json/wp/v2/
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ self._site_url = os.environ["WP_SITE_URL"].rstrip("/")
23
+ self._auth = (os.environ["WP_USER"], os.environ["WP_APP_PASSWORD"])
24
+
25
+ def get_me(self) -> dict:
26
+ """認証情報の疎通確認。
27
+
28
+ Returns:
29
+ 認証ユーザーの情報
30
+
31
+ Raises:
32
+ WordPressAPIError: API呼び出しに失敗した場合
33
+ """
34
+ url = f"{self._site_url}/wp-json/wp/v2/users/me"
35
+ try:
36
+ response = requests.get(url, auth=self._auth)
37
+ response.raise_for_status()
38
+ except requests.exceptions.HTTPError as e:
39
+ logger.error(f"認証に失敗しました: {e}")
40
+ raise WordPressAPIError(f"認証に失敗しました: {e}") from e
41
+ logger.info(
42
+ f"Authentication successful for user: {response.json().get('name')}"
43
+ )
44
+ return response.json()
45
+
46
+ def create_post(
47
+ self,
48
+ title: str,
49
+ content: str,
50
+ status: str = "publish",
51
+ ) -> dict:
52
+ """新しい記事を投稿する。
53
+
54
+ Args:
55
+ title: 記事のタイトル
56
+ content: 記事の内容(HTML)
57
+ status: 記事のステータス('publish' または 'draft')
58
+
59
+ Returns:
60
+ 作成された記事の情報
61
+
62
+ Raises:
63
+ WordPressAPIError: API呼び出しに失敗した場合
64
+ """
65
+ url = f"{self._site_url}/wp-json/wp/v2/posts"
66
+ payload = {"title": title, "content": content, "status": status}
67
+ try:
68
+ response = requests.post(url, json=payload, auth=self._auth)
69
+ response.raise_for_status()
70
+ except requests.exceptions.HTTPError as e:
71
+ logger.error(f"記事の投稿に失敗しました: {e}")
72
+ raise WordPressAPIError(f"記事の投稿に失敗しました: {e}") from e
73
+ logger.info(f"Post created successfully: {response.json().get('link')}")
74
+ return response.json()
75
+
76
+ def update_post(
77
+ self,
78
+ post_id: int,
79
+ title: str,
80
+ content: str,
81
+ ) -> dict:
82
+ """既存の記事を更新する。
83
+
84
+ Args:
85
+ post_id: 更新する記事のID
86
+ title: 更新後のタイトル
87
+ content: 更新後の内容(HTML)
88
+
89
+ Returns:
90
+ 更新された記事の情報
91
+
92
+ Raises:
93
+ WordPressAPIError: API呼び出しに失敗した場合
94
+ """
95
+ url = f"{self._site_url}/wp-json/wp/v2/posts/{post_id}"
96
+ payload = {"title": title, "content": content}
97
+ try:
98
+ response = requests.post(url, json=payload, auth=self._auth)
99
+ response.raise_for_status()
100
+ except requests.exceptions.HTTPError as e:
101
+ logger.error(f"記事の更新に失敗しました: id={post_id}, {e}")
102
+ raise WordPressAPIError(f"記事の更新に失敗しました: {e}") from e
103
+ logger.info(f"Post updated successfully: {response.json().get('link')}")
104
+ return response.json()
File without changes
@@ -0,0 +1,22 @@
1
+ from wpctl.api.api_wordpress import ApiWordpress
2
+ from wpctl.libs.file_reader import read_file
3
+ from wpctl.utils.custom_logger import get_logger
4
+
5
+ logger = get_logger(__name__)
6
+
7
+
8
+ def run(file_path: str, title: str) -> dict:
9
+ """記事を投稿する。
10
+
11
+ Args:
12
+ file_path: 投稿するファイルのパス(.txt / .html / .md)
13
+ title: 記事のタイトル
14
+
15
+ Returns:
16
+ WordPress API のレスポンスデータ
17
+ """
18
+ content = read_file(file_path)
19
+ api = ApiWordpress()
20
+ result = api.create_post(title=title, content=content)
21
+ logger.info(f"記事を投稿しました: id={result.get('id')}, link={result.get('link')}")
22
+ return result
@@ -0,0 +1,25 @@
1
+ from wpctl.api.api_wordpress import ApiWordpress
2
+ from wpctl.libs.file_reader import read_file
3
+ from wpctl.utils.custom_logger import get_logger
4
+
5
+ logger = get_logger(__name__)
6
+
7
+
8
+ def run(file_path: str, post_id: int, title: str) -> dict:
9
+ """記事を更新する。
10
+
11
+ Args:
12
+ file_path: 投稿するファイルのパス(.txt / .html / .md)
13
+ post_id: 更新する記事のID
14
+ title: 記事のタイトル
15
+
16
+ Returns:
17
+ WordPress API のレスポンスデータ
18
+ """
19
+ content = read_file(file_path)
20
+ api = ApiWordpress()
21
+ result = api.update_post(post_id=post_id, title=title, content=content)
22
+ logger.info(
23
+ f"記事を更新しました: id={result.get('id')}, link={result.get('link')}"
24
+ )
25
+ return result
File without changes
@@ -0,0 +1,48 @@
1
+ from pathlib import Path
2
+
3
+ from wpctl.libs.md_to_html import convert
4
+ from wpctl.utils.custom_logger import get_logger
5
+
6
+ logger = get_logger(__name__)
7
+
8
+ SUPPORTED_EXTENSIONS = {".txt", ".html", ".md"}
9
+
10
+
11
+ class FileReadError(Exception):
12
+ """ファイル読み込みエラー。"""
13
+
14
+
15
+ def read_file(file_path: str) -> str:
16
+ """ファイルを読み込んで文字列として返す。
17
+
18
+ Markdownファイルはサニタイズ済みHTMLに変換して返す。
19
+ .txt / .html はそのまま返す。
20
+
21
+ Args:
22
+ file_path: 読み込むファイルのパス
23
+
24
+ Returns:
25
+ ファイルの内容(Markdownの場合はHTML変換済み)
26
+
27
+ Raises:
28
+ FileReadError: ファイルが存在しない、または未対応の拡張子の場合
29
+ """
30
+ path = Path(file_path)
31
+
32
+ if not path.exists():
33
+ raise FileReadError(f"ファイルが見つかりません: {file_path}")
34
+
35
+ ext = path.suffix.lower()
36
+ if ext not in SUPPORTED_EXTENSIONS:
37
+ supported = ", ".join(sorted(SUPPORTED_EXTENSIONS))
38
+ raise FileReadError(
39
+ f"未対応のファイル形式です: {ext}(対応形式: {supported})"
40
+ )
41
+
42
+ text = path.read_text(encoding="utf-8")
43
+ logger.info(f"ファイルを読み込みました: {file_path}")
44
+
45
+ if ext == ".md":
46
+ return convert(text)
47
+
48
+ return text
@@ -0,0 +1,35 @@
1
+ import markdown
2
+ import nh3
3
+
4
+ _ALLOWED_TAGS = {
5
+ "p", "br", "strong", "b", "em", "i", "u", "s",
6
+ "h1", "h2", "h3", "h4", "h5", "h6",
7
+ "ul", "ol", "li", "dl", "dt", "dd",
8
+ "blockquote", "pre", "code",
9
+ "table", "thead", "tbody", "tr", "th", "td",
10
+ "a", "img", "hr",
11
+ }
12
+
13
+ _ALLOWED_ATTRIBUTES: dict[str, set[str]] = {
14
+ "a": {"href", "title", "target"},
15
+ "img": {"src", "alt", "width", "height"},
16
+ "td": {"colspan", "rowspan"},
17
+ "th": {"colspan", "rowspan"},
18
+ "code": {"class"},
19
+ "pre": {"class"},
20
+ }
21
+
22
+
23
+ def convert(text: str) -> str:
24
+ """MarkdownをサニタイズされたHTMLに変換する。
25
+
26
+ script タグ・イベント属性・外部JavaScript埋め込みを除去する。
27
+
28
+ Args:
29
+ text: Markdown形式のテキスト
30
+
31
+ Returns:
32
+ サニタイズ済みのHTML文字列
33
+ """
34
+ html = markdown.markdown(text, extensions=["tables", "fenced_code"])
35
+ return nh3.clean(html, tags=_ALLOWED_TAGS, attributes=_ALLOWED_ATTRIBUTES)
@@ -0,0 +1,82 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ from wpctl.run import run
6
+ from wpctl.utils.custom_logger import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+ _REQUIRED_ENV_VARS = ["WP_SITE_URL", "WP_USER", "WP_APP_PASSWORD"]
11
+
12
+
13
+ def _validate_env() -> None:
14
+ """必須環境変数が設定されているか検証する。
15
+
16
+ Raises:
17
+ SystemExit: 未設定の環境変数が存在する場合
18
+ """
19
+ missing = [var for var in _REQUIRED_ENV_VARS if not os.environ.get(var)]
20
+ if missing:
21
+ for var in missing:
22
+ print(f"Error: 環境変数 {var} が設定されていません。", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+
26
+ def _build_parser() -> argparse.ArgumentParser:
27
+ """CLIパーサーを構築する。
28
+
29
+ Returns:
30
+ ArgumentParser: 構築済みのパーサー
31
+ """
32
+ parser = argparse.ArgumentParser(
33
+ prog="wpctl",
34
+ description="WordPress APIを活用して記事を投稿・管理するCLIツール",
35
+ )
36
+ subparsers = parser.add_subparsers(dest="command")
37
+
38
+ post_parser = subparsers.add_parser("post", help="記事を管理する")
39
+ post_subparsers = post_parser.add_subparsers(dest="subcommand")
40
+
41
+ create_parser = post_subparsers.add_parser("create", help="記事を投稿する")
42
+ create_parser.add_argument(
43
+ "file_path", metavar="FilePath", help="投稿するファイルのパス"
44
+ )
45
+ create_parser.add_argument(
46
+ "--title", "-t",
47
+ default="タイトル未設定",
48
+ help="記事のタイトル(デフォルト: タイトル未設定)",
49
+ )
50
+
51
+ update_parser = post_subparsers.add_parser("update", help="記事を更新する")
52
+ update_parser.add_argument(
53
+ "file_path", metavar="FilePath", help="投稿するファイルのパス"
54
+ )
55
+ update_parser.add_argument(
56
+ "--id", dest="post_id", type=int, required=True, help="更新する記事のID"
57
+ )
58
+ update_parser.add_argument(
59
+ "--title", "-t",
60
+ default="タイトル未設定",
61
+ help="記事のタイトル(デフォルト: タイトル未設定)",
62
+ )
63
+
64
+ return parser
65
+
66
+
67
+ def main():
68
+ """エントリーポイント。CLI引数を解析してrunに渡す。"""
69
+ parser = _build_parser()
70
+ args = parser.parse_args()
71
+
72
+ if args.command is None:
73
+ parser.print_help()
74
+ return
75
+
76
+ _validate_env()
77
+ logger.info(f"wpctl {args.command} {args.subcommand}")
78
+ run(args)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,67 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from wpctl.api.api_wordpress import WordPressAPIError
5
+ from wpctl.libs.file_reader import FileReadError
6
+ from wpctl.utils.custom_logger import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ def run(args: argparse.Namespace) -> None:
12
+ """ツールのロジック開始点。コマンドに応じた処理を呼び出す。
13
+
14
+ Args:
15
+ args: argparseで解析された引数
16
+ """
17
+ if args.command == "post":
18
+ _run_post(args)
19
+ else:
20
+ raise ValueError(f"Unknown command: {args.command}")
21
+
22
+
23
+ def _run_post(args: argparse.Namespace) -> None:
24
+ """post コマンドのサブコマンドを振り分ける。
25
+
26
+ Args:
27
+ args: argparseで解析された引数
28
+ """
29
+ if args.subcommand == "create":
30
+ _run_post_create(args)
31
+ elif args.subcommand == "update":
32
+ _run_post_update(args)
33
+ else:
34
+ raise ValueError(f"Unknown subcommand: {args.subcommand}")
35
+
36
+
37
+ def _run_post_create(args: argparse.Namespace) -> None:
38
+ """記事を投稿する。
39
+
40
+ Args:
41
+ args: argparseで解析された引数
42
+ file_path: 投稿するファイルのパス
43
+ title: 記事のタイトル
44
+ """
45
+ from wpctl.commands.create import run as create_run
46
+ try:
47
+ create_run(file_path=args.file_path, title=args.title)
48
+ except (FileReadError, WordPressAPIError) as e:
49
+ logger.error(str(e))
50
+ sys.exit(1)
51
+
52
+
53
+ def _run_post_update(args: argparse.Namespace) -> None:
54
+ """記事を更新する。
55
+
56
+ Args:
57
+ args: argparseで解析された引数
58
+ file_path: 投稿するファイルのパス
59
+ post_id: 更新する記事のID
60
+ title: 記事のタイトル
61
+ """
62
+ from wpctl.commands.update import run as update_run
63
+ try:
64
+ update_run(file_path=args.file_path, post_id=args.post_id, title=args.title)
65
+ except (FileReadError, WordPressAPIError) as e:
66
+ logger.error(str(e))
67
+ sys.exit(1)
File without changes