confpub-cli 1.1.0__tar.gz → 1.2.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.
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/PKG-INFO +1 -1
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/__init__.py +1 -1
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/applier.py +9 -1
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/cli.py +2 -2
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/confluence.py +37 -7
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/publish.py +9 -2
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_confluence.py +66 -1
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/.github/workflows/publish.yml +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/.gitignore +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/CLAUDE.md +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/LICENSE +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/PRD.md +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/README.md +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/assets.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/config.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/converter.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/envelope.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/errors.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/guide.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/lockfile.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/manifest.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/output.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/planner.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/puller.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/py.typed +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/reverse_converter.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/validator.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/verifier.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub.lock +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/pyproject.toml +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/__init__.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/conftest.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_applier.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_assets.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_config.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_converter.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_envelope.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_errors.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_guide.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_integration.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_lockfile.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_manifest.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_output.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_planner.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_publish.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_puller.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_reverse_converter.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_validator.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_verifier.py +0 -0
- {confpub_cli-1.1.0 → confpub_cli-1.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confpub-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Agent-first CLI to publish Markdown to Confluence
|
|
5
5
|
Project-URL: Homepage, https://github.com/ThomasRohde/confpub-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/ThomasRohde/confpub-cli.git
|
|
@@ -13,7 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from confpub.assets import AssetRef, discover_assets, rewrite_image_urls, upload_assets
|
|
15
15
|
from confpub.config import load_config
|
|
16
|
-
from confpub.confluence import ConfluenceClient
|
|
16
|
+
from confpub.confluence import ConfluenceClient, build_page_url
|
|
17
17
|
from confpub.converter import convert_markdown, fingerprint_content
|
|
18
18
|
from confpub.errors import ERR_CONFLICT_FINGERPRINT, ERR_IO_FILE_NOT_FOUND, ConfpubError
|
|
19
19
|
from confpub.lockfile import Lockfile, load_lockfile, save_lockfile, update_lockfile
|
|
@@ -113,6 +113,10 @@ def apply_plan(
|
|
|
113
113
|
new_version = new_version.get("number", 1)
|
|
114
114
|
change["after"]["page_id"] = new_id
|
|
115
115
|
change["after"]["version"] = new_version
|
|
116
|
+
change["after"]["webui"] = build_page_url(
|
|
117
|
+
config.base_url or "", config.is_cloud,
|
|
118
|
+
plan.space, new_id, page.title,
|
|
119
|
+
)
|
|
116
120
|
|
|
117
121
|
# Upload attachments
|
|
118
122
|
if assets:
|
|
@@ -167,6 +171,10 @@ def apply_plan(
|
|
|
167
171
|
if isinstance(new_version, dict):
|
|
168
172
|
new_version = new_version.get("number", (before_version or 0) + 1)
|
|
169
173
|
change["after"]["version"] = new_version
|
|
174
|
+
change["after"]["webui"] = build_page_url(
|
|
175
|
+
config.base_url or "", config.is_cloud,
|
|
176
|
+
plan.space, page.confluence_page_id, page.title,
|
|
177
|
+
)
|
|
170
178
|
|
|
171
179
|
# Upload attachments
|
|
172
180
|
if assets:
|
|
@@ -189,7 +189,7 @@ def page_list(
|
|
|
189
189
|
from confpub.confluence import build_client, _slim_page
|
|
190
190
|
client = build_client()
|
|
191
191
|
pages = client.list_pages(space, start=start, limit=limit)
|
|
192
|
-
ctx.result = {"pages": [_slim_page(p, base_url=client._config.base_url.rstrip("/")) for p in pages]}
|
|
192
|
+
ctx.result = {"pages": [_slim_page(p, base_url=client._config.base_url.rstrip("/"), is_cloud=client._config.is_cloud) for p in pages]}
|
|
193
193
|
|
|
194
194
|
|
|
195
195
|
@page_app.command("inspect")
|
|
@@ -217,7 +217,7 @@ def page_inspect(
|
|
|
217
217
|
if raw:
|
|
218
218
|
ctx.result = page
|
|
219
219
|
else:
|
|
220
|
-
result = _slim_page(page, base_url=client._config.base_url.rstrip("/"))
|
|
220
|
+
result = _slim_page(page, base_url=client._config.base_url.rstrip("/"), is_cloud=client._config.is_cloud)
|
|
221
221
|
if format == "markdown" and "body_storage" in result:
|
|
222
222
|
from confpub.reverse_converter import convert_storage_to_markdown
|
|
223
223
|
conversion = convert_storage_to_markdown(result["body_storage"])
|
|
@@ -267,7 +267,8 @@ class ConfluenceClient:
|
|
|
267
267
|
raw = result
|
|
268
268
|
else:
|
|
269
269
|
raw = []
|
|
270
|
-
|
|
270
|
+
base_url = self._config.base_url.rstrip("/") if self._config.base_url else ""
|
|
271
|
+
return [_slim_space(s, base_url=base_url, is_cloud=self._config.is_cloud) for s in raw]
|
|
271
272
|
except Exception as exc:
|
|
272
273
|
self._handle_error(exc, "list_spaces")
|
|
273
274
|
return []
|
|
@@ -407,7 +408,7 @@ class ConfluenceClient:
|
|
|
407
408
|
api_limit = raw.get("limit", limit) if isinstance(raw, dict) else limit
|
|
408
409
|
|
|
409
410
|
results = [
|
|
410
|
-
_slim_search_result(r, base_url=base_url, excerpt_length=excerpt_length)
|
|
411
|
+
_slim_search_result(r, base_url=base_url, is_cloud=self._config.is_cloud, excerpt_length=excerpt_length)
|
|
411
412
|
for r in results_raw
|
|
412
413
|
]
|
|
413
414
|
return {
|
|
@@ -433,7 +434,30 @@ class ConfluenceClient:
|
|
|
433
434
|
return None
|
|
434
435
|
|
|
435
436
|
|
|
436
|
-
def
|
|
437
|
+
def _webui_base(base_url: str, is_cloud: bool) -> str:
|
|
438
|
+
"""Return the correct base URL for webui links.
|
|
439
|
+
|
|
440
|
+
Cloud instances need ``/wiki`` in the path; DC/Server do not.
|
|
441
|
+
Handles base_url configured with or without the ``/wiki`` suffix.
|
|
442
|
+
"""
|
|
443
|
+
base = base_url.rstrip("/")
|
|
444
|
+
if is_cloud and not base.endswith("/wiki"):
|
|
445
|
+
base += "/wiki"
|
|
446
|
+
return base
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def build_page_url(
|
|
450
|
+
base_url: str, is_cloud: bool, space: str, page_id: str, title: str = "",
|
|
451
|
+
) -> str:
|
|
452
|
+
"""Construct a full webui URL for a Confluence page."""
|
|
453
|
+
base = _webui_base(base_url, is_cloud)
|
|
454
|
+
if title:
|
|
455
|
+
encoded_title = title.replace(" ", "+")
|
|
456
|
+
return f"{base}/spaces/{space}/pages/{page_id}/{encoded_title}"
|
|
457
|
+
return f"{base}/spaces/{space}/pages/{page_id}"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _slim_page(page: dict[str, Any], *, base_url: str = "", is_cloud: bool = False) -> dict[str, Any]:
|
|
437
461
|
"""Extract agent-relevant fields from a raw Confluence page object."""
|
|
438
462
|
result: dict[str, Any] = {
|
|
439
463
|
"id": page.get("id"),
|
|
@@ -456,12 +480,12 @@ def _slim_page(page: dict[str, Any], *, base_url: str = "") -> dict[str, Any]:
|
|
|
456
480
|
result["parent_title"] = parent.get("title")
|
|
457
481
|
links = page.get("_links", {})
|
|
458
482
|
if "webui" in links:
|
|
459
|
-
base = base_url
|
|
483
|
+
base = _webui_base(base_url, is_cloud) if base_url else links.get("base", "")
|
|
460
484
|
result["webui"] = base + links["webui"]
|
|
461
485
|
return result
|
|
462
486
|
|
|
463
487
|
|
|
464
|
-
def _slim_space(space: dict[str, Any]) -> dict[str, Any]:
|
|
488
|
+
def _slim_space(space: dict[str, Any], *, base_url: str = "", is_cloud: bool = False) -> dict[str, Any]:
|
|
465
489
|
"""Extract agent-relevant fields from a raw Confluence space object."""
|
|
466
490
|
result: dict[str, Any] = {
|
|
467
491
|
"id": space.get("id"),
|
|
@@ -478,7 +502,12 @@ def _slim_space(space: dict[str, Any]) -> dict[str, Any]:
|
|
|
478
502
|
result["description"] = value
|
|
479
503
|
links = space.get("_links", {})
|
|
480
504
|
if "webui" in links:
|
|
481
|
-
|
|
505
|
+
webui_path = links.get("webui", "")
|
|
506
|
+
if base_url and webui_path:
|
|
507
|
+
base = _webui_base(base_url, is_cloud)
|
|
508
|
+
result["webui"] = base + webui_path
|
|
509
|
+
else:
|
|
510
|
+
result["webui"] = webui_path
|
|
482
511
|
return result
|
|
483
512
|
|
|
484
513
|
|
|
@@ -511,6 +540,7 @@ def _slim_search_result(
|
|
|
511
540
|
item: dict[str, Any],
|
|
512
541
|
*,
|
|
513
542
|
base_url: str = "",
|
|
543
|
+
is_cloud: bool = False,
|
|
514
544
|
excerpt_length: int = 0,
|
|
515
545
|
) -> dict[str, Any]:
|
|
516
546
|
"""Extract agent-relevant fields from a raw Confluence search result.
|
|
@@ -536,7 +566,7 @@ def _slim_search_result(
|
|
|
536
566
|
|
|
537
567
|
webui = content.get("_links", {}).get("webui", "")
|
|
538
568
|
if webui:
|
|
539
|
-
_set("url", base_url + webui)
|
|
569
|
+
_set("url", _webui_base(base_url, is_cloud) + webui if base_url else webui)
|
|
540
570
|
|
|
541
571
|
_set("space_key", content.get("space", {}).get("key"))
|
|
542
572
|
|
|
@@ -12,7 +12,7 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
from confpub.assets import discover_assets, rewrite_image_urls, upload_assets
|
|
14
14
|
from confpub.config import load_config
|
|
15
|
-
from confpub.confluence import ConfluenceClient
|
|
15
|
+
from confpub.confluence import ConfluenceClient, build_page_url
|
|
16
16
|
from confpub.converter import convert_markdown, fingerprint_content
|
|
17
17
|
from confpub.errors import (
|
|
18
18
|
ERR_IO_FILE_NOT_FOUND,
|
|
@@ -202,7 +202,14 @@ def publish_page(
|
|
|
202
202
|
"title": page_title,
|
|
203
203
|
"confluence_page_id": page_id,
|
|
204
204
|
"before": {"version": current_version} if current_version else None,
|
|
205
|
-
"after": {
|
|
205
|
+
"after": {
|
|
206
|
+
"version": new_version,
|
|
207
|
+
"page_id": page_id,
|
|
208
|
+
"webui": build_page_url(
|
|
209
|
+
config.base_url or "", config.is_cloud,
|
|
210
|
+
space, page_id, page_title,
|
|
211
|
+
),
|
|
212
|
+
},
|
|
206
213
|
}
|
|
207
214
|
if backup_file_path:
|
|
208
215
|
change["backup_path"] = backup_file_path
|
|
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from confpub.config import ResolvedConfig
|
|
8
|
-
from confpub.confluence import ConfluenceClient, _slim_page, _slim_search_result
|
|
8
|
+
from confpub.confluence import ConfluenceClient, _slim_page, _slim_search_result, _webui_base, build_page_url
|
|
9
9
|
from confpub.errors import (
|
|
10
10
|
ERR_AUTH_FORBIDDEN,
|
|
11
11
|
ERR_AUTH_REQUIRED,
|
|
@@ -313,6 +313,36 @@ class TestSlimPage:
|
|
|
313
313
|
result = _slim_page(page)
|
|
314
314
|
assert result["webui"] == "https://wiki.example.com/spaces/DEV/pages/1"
|
|
315
315
|
|
|
316
|
+
def test_cloud_webui_adds_wiki_prefix(self):
|
|
317
|
+
"""Cloud URLs need /wiki between base and webui path."""
|
|
318
|
+
page = {
|
|
319
|
+
"id": "1",
|
|
320
|
+
"title": "T",
|
|
321
|
+
"_links": {"webui": "/spaces/SD/pages/98376/What+Works+Well"},
|
|
322
|
+
}
|
|
323
|
+
result = _slim_page(page, base_url="https://myorg.atlassian.net", is_cloud=True)
|
|
324
|
+
assert result["webui"] == "https://myorg.atlassian.net/wiki/spaces/SD/pages/98376/What+Works+Well"
|
|
325
|
+
|
|
326
|
+
def test_cloud_webui_no_double_wiki(self):
|
|
327
|
+
"""If base_url already ends with /wiki, don't duplicate it."""
|
|
328
|
+
page = {
|
|
329
|
+
"id": "1",
|
|
330
|
+
"title": "T",
|
|
331
|
+
"_links": {"webui": "/spaces/SD/pages/1"},
|
|
332
|
+
}
|
|
333
|
+
result = _slim_page(page, base_url="https://myorg.atlassian.net/wiki", is_cloud=True)
|
|
334
|
+
assert result["webui"] == "https://myorg.atlassian.net/wiki/spaces/SD/pages/1"
|
|
335
|
+
|
|
336
|
+
def test_dc_webui_no_wiki_prefix(self):
|
|
337
|
+
"""DC/Server URLs should NOT get /wiki prefix."""
|
|
338
|
+
page = {
|
|
339
|
+
"id": "1",
|
|
340
|
+
"title": "T",
|
|
341
|
+
"_links": {"webui": "/spaces/DEV/pages/1"},
|
|
342
|
+
}
|
|
343
|
+
result = _slim_page(page, base_url="https://confluence.corp.com", is_cloud=False)
|
|
344
|
+
assert result["webui"] == "https://confluence.corp.com/spaces/DEV/pages/1"
|
|
345
|
+
|
|
316
346
|
def test_omits_missing_optional_fields(self):
|
|
317
347
|
page = {"id": "1", "title": "T"}
|
|
318
348
|
result = _slim_page(page)
|
|
@@ -496,3 +526,38 @@ class TestSlimSearchResult:
|
|
|
496
526
|
result = _slim_search_result(item, excerpt_length=30)
|
|
497
527
|
assert result["excerpt"].endswith("…")
|
|
498
528
|
assert len(result["excerpt"]) <= 35
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class TestWebuiBase:
|
|
532
|
+
def test_cloud_without_wiki(self):
|
|
533
|
+
assert _webui_base("https://myorg.atlassian.net", True) == "https://myorg.atlassian.net/wiki"
|
|
534
|
+
|
|
535
|
+
def test_cloud_with_wiki(self):
|
|
536
|
+
assert _webui_base("https://myorg.atlassian.net/wiki", True) == "https://myorg.atlassian.net/wiki"
|
|
537
|
+
|
|
538
|
+
def test_cloud_trailing_slash(self):
|
|
539
|
+
assert _webui_base("https://myorg.atlassian.net/", True) == "https://myorg.atlassian.net/wiki"
|
|
540
|
+
|
|
541
|
+
def test_dc_plain(self):
|
|
542
|
+
assert _webui_base("https://confluence.corp.com", False) == "https://confluence.corp.com"
|
|
543
|
+
|
|
544
|
+
def test_dc_context_path(self):
|
|
545
|
+
assert _webui_base("https://corp.com/confluence", False) == "https://corp.com/confluence"
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class TestBuildPageUrl:
|
|
549
|
+
def test_cloud_with_title(self):
|
|
550
|
+
url = build_page_url("https://myorg.atlassian.net", True, "SD", "12345", "My Page")
|
|
551
|
+
assert url == "https://myorg.atlassian.net/wiki/spaces/SD/pages/12345/My+Page"
|
|
552
|
+
|
|
553
|
+
def test_cloud_without_title(self):
|
|
554
|
+
url = build_page_url("https://myorg.atlassian.net", True, "SD", "12345")
|
|
555
|
+
assert url == "https://myorg.atlassian.net/wiki/spaces/SD/pages/12345"
|
|
556
|
+
|
|
557
|
+
def test_dc_with_title(self):
|
|
558
|
+
url = build_page_url("https://confluence.corp.com", False, "DEV", "456", "Test Page")
|
|
559
|
+
assert url == "https://confluence.corp.com/spaces/DEV/pages/456/Test+Page"
|
|
560
|
+
|
|
561
|
+
def test_dc_without_title(self):
|
|
562
|
+
url = build_page_url("https://confluence.corp.com", False, "DEV", "456")
|
|
563
|
+
assert url == "https://confluence.corp.com/spaces/DEV/pages/456"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|