blog-cli 0.1.0__tar.gz → 0.2.1__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.
- {blog_cli-0.1.0 → blog_cli-0.2.1}/PKG-INFO +16 -13
- {blog_cli-0.1.0 → blog_cli-0.2.1}/README.md +12 -9
- {blog_cli-0.1.0 → blog_cli-0.2.1}/pyproject.toml +4 -4
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/blog_cli.egg-info/PKG-INFO +16 -13
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/personal_cli/cli.py +80 -16
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/personal_cli/client.py +25 -6
- {blog_cli-0.1.0 → blog_cli-0.2.1}/tests/test_cli.py +1 -1
- {blog_cli-0.1.0 → blog_cli-0.2.1}/setup.cfg +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/blog_cli.egg-info/SOURCES.txt +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/blog_cli.egg-info/dependency_links.txt +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/blog_cli.egg-info/entry_points.txt +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/blog_cli.egg-info/requires.txt +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/blog_cli.egg-info/top_level.txt +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/personal_cli/__init__.py +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/personal_cli/__main__.py +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/src/personal_cli/formatting.py +0 -0
- {blog_cli-0.1.0 → blog_cli-0.2.1}/tests/test_client.py +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blog-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Agent-friendly CLI for managing a blog. Create drafts, upload media, generate time-limited preview links.
|
|
5
5
|
Author: tanaka-mambinge
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Repository, https://github.com/tanaka-mambinge/personal
|
|
8
|
-
Project-URL: Issues, https://github.com/tanaka-mambinge/personal/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/tanaka-mambinge/personal-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/tanaka-mambinge/personal-cli/issues
|
|
9
9
|
Keywords: blog,cli,agent,markdown,writing
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
14
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
Requires-Dist: httpx>=0.28
|
|
17
17
|
Requires-Dist: typer>=0.16
|
|
@@ -35,32 +35,35 @@ Designed for use by both humans and AI agents.
|
|
|
35
35
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
|
+
```bash
|
|
39
|
+
pip install blog-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or via pipx/uv:
|
|
43
|
+
|
|
38
44
|
```bash
|
|
39
45
|
pipx install blog-cli
|
|
40
|
-
# or
|
|
41
46
|
uv tool install blog-cli
|
|
42
47
|
```
|
|
43
48
|
|
|
44
49
|
For development:
|
|
45
50
|
|
|
46
51
|
```bash
|
|
47
|
-
git clone https://github.com/tanaka-mambinge/personal
|
|
48
|
-
cd personal
|
|
52
|
+
git clone https://github.com/tanaka-mambinge/personal-cli
|
|
53
|
+
cd personal-cli
|
|
49
54
|
uv sync --extra dev
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
## Configure
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
All three are required:
|
|
55
60
|
|
|
56
61
|
```bash
|
|
57
|
-
export PERSONAL_SERVER_URL="
|
|
58
|
-
export PERSONAL_API_KEY="your-api-key"
|
|
59
|
-
export PERSONAL_SITE_URL="
|
|
62
|
+
export PERSONAL_SERVER_URL="<your-server-url>"
|
|
63
|
+
export PERSONAL_API_KEY="<your-api-key>"
|
|
64
|
+
export PERSONAL_SITE_URL="<your-site-url>"
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
Empty `PERSONAL_API_KEY` disables auth if your server has no key configured.
|
|
63
|
-
|
|
64
67
|
## Usage
|
|
65
68
|
|
|
66
69
|
### Articles
|
|
@@ -6,32 +6,35 @@ Designed for use by both humans and AI agents.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
```bash
|
|
10
|
+
pip install blog-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or via pipx/uv:
|
|
14
|
+
|
|
9
15
|
```bash
|
|
10
16
|
pipx install blog-cli
|
|
11
|
-
# or
|
|
12
17
|
uv tool install blog-cli
|
|
13
18
|
```
|
|
14
19
|
|
|
15
20
|
For development:
|
|
16
21
|
|
|
17
22
|
```bash
|
|
18
|
-
git clone https://github.com/tanaka-mambinge/personal
|
|
19
|
-
cd personal
|
|
23
|
+
git clone https://github.com/tanaka-mambinge/personal-cli
|
|
24
|
+
cd personal-cli
|
|
20
25
|
uv sync --extra dev
|
|
21
26
|
```
|
|
22
27
|
|
|
23
28
|
## Configure
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
All three are required:
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
|
-
export PERSONAL_SERVER_URL="
|
|
29
|
-
export PERSONAL_API_KEY="your-api-key"
|
|
30
|
-
export PERSONAL_SITE_URL="
|
|
33
|
+
export PERSONAL_SERVER_URL="<your-server-url>"
|
|
34
|
+
export PERSONAL_API_KEY="<your-api-key>"
|
|
35
|
+
export PERSONAL_SITE_URL="<your-site-url>"
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
Empty `PERSONAL_API_KEY` disables auth if your server has no key configured.
|
|
34
|
-
|
|
35
38
|
## Usage
|
|
36
39
|
|
|
37
40
|
### Articles
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "blog-cli"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Agent-friendly CLI for managing a blog. Create drafts, upload media, generate time-limited preview links."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [{name = "tanaka-mambinge"}]
|
|
13
13
|
keywords = ["blog", "cli", "agent", "markdown", "writing"]
|
|
@@ -23,8 +23,8 @@ dependencies = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[project.urls]
|
|
26
|
-
Repository = "https://github.com/tanaka-mambinge/personal"
|
|
27
|
-
Issues = "https://github.com/tanaka-mambinge/personal/issues"
|
|
26
|
+
Repository = "https://github.com/tanaka-mambinge/personal-cli"
|
|
27
|
+
Issues = "https://github.com/tanaka-mambinge/personal-cli/issues"
|
|
28
28
|
|
|
29
29
|
[project.scripts]
|
|
30
30
|
blog-cli = "personal_cli.cli:main"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blog-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Agent-friendly CLI for managing a blog. Create drafts, upload media, generate time-limited preview links.
|
|
5
5
|
Author: tanaka-mambinge
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Repository, https://github.com/tanaka-mambinge/personal
|
|
8
|
-
Project-URL: Issues, https://github.com/tanaka-mambinge/personal/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/tanaka-mambinge/personal-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/tanaka-mambinge/personal-cli/issues
|
|
9
9
|
Keywords: blog,cli,agent,markdown,writing
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
14
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
Requires-Dist: httpx>=0.28
|
|
17
17
|
Requires-Dist: typer>=0.16
|
|
@@ -35,32 +35,35 @@ Designed for use by both humans and AI agents.
|
|
|
35
35
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
|
+
```bash
|
|
39
|
+
pip install blog-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or via pipx/uv:
|
|
43
|
+
|
|
38
44
|
```bash
|
|
39
45
|
pipx install blog-cli
|
|
40
|
-
# or
|
|
41
46
|
uv tool install blog-cli
|
|
42
47
|
```
|
|
43
48
|
|
|
44
49
|
For development:
|
|
45
50
|
|
|
46
51
|
```bash
|
|
47
|
-
git clone https://github.com/tanaka-mambinge/personal
|
|
48
|
-
cd personal
|
|
52
|
+
git clone https://github.com/tanaka-mambinge/personal-cli
|
|
53
|
+
cd personal-cli
|
|
49
54
|
uv sync --extra dev
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
## Configure
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
All three are required:
|
|
55
60
|
|
|
56
61
|
```bash
|
|
57
|
-
export PERSONAL_SERVER_URL="
|
|
58
|
-
export PERSONAL_API_KEY="your-api-key"
|
|
59
|
-
export PERSONAL_SITE_URL="
|
|
62
|
+
export PERSONAL_SERVER_URL="<your-server-url>"
|
|
63
|
+
export PERSONAL_API_KEY="<your-api-key>"
|
|
64
|
+
export PERSONAL_SITE_URL="<your-site-url>"
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
Empty `PERSONAL_API_KEY` disables auth if your server has no key configured.
|
|
63
|
-
|
|
64
67
|
## Usage
|
|
65
68
|
|
|
66
69
|
### Articles
|
|
@@ -16,9 +16,9 @@ app.add_typer(article_app, name="article")
|
|
|
16
16
|
app.add_typer(media_app, name="media")
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def build_client(server_url: str | None = None) -> ArticleApiClient:
|
|
20
|
-
url, api_key = get_default_config()
|
|
21
|
-
return ArticleApiClient(server_url or url, api_key=api_key)
|
|
19
|
+
def build_client(server_url: str | None = None, insecure: bool = False) -> ArticleApiClient:
|
|
20
|
+
url, api_key, _ = get_default_config()
|
|
21
|
+
return ArticleApiClient(server_url or url, api_key=api_key, verify=not insecure)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def run(coro):
|
|
@@ -30,10 +30,11 @@ def article_list(
|
|
|
30
30
|
type_filter: str = typer.Option("all", "--type", help="all, blog, or project."),
|
|
31
31
|
status: str | None = typer.Option(None, "--status", help="Filter by article status."),
|
|
32
32
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
33
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
33
34
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
34
35
|
) -> None:
|
|
35
36
|
try:
|
|
36
|
-
client = build_client(server_url)
|
|
37
|
+
client = build_client(server_url, insecure=insecure)
|
|
37
38
|
articles = run(client.list_articles(status=status, type_filter=type_filter))
|
|
38
39
|
emit_result(articles, json_output=json_output)
|
|
39
40
|
except CLIError as exc:
|
|
@@ -45,10 +46,11 @@ def article_list(
|
|
|
45
46
|
def article_show(
|
|
46
47
|
slug: str,
|
|
47
48
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
49
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
48
50
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
49
51
|
) -> None:
|
|
50
52
|
try:
|
|
51
|
-
client = build_client(server_url)
|
|
53
|
+
client = build_client(server_url, insecure=insecure)
|
|
52
54
|
article = run(client.get_article(slug))
|
|
53
55
|
emit_result(article, json_output=json_output)
|
|
54
56
|
except CLIError as exc:
|
|
@@ -67,11 +69,12 @@ def article_create(
|
|
|
67
69
|
markdown: str | None = typer.Option(None, "--markdown", help="Inline markdown body."),
|
|
68
70
|
markdown_file: Path | None = typer.Option(None, "--markdown-file", exists=True, readable=True, dir_okay=False),
|
|
69
71
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
72
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
70
73
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
71
74
|
) -> None:
|
|
72
75
|
try:
|
|
73
76
|
body = read_markdown_from_source(markdown=markdown, markdown_file=markdown_file)
|
|
74
|
-
client = build_client(server_url)
|
|
77
|
+
client = build_client(server_url, insecure=insecure)
|
|
75
78
|
payload = {
|
|
76
79
|
"title": title,
|
|
77
80
|
"description": description,
|
|
@@ -99,10 +102,11 @@ def article_update(
|
|
|
99
102
|
markdown: str | None = typer.Option(None, "--markdown", help="Inline markdown body."),
|
|
100
103
|
markdown_file: Path | None = typer.Option(None, "--markdown-file", exists=True, readable=True, dir_okay=False),
|
|
101
104
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
105
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
102
106
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
103
107
|
) -> None:
|
|
104
108
|
try:
|
|
105
|
-
client = build_client(server_url)
|
|
109
|
+
client = build_client(server_url, insecure=insecure)
|
|
106
110
|
payload: dict[str, object] = {}
|
|
107
111
|
if title is not None:
|
|
108
112
|
payload["title"] = title
|
|
@@ -128,10 +132,11 @@ def article_publish(
|
|
|
128
132
|
slug: str,
|
|
129
133
|
published_by: str | None = typer.Option(None, "--published-by", help="Who published the article."),
|
|
130
134
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
135
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
131
136
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
132
137
|
) -> None:
|
|
133
138
|
try:
|
|
134
|
-
client = build_client(server_url)
|
|
139
|
+
client = build_client(server_url, insecure=insecure)
|
|
135
140
|
article = run(client.publish_article(slug, {"published_by": published_by} if published_by else None))
|
|
136
141
|
emit_result(article, json_output=json_output)
|
|
137
142
|
except CLIError as exc:
|
|
@@ -143,10 +148,11 @@ def article_publish(
|
|
|
143
148
|
def article_delete(
|
|
144
149
|
slug: str,
|
|
145
150
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
151
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
146
152
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
147
153
|
) -> None:
|
|
148
154
|
try:
|
|
149
|
-
client = build_client(server_url)
|
|
155
|
+
client = build_client(server_url, insecure=insecure)
|
|
150
156
|
result = run(client.delete_article(slug))
|
|
151
157
|
emit_result(result, json_output=json_output)
|
|
152
158
|
except CLIError as exc:
|
|
@@ -158,10 +164,11 @@ def article_delete(
|
|
|
158
164
|
def article_unarchive(
|
|
159
165
|
slug: str,
|
|
160
166
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
167
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
161
168
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
162
169
|
) -> None:
|
|
163
170
|
try:
|
|
164
|
-
client = build_client(server_url)
|
|
171
|
+
client = build_client(server_url, insecure=insecure)
|
|
165
172
|
article = run(client.unarchive_article(slug))
|
|
166
173
|
emit_result(article, json_output=json_output)
|
|
167
174
|
except CLIError as exc:
|
|
@@ -175,12 +182,13 @@ def article_preview(
|
|
|
175
182
|
ttl_hours: int = typer.Option(24, "--ttl-hours", help="Hours until the preview link expires."),
|
|
176
183
|
site_url: str | None = typer.Option(None, "--site-url", help="Base URL of the personal site."),
|
|
177
184
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
185
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
178
186
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
179
187
|
) -> None:
|
|
180
188
|
try:
|
|
181
|
-
|
|
182
|
-
resolved_site_url = site_url or
|
|
183
|
-
client = build_client(server_url)
|
|
189
|
+
_, _, default_site_url = get_default_config()
|
|
190
|
+
resolved_site_url = site_url or default_site_url
|
|
191
|
+
client = build_client(server_url, insecure=insecure)
|
|
184
192
|
result = run(client.generate_preview(slug, ttl_hours=ttl_hours, base_url=resolved_site_url))
|
|
185
193
|
emit_result(result, json_output=json_output)
|
|
186
194
|
except CLIError as exc:
|
|
@@ -188,15 +196,69 @@ def article_preview(
|
|
|
188
196
|
raise typer.Exit(code=1) from exc
|
|
189
197
|
|
|
190
198
|
|
|
199
|
+
@article_app.command("tag-list")
|
|
200
|
+
@article_app.command("tags")
|
|
201
|
+
def tag_list(
|
|
202
|
+
slug: str,
|
|
203
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
204
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
205
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
206
|
+
) -> None:
|
|
207
|
+
try:
|
|
208
|
+
client = build_client(server_url, insecure=insecure)
|
|
209
|
+
result = run(client.list_tags(slug))
|
|
210
|
+
emit_result(result, json_output=json_output)
|
|
211
|
+
except CLIError as exc:
|
|
212
|
+
typer.echo(str(exc), err=True)
|
|
213
|
+
raise typer.Exit(code=1) from exc
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@article_app.command("tag-add")
|
|
217
|
+
def tag_add(
|
|
218
|
+
slug: str,
|
|
219
|
+
tag: list[str] = typer.Option(..., "--tag", help="Tag to attach. Repeat for multiple."),
|
|
220
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
221
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
222
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
223
|
+
) -> None:
|
|
224
|
+
try:
|
|
225
|
+
client = build_client(server_url, insecure=insecure)
|
|
226
|
+
result = run(client.attach_tags(slug, tag))
|
|
227
|
+
emit_result(result, json_output=json_output)
|
|
228
|
+
except CLIError as exc:
|
|
229
|
+
typer.echo(str(exc), err=True)
|
|
230
|
+
raise typer.Exit(code=1) from exc
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@article_app.command("tag-remove")
|
|
234
|
+
def tag_remove(
|
|
235
|
+
slug: str,
|
|
236
|
+
tag: list[str] = typer.Option(..., "--tag", help="Tag to remove. Repeat for multiple."),
|
|
237
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
238
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
239
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
240
|
+
) -> None:
|
|
241
|
+
try:
|
|
242
|
+
client = build_client(server_url, insecure=insecure)
|
|
243
|
+
for t in tag:
|
|
244
|
+
run(client.remove_tag(slug, t))
|
|
245
|
+
result = run(client.list_tags(slug))
|
|
246
|
+
emit_result(result, json_output=json_output)
|
|
247
|
+
except CLIError as exc:
|
|
248
|
+
typer.echo(str(exc), err=True)
|
|
249
|
+
raise typer.Exit(code=1) from exc
|
|
250
|
+
|
|
251
|
+
|
|
191
252
|
@media_app.command("upload")
|
|
192
253
|
def media_upload(
|
|
193
254
|
name: str = typer.Option(..., "--name", help="Unique name for the media (e.g. hero-image)."),
|
|
194
255
|
path: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, readable=True),
|
|
195
256
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
257
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
196
258
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
197
259
|
) -> None:
|
|
198
260
|
try:
|
|
199
|
-
client = build_client(server_url)
|
|
261
|
+
client = build_client(server_url, insecure=insecure)
|
|
200
262
|
result = run(client.upload_media(name, path))
|
|
201
263
|
emit_result(result, json_output=json_output)
|
|
202
264
|
except CLIError as exc:
|
|
@@ -209,10 +271,11 @@ def media_update(
|
|
|
209
271
|
name: str = typer.Option(..., "--name", help="Name of the media to replace."),
|
|
210
272
|
path: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, readable=True),
|
|
211
273
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
274
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
212
275
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
213
276
|
) -> None:
|
|
214
277
|
try:
|
|
215
|
-
client = build_client(server_url)
|
|
278
|
+
client = build_client(server_url, insecure=insecure)
|
|
216
279
|
result = run(client.update_media(name, path))
|
|
217
280
|
emit_result(result, json_output=json_output)
|
|
218
281
|
except CLIError as exc:
|
|
@@ -224,10 +287,11 @@ def media_update(
|
|
|
224
287
|
def media_delete(
|
|
225
288
|
name: str = typer.Option(..., "--name", help="Name of the media to delete."),
|
|
226
289
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
290
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
227
291
|
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
228
292
|
) -> None:
|
|
229
293
|
try:
|
|
230
|
-
client = build_client(server_url)
|
|
294
|
+
client = build_client(server_url, insecure=insecure)
|
|
231
295
|
result = run(client.delete_media(name))
|
|
232
296
|
emit_result(result, json_output=json_output)
|
|
233
297
|
except CLIError as exc:
|
|
@@ -20,11 +20,13 @@ class ArticleApiClient:
|
|
|
20
20
|
api_key: str | None = None,
|
|
21
21
|
transport: httpx.AsyncBaseTransport | None = None,
|
|
22
22
|
timeout: float = 30.0,
|
|
23
|
+
verify: bool = True,
|
|
23
24
|
) -> None:
|
|
24
25
|
self.base_url = base_url.rstrip("/")
|
|
25
26
|
self.api_key = api_key
|
|
26
27
|
self.transport = transport
|
|
27
28
|
self.timeout = timeout
|
|
29
|
+
self.verify = verify
|
|
28
30
|
|
|
29
31
|
def _headers(self) -> dict[str, str]:
|
|
30
32
|
if not self.api_key:
|
|
@@ -45,6 +47,7 @@ class ArticleApiClient:
|
|
|
45
47
|
transport=self.transport,
|
|
46
48
|
timeout=self.timeout,
|
|
47
49
|
headers=self._headers(),
|
|
50
|
+
verify=self.verify,
|
|
48
51
|
) as client:
|
|
49
52
|
response = await client.request(method, path, json=json, params=params, files=files)
|
|
50
53
|
if response.status_code >= 400:
|
|
@@ -108,7 +111,7 @@ class ArticleApiClient:
|
|
|
108
111
|
result = await self._request_json("DELETE", f"/api/v1/media/{name}")
|
|
109
112
|
return dict(result or {})
|
|
110
113
|
|
|
111
|
-
async def generate_preview(self, slug: str, *, ttl_hours: int = 24, base_url: str
|
|
114
|
+
async def generate_preview(self, slug: str, *, ttl_hours: int = 24, base_url: str) -> dict[str, Any]:
|
|
112
115
|
result = await self._request_json(
|
|
113
116
|
"POST",
|
|
114
117
|
f"/api/v1/articles/{slug}/preview",
|
|
@@ -116,14 +119,30 @@ class ArticleApiClient:
|
|
|
116
119
|
)
|
|
117
120
|
return dict(result)
|
|
118
121
|
|
|
122
|
+
async def list_tags(self, slug: str) -> dict[str, Any]:
|
|
123
|
+
result = await self._request_json("GET", f"/api/v1/articles/{slug}/tags")
|
|
124
|
+
return dict(result)
|
|
125
|
+
|
|
126
|
+
async def attach_tags(self, slug: str, tags: list[str]) -> dict[str, Any]:
|
|
127
|
+
result = await self._request_json("POST", f"/api/v1/articles/{slug}/tags", json={"tags": tags})
|
|
128
|
+
return dict(result)
|
|
129
|
+
|
|
130
|
+
async def remove_tag(self, slug: str, tag: str) -> dict[str, Any]:
|
|
131
|
+
result = await self._request_json("DELETE", f"/api/v1/articles/{slug}/tags", params={"tag": tag})
|
|
132
|
+
return dict(result)
|
|
133
|
+
|
|
119
134
|
|
|
120
|
-
def get_default_config() -> tuple[str, str | None]:
|
|
135
|
+
def get_default_config() -> tuple[str, str | None, str]:
|
|
121
136
|
import os
|
|
122
137
|
_load_dotenv()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
138
|
+
server_url = os.environ.get("PERSONAL_SERVER_URL")
|
|
139
|
+
if not server_url:
|
|
140
|
+
raise CLIError("PERSONAL_SERVER_URL is not set.")
|
|
141
|
+
api_key = os.environ.get("PERSONAL_API_KEY")
|
|
142
|
+
if not api_key:
|
|
143
|
+
raise CLIError("PERSONAL_API_KEY is not set.")
|
|
144
|
+
site_url = os.environ.get("PERSONAL_SITE_URL", "")
|
|
145
|
+
return (server_url, api_key, site_url)
|
|
127
146
|
|
|
128
147
|
|
|
129
148
|
def _load_dotenv() -> None:
|
|
@@ -15,7 +15,7 @@ def runner() -> CliRunner:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _build_client_mock(live_server: str):
|
|
18
|
-
return lambda server_url=None: ArticleApiClient(live_server)
|
|
18
|
+
return lambda server_url=None, insecure=False: ArticleApiClient(live_server)
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_article_cli_smoke(monkeypatch, runner: CliRunner, live_server: str) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|