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.
Files changed (50) hide show
  1. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/PKG-INFO +1 -1
  2. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/__init__.py +1 -1
  3. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/applier.py +9 -1
  4. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/cli.py +2 -2
  5. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/confluence.py +37 -7
  6. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/publish.py +9 -2
  7. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_confluence.py +66 -1
  8. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/.github/workflows/publish.yml +0 -0
  9. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/.gitignore +0 -0
  10. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/CLAUDE.md +0 -0
  11. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/LICENSE +0 -0
  12. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/PRD.md +0 -0
  13. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/README.md +0 -0
  14. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/assets.py +0 -0
  15. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/config.py +0 -0
  16. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/converter.py +0 -0
  17. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/envelope.py +0 -0
  18. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/errors.py +0 -0
  19. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/guide.py +0 -0
  20. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/lockfile.py +0 -0
  21. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/manifest.py +0 -0
  22. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/output.py +0 -0
  23. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/planner.py +0 -0
  24. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/puller.py +0 -0
  25. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/py.typed +0 -0
  26. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/reverse_converter.py +0 -0
  27. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/validator.py +0 -0
  28. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub/verifier.py +0 -0
  29. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/confpub.lock +0 -0
  30. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/pyproject.toml +0 -0
  31. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/__init__.py +0 -0
  32. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/conftest.py +0 -0
  33. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_applier.py +0 -0
  34. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_assets.py +0 -0
  35. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_config.py +0 -0
  36. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_converter.py +0 -0
  37. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_envelope.py +0 -0
  38. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_errors.py +0 -0
  39. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_guide.py +0 -0
  40. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_integration.py +0 -0
  41. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_lockfile.py +0 -0
  42. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_manifest.py +0 -0
  43. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_output.py +0 -0
  44. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_planner.py +0 -0
  45. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_publish.py +0 -0
  46. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_puller.py +0 -0
  47. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_reverse_converter.py +0 -0
  48. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_validator.py +0 -0
  49. {confpub_cli-1.1.0 → confpub_cli-1.2.0}/tests/test_verifier.py +0 -0
  50. {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.1.0
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
@@ -1,3 +1,3 @@
1
1
  """confpub — Agent-first CLI to publish Markdown to Confluence."""
2
2
 
3
- __version__ = "1.1.0"
3
+ __version__ = "1.2.0"
@@ -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
- return [_slim_space(s) for s in raw]
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 _slim_page(page: dict[str, Any], *, base_url: str = "") -> dict[str, Any]:
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 or links.get("base", "")
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
- result["webui"] = links.get("webui", "")
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": {"version": new_version, "page_id": page_id},
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