mkdocs2confluence 0.7.22__tar.gz → 0.7.24__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.22 → mkdocs2confluence-0.7.24}/PKG-INFO +1 -1
  2. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/pyproject.toml +1 -1
  3. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs2confluence.egg-info/PKG-INFO +1 -1
  4. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/cli.py +0 -1
  5. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/publisher/client.py +52 -17
  6. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/publisher/pipeline.py +2 -2
  7. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_publish_client.py +46 -5
  8. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/LICENSE +0 -0
  9. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/README.md +0 -0
  10. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/setup.cfg +0 -0
  11. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  12. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  13. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  14. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  15. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  16. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/__init__.py +0 -0
  17. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  18. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  19. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  20. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/ir/document.py +0 -0
  21. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  22. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  23. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  24. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/loader/config.py +0 -0
  25. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  26. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  27. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/loader/page.py +0 -0
  28. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  29. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  30. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  31. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  32. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  33. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  34. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  35. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  36. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  37. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  38. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  39. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  40. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  41. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preview/render.py +0 -0
  42. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/preview/server.py +0 -0
  43. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  44. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  45. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  46. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  47. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  48. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  49. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  50. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  51. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_abbrevs.py +0 -0
  52. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_cli.py +0 -0
  53. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_editlink.py +0 -0
  54. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_emitter.py +0 -0
  55. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_extra_css.py +0 -0
  56. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_frontmatter.py +0 -0
  57. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_icons.py +0 -0
  58. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_images.py +0 -0
  59. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_internallinks.py +0 -0
  60. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_ir.py +0 -0
  61. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_linkdefs.py +0 -0
  62. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_loader.py +0 -0
  63. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_mermaid.py +0 -0
  64. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_page_loader.py +0 -0
  65. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_parser.py +0 -0
  66. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_pdf.py +0 -0
  67. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_preprocess.py +0 -0
  68. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_preview.py +0 -0
  69. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_publish_config.py +0 -0
  70. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_publish_pipeline.py +0 -0
  71. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_server.py +0 -0
  72. {mkdocs2confluence-0.7.22 → mkdocs2confluence-0.7.24}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.22
3
+ Version: 0.7.24
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.22"
3
+ version = "0.7.24"
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.22
3
+ Version: 0.7.24
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,7 +476,6 @@ 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
480
  elif conf_config.space_key:
482
481
  space_id = client.get_space_id(conf_config.space_key)
@@ -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,6 +125,7 @@ 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
+
128
129
  # ── Folders ────────────────────────────────────────────────────────────────
129
130
 
130
131
  def find_folder_under(
@@ -365,28 +366,62 @@ class ConfluenceClient:
365
366
  def set_page_status(self, page_id: str, status_key: str, space_key: str | None = None) -> None:
366
367
  """Set the Confluence page content state (e.g. ``rough-draft``, ``in-progress``).
367
368
 
368
- Uses the v1 ``PUT /content/{id}/state`` endpoint. When *space_key* is
369
- provided the space states are fetched once (cached) so the correct
370
- ``id``, ``name``, and ``color`` are sent required to match an
371
- 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.
372
373
  """
374
+ def _normalize(s: str) -> str:
375
+ return s.lower().replace("-", " ").strip()
376
+
373
377
  name = status_key.replace("-", " ").title()
374
- state_body: dict[str, Any] = {"name": name}
375
-
376
- if space_key:
377
- if space_key not in self._space_states:
378
- resp = self._http.get(self._v1(f"/space/{space_key}/state"))
379
- if resp.is_success:
380
- self._space_states[space_key] = resp.json() if isinstance(resp.json(), list) else []
381
- for state in self._space_states.get(space_key, []):
382
- if state.get("name", "").lower() == name.lower():
383
- state_body = {"id": state["id"], "name": state["name"], "color": state["color"]}
384
- 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")}
385
407
 
386
408
  url = self._v1(f"/content/{page_id}/state")
387
- resp = self._http.put(url, json=state_body)
409
+ resp = self._http.put(url, json=body)
388
410
  self._raise_for_status(resp, f"set_page_status({page_id!r}, {status_key!r})")
389
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
+
390
425
  def list_attachments(self, page_id: str) -> dict[str, dict[str, Any]]:
391
426
  """Return a ``{filename: metadata}`` mapping of all page attachments.
392
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: