mkdocs2confluence 0.7.23__tar.gz → 0.7.25__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 (72) hide show
  1. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/PKG-INFO +1 -1
  2. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/pyproject.toml +1 -1
  3. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs2confluence.egg-info/PKG-INFO +1 -1
  4. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/cli.py +1 -4
  5. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/publisher/client.py +51 -31
  6. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/publisher/pipeline.py +2 -2
  7. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_publish_client.py +46 -5
  8. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/LICENSE +0 -0
  9. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/README.md +0 -0
  10. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/setup.cfg +0 -0
  11. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  12. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  13. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  14. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  15. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  16. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/__init__.py +0 -0
  17. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  18. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  19. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  20. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/ir/document.py +0 -0
  21. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  22. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  23. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  24. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/loader/config.py +0 -0
  25. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  26. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  27. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/loader/page.py +0 -0
  28. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  29. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  30. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  31. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  32. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  33. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  34. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  35. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  36. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  37. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  38. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  39. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  40. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  41. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preview/render.py +0 -0
  42. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/preview/server.py +0 -0
  43. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  44. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  45. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  46. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  47. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  48. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  49. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  50. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  51. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_abbrevs.py +0 -0
  52. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_cli.py +0 -0
  53. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_editlink.py +0 -0
  54. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_emitter.py +0 -0
  55. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_extra_css.py +0 -0
  56. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_frontmatter.py +0 -0
  57. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_icons.py +0 -0
  58. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_images.py +0 -0
  59. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_internallinks.py +0 -0
  60. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_ir.py +0 -0
  61. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_linkdefs.py +0 -0
  62. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_loader.py +0 -0
  63. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_mermaid.py +0 -0
  64. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_page_loader.py +0 -0
  65. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_parser.py +0 -0
  66. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_pdf.py +0 -0
  67. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_preprocess.py +0 -0
  68. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_preview.py +0 -0
  69. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_publish_config.py +0 -0
  70. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_publish_pipeline.py +0 -0
  71. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_server.py +0 -0
  72. {mkdocs2confluence-0.7.23 → mkdocs2confluence-0.7.25}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.23
3
+ Version: 0.7.25
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.7.23"
3
+ version = "0.7.25"
4
4
  description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
5
5
  readme = "README.md"
6
6
  license = { text = "GPL-3.0-or-later" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.23
3
+ Version: 0.7.25
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -476,12 +476,9 @@ def _cmd_publish(args: argparse.Namespace) -> None:
476
476
  with ConfluenceClient(conf_config) as client:
477
477
  if conf_config.parent_page_id:
478
478
  # parent_page_id is the authoritative anchor — derive space from it.
479
- # space_key is only used when no parent_page_id is configured.
480
479
  space_id = client.get_space_id_from_page(conf_config.parent_page_id)
481
- space_key = conf_config.space_key or client.get_space_key_from_page(conf_config.parent_page_id)
482
480
  elif conf_config.space_key:
483
481
  space_id = client.get_space_id(conf_config.space_key)
484
- space_key = conf_config.space_key
485
482
  else:
486
483
  print(
487
484
  "error: cannot determine space — set 'space_key' or 'parent_page_id' in mkdocs.yml",
@@ -494,7 +491,7 @@ def _cmd_publish(args: argparse.Namespace) -> None:
494
491
  partial = bool(getattr(args, "page", None) or getattr(args, "section", None))
495
492
  report = execute_publish(
496
493
  plan, client, dry_run=False, space_id=space_id,
497
- space_key=space_key,
494
+ space_key=conf_config.space_key,
498
495
  docs_dir=config.docs_dir, full_width=conf_config.full_width,
499
496
  root_page_id=conf_config.parent_page_id,
500
497
  prune=getattr(args, "prune", False) and not partial,
@@ -33,7 +33,7 @@ class ConfluenceClient:
33
33
  def __init__(self, config: ConfluenceConfig) -> None:
34
34
  self._config = config
35
35
  self._client: httpx.Client | None = None
36
- self._space_states: dict[str, list[dict[str, Any]]] = {} # space_key → list of ContentState
36
+ self._space_states: dict[str, list[dict[str, Any]]] = {} # cache_key → list of ContentState
37
37
 
38
38
  # ── Context manager ────────────────────────────────────────────────────────
39
39
 
@@ -125,20 +125,6 @@ class ConfluenceClient:
125
125
  raise ConfluenceError(f"Could not determine spaceId from page {page_id!r}.")
126
126
  return str(space_id)
127
127
 
128
- def get_space_key_from_page(self, page_id: str) -> str | None:
129
- """Return the space key for the space containing *page_id*.
130
-
131
- Uses the v1 content endpoint which includes ``space.key``.
132
- Returns ``None`` on any failure (non-fatal — only used for state lookup).
133
- """
134
- try:
135
- url = self._v1(f"/content/{page_id}")
136
- resp = self._http.get(url, params={"expand": "space"})
137
- if resp.is_success:
138
- return resp.json().get("space", {}).get("key") or None
139
- except Exception:
140
- pass
141
- return None
142
128
 
143
129
  # ── Folders ────────────────────────────────────────────────────────────────
144
130
 
@@ -380,28 +366,62 @@ class ConfluenceClient:
380
366
  def set_page_status(self, page_id: str, status_key: str, space_key: str | None = None) -> None:
381
367
  """Set the Confluence page content state (e.g. ``rough-draft``, ``in-progress``).
382
368
 
383
- Uses the v1 ``PUT /content/{id}/state`` endpoint. When *space_key* is
384
- provided the space states are fetched once (cached) so the correct
385
- ``id``, ``name``, and ``color`` are sent required to match an
386
- existing space state rather than creating a new custom one.
369
+ Uses ``GET /content/{id}/state/available`` (requires only edit permission) on the
370
+ first call to discover available space states, then caches them for the run.
371
+ Sends ``{id, name, color}`` when a matching space state is found, otherwise
372
+ falls back to ``{name, color}`` with a sensible default colour.
387
373
  """
374
+ def _normalize(s: str) -> str:
375
+ return s.lower().replace("-", " ").strip()
376
+
388
377
  name = status_key.replace("-", " ").title()
389
- state_body: dict[str, Any] = {"name": name}
390
-
391
- if space_key:
392
- if space_key not in self._space_states:
393
- resp = self._http.get(self._v1(f"/space/{space_key}/state"))
394
- if resp.is_success:
395
- self._space_states[space_key] = resp.json() if isinstance(resp.json(), list) else []
396
- for state in self._space_states.get(space_key, []):
397
- if state.get("name", "").lower() == name.lower():
398
- state_body = {"id": state["id"], "name": state["name"], "color": state["color"]}
399
- break
378
+ cache_key = space_key or "_default"
379
+
380
+ # Fetch space states once per run (keyed by space so one run = one API call)
381
+ if cache_key not in self._space_states:
382
+ self._space_states[cache_key] = self._fetch_available_states(page_id)
383
+
384
+ matched: dict[str, Any] | None = None
385
+ for state in self._space_states.get(cache_key, []):
386
+ if _normalize(state.get("name", "")) == _normalize(name):
387
+ matched = state
388
+ break
389
+
390
+ if matched:
391
+ body: dict[str, Any] = {
392
+ "id": matched["id"],
393
+ "name": matched["name"],
394
+ "color": matched["color"],
395
+ }
396
+ else:
397
+ # Fall back: name + color (required together when no id)
398
+ _default_colors = {
399
+ "in progress": "#2684ff",
400
+ "rough draft": "#97a0af",
401
+ "reviewed": "#57d9a3",
402
+ "done": "#57d9a3",
403
+ "in review": "#ffc400",
404
+ "outdated": "#ff7452",
405
+ }
406
+ body = {"name": name, "color": _default_colors.get(_normalize(name), "#2684ff")}
400
407
 
401
408
  url = self._v1(f"/content/{page_id}/state")
402
- resp = self._http.put(url, json=state_body)
409
+ resp = self._http.put(url, json=body)
403
410
  self._raise_for_status(resp, f"set_page_status({page_id!r}, {status_key!r})")
404
411
 
412
+ def _fetch_available_states(self, page_id: str) -> list[dict[str, Any]]:
413
+ """Fetch space content states via the content-available endpoint.
414
+
415
+ Requires only content-edit permission (not space admin).
416
+ Returns the ``spaceContentStates`` list, which is the same for every
417
+ page in the space.
418
+ """
419
+ resp = self._http.get(self._v1(f"/content/{page_id}/state/available"))
420
+ if resp.is_success:
421
+ data: dict[str, Any] = resp.json()
422
+ return data.get("spaceContentStates") or []
423
+ return []
424
+
405
425
  def list_attachments(self, page_id: str) -> dict[str, dict[str, Any]]:
406
426
  """Return a ``{filename: metadata}`` mapping of all page attachments.
407
427
 
@@ -666,8 +666,8 @@ def _post_process_action(
666
666
  try:
667
667
  client.set_page_status(action.page_id, action.confluence_status, space_key=space_key)
668
668
  except Exception as exc:
669
- if not quiet:
670
- print(f" [warn] could not set page status '{action.confluence_status}': {exc}", file=sys.stderr)
669
+ # Always print status errors — user configured status explicitly
670
+ print(f" [warn] could not set page status '{action.confluence_status}': {exc}", file=sys.stderr)
671
671
 
672
672
  # Upload assets — skip files whose mtime is not newer than Confluence.
673
673
  if action.page_id and action.attachments:
@@ -168,19 +168,60 @@ def test_set_page_labels_skips_post_when_empty() -> None:
168
168
 
169
169
 
170
170
  def test_set_page_status_sends_put() -> None:
171
- """set_page_status PUTs name-based body to the v1 /content/{id}/state endpoint."""
172
- transport = _MockTransport(httpx.Response(200, json={}))
171
+ """set_page_status fetches available states then PUTs {name, color} to /content/{id}/state."""
172
+ # First request: GET /content/42/state/available (returns empty → fallback)
173
+ # Second request: PUT /content/42/state
174
+ available_resp = httpx.Response(200, json={"spaceContentStates": [], "customContentStates": []})
175
+ put_resp = httpx.Response(200, json={})
176
+ transport = _MockTransport(available_resp, put_resp)
173
177
  config = _make_config()
174
178
  with ConfluenceClient(config) as client:
175
179
  client._client = httpx.Client(transport=transport) # type: ignore[assignment]
176
180
  client.set_page_status("42", "in-progress")
177
- assert len(transport.requests) == 1
178
- req = transport.requests[0]
181
+ assert len(transport.requests) == 2
182
+ assert transport.requests[0].method == "GET"
183
+ assert "/content/42/state/available" in str(transport.requests[0].url)
184
+ req = transport.requests[1]
179
185
  assert req.method == "PUT"
180
186
  assert "/content/42/state" in str(req.url)
181
187
  import json
182
188
  body = json.loads(req.content)
183
- assert body == {"name": "In Progress"}
189
+ # Fallback: name + default colour (no matching space state)
190
+ assert body["name"] == "In Progress"
191
+ assert "color" in body
192
+
193
+
194
+ def test_set_page_status_uses_space_state_when_matched() -> None:
195
+ """set_page_status sends {id, name, color} when a matching space state is found."""
196
+ space_states = [{"id": 1, "name": "In Progress", "color": "#2684ff"}]
197
+ available_resp = httpx.Response(200, json={"spaceContentStates": space_states, "customContentStates": []})
198
+ put_resp = httpx.Response(200, json={})
199
+ transport = _MockTransport(available_resp, put_resp)
200
+ config = _make_config()
201
+ with ConfluenceClient(config) as client:
202
+ client._client = httpx.Client(transport=transport) # type: ignore[assignment]
203
+ client.set_page_status("42", "in-progress")
204
+ import json
205
+ body = json.loads(transport.requests[1].content)
206
+ assert body == {"id": 1, "name": "In Progress", "color": "#2684ff"}
207
+
208
+
209
+ def test_set_page_status_caches_space_states() -> None:
210
+ """Space states are fetched only once even when set_page_status is called twice."""
211
+ space_states = [{"id": 0, "name": "Rough Draft", "color": "#97a0af"}]
212
+ available_resp = httpx.Response(200, json={"spaceContentStates": space_states, "customContentStates": []})
213
+ put_resp = httpx.Response(200, json={})
214
+ transport = _MockTransport(available_resp, put_resp, put_resp)
215
+ config = _make_config()
216
+ with ConfluenceClient(config) as client:
217
+ client._client = httpx.Client(transport=transport) # type: ignore[assignment]
218
+ client.set_page_status("42", "rough-draft")
219
+ client.set_page_status("99", "rough-draft")
220
+ # Only one GET (cached), two PUTs
221
+ gets = [r for r in transport.requests if r.method == "GET"]
222
+ puts = [r for r in transport.requests if r.method == "PUT"]
223
+ assert len(gets) == 1
224
+ assert len(puts) == 2
184
225
 
185
226
 
186
227
  def test_set_page_full_width_creates_property_when_absent() -> None: