markdown-to-confluence 0.2.6__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
md2conf/api.py CHANGED
@@ -1,21 +1,21 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2024, Levente Hunyadi
4
+ Copyright 2022-2025, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
8
8
 
9
+ import enum
9
10
  import io
10
11
  import json
11
12
  import logging
12
13
  import mimetypes
13
14
  import typing
14
- from contextlib import contextmanager
15
15
  from dataclasses import dataclass
16
16
  from pathlib import Path
17
17
  from types import TracebackType
18
- from typing import Dict, Generator, List, Optional, Type, Union
18
+ from typing import Optional, Union
19
19
  from urllib.parse import urlencode, urlparse, urlunparse
20
20
 
21
21
  import requests
@@ -31,12 +31,17 @@ JsonType = Union[
31
31
  int,
32
32
  float,
33
33
  str,
34
- Dict[str, "JsonType"],
35
- List["JsonType"],
34
+ dict[str, "JsonType"],
35
+ list["JsonType"],
36
36
  ]
37
37
 
38
38
 
39
- def build_url(base_url: str, query: Optional[Dict[str, str]] = None) -> str:
39
+ class ConfluenceVersion(enum.Enum):
40
+ VERSION_1 = "rest/api"
41
+ VERSION_2 = "api/v2"
42
+
43
+
44
+ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
40
45
  "Builds a URL with scheme, host, port, path and query string parameters."
41
46
 
42
47
  scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
@@ -66,7 +71,7 @@ class ConfluenceAttachment:
66
71
  @dataclass
67
72
  class ConfluencePage:
68
73
  id: str
69
- space_key: str
74
+ space_id: str
70
75
  title: str
71
76
  version: int
72
77
  content: str
@@ -101,7 +106,7 @@ class ConfluenceAPI:
101
106
 
102
107
  def __exit__(
103
108
  self,
104
- exc_type: Optional[Type[BaseException]],
109
+ exc_type: Optional[type[BaseException]],
105
110
  exc_val: Optional[BaseException],
106
111
  exc_tb: Optional[TracebackType],
107
112
  ) -> None:
@@ -116,6 +121,9 @@ class ConfluenceSession:
116
121
  base_path: str
117
122
  space_key: str
118
123
 
124
+ _space_id_to_key: dict[str, str]
125
+ _space_key_to_id: dict[str, str]
126
+
119
127
  def __init__(
120
128
  self, session: requests.Session, domain: str, base_path: str, space_key: str
121
129
  ) -> None:
@@ -124,30 +132,46 @@ class ConfluenceSession:
124
132
  self.base_path = base_path
125
133
  self.space_key = space_key
126
134
 
135
+ self._space_id_to_key = {}
136
+ self._space_key_to_id = {}
137
+
127
138
  def close(self) -> None:
128
139
  self.session.close()
140
+ self.session = requests.Session()
129
141
 
130
- @contextmanager
131
- def switch_space(self, new_space_key: str) -> Generator[None, None, None]:
132
- old_space_key = self.space_key
133
- self.space_key = new_space_key
134
- try:
135
- yield
136
- finally:
137
- self.space_key = old_space_key
142
+ def _build_url(
143
+ self,
144
+ version: ConfluenceVersion,
145
+ path: str,
146
+ query: Optional[dict[str, str]] = None,
147
+ ) -> str:
148
+ """
149
+ Builds a full URL for invoking the Confluence API.
138
150
 
139
- def _build_url(self, path: str, query: Optional[Dict[str, str]] = None) -> str:
140
- base_url = f"https://{self.domain}{self.base_path}rest/api{path}"
151
+ :param prefix: A URL path prefix that depends on the Confluence API version.
152
+ :param path: Path of API endpoint to invoke.
153
+ :param query: Query parameters to pass to the API endpoint.
154
+ :returns: A full URL.
155
+ """
156
+
157
+ base_url = f"https://{self.domain}{self.base_path}{version.value}{path}"
141
158
  return build_url(base_url, query)
142
159
 
143
- def _invoke(self, path: str, query: Dict[str, str]) -> JsonType:
144
- url = self._build_url(path, query)
160
+ def _invoke(
161
+ self,
162
+ version: ConfluenceVersion,
163
+ path: str,
164
+ query: Optional[dict[str, str]] = None,
165
+ ) -> JsonType:
166
+ "Execute an HTTP request via Confluence API."
167
+
168
+ url = self._build_url(version, path, query)
145
169
  response = self.session.get(url)
146
170
  response.raise_for_status()
147
171
  return response.json()
148
172
 
149
- def _save(self, path: str, data: dict) -> None:
150
- url = self._build_url(path)
173
+ def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
174
+ url = self._build_url(version, path)
151
175
  response = self.session.put(
152
176
  url,
153
177
  data=json.dumps(data),
@@ -155,24 +179,68 @@ class ConfluenceSession:
155
179
  )
156
180
  response.raise_for_status()
157
181
 
182
+ def space_id_to_key(self, id: str) -> str:
183
+ "Finds the Confluence space key for a space ID."
184
+
185
+ key = self._space_id_to_key.get(id)
186
+ if key is None:
187
+ payload = self._invoke(
188
+ ConfluenceVersion.VERSION_2,
189
+ "/spaces",
190
+ {"ids": id, "type": "global", "status": "current"},
191
+ )
192
+ payload = typing.cast(dict[str, JsonType], payload)
193
+ results = typing.cast(list[JsonType], payload["results"])
194
+ if len(results) != 1:
195
+ raise ConfluenceError(f"unique space not found with id: {id}")
196
+
197
+ result = typing.cast(dict[str, JsonType], results[0])
198
+ key = typing.cast(str, result["key"])
199
+
200
+ self._space_id_to_key[id] = key
201
+
202
+ return key
203
+
204
+ def space_key_to_id(self, key: str) -> str:
205
+ "Finds the Confluence space ID for a space key."
206
+
207
+ id = self._space_key_to_id.get(key)
208
+ if id is None:
209
+ payload = self._invoke(
210
+ ConfluenceVersion.VERSION_2,
211
+ "/spaces",
212
+ {"keys": key, "type": "global", "status": "current"},
213
+ )
214
+ payload = typing.cast(dict[str, JsonType], payload)
215
+ results = typing.cast(list[JsonType], payload["results"])
216
+ if len(results) != 1:
217
+ raise ConfluenceError(f"unique space not found with key: {key}")
218
+
219
+ result = typing.cast(dict[str, JsonType], results[0])
220
+ id = typing.cast(str, result["id"])
221
+
222
+ self._space_key_to_id[key] = id
223
+
224
+ return id
225
+
158
226
  def get_attachment_by_name(
159
- self, page_id: str, filename: str, *, space_key: Optional[str] = None
227
+ self, page_id: str, filename: str
160
228
  ) -> ConfluenceAttachment:
161
- path = f"/content/{page_id}/child/attachment"
162
- query = {"spaceKey": space_key or self.space_key, "filename": filename}
163
- data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
229
+ path = f"/pages/{page_id}/attachments"
230
+ query = {"filename": filename}
231
+ data = typing.cast(
232
+ dict[str, JsonType], self._invoke(ConfluenceVersion.VERSION_2, path, query)
233
+ )
164
234
 
165
- results = typing.cast(List[JsonType], data["results"])
235
+ results = typing.cast(list[JsonType], data["results"])
166
236
  if len(results) != 1:
167
237
  raise ConfluenceError(f"no such attachment on page {page_id}: {filename}")
168
- result = typing.cast(Dict[str, JsonType], results[0])
238
+ result = typing.cast(dict[str, JsonType], results[0])
169
239
 
170
240
  id = typing.cast(str, result["id"])
171
- extensions = typing.cast(Dict[str, JsonType], result["extensions"])
172
- media_type = typing.cast(str, extensions["mediaType"])
173
- file_size = typing.cast(int, extensions["fileSize"])
174
- comment = extensions.get("comment", "")
175
- comment = typing.cast(str, comment)
241
+ media_type = typing.cast(str, result["mediaType"])
242
+ file_size = typing.cast(int, result["fileSize"])
243
+ comment = typing.cast(str, result.get("comment", ""))
176
244
  return ConfluenceAttachment(id, media_type, file_size, comment)
177
245
 
178
246
  def upload_attachment(
@@ -205,9 +273,7 @@ class ConfluenceSession:
205
273
  raise ConfluenceError(f"file not found: {attachment_path}")
206
274
 
207
275
  try:
208
- attachment = self.get_attachment_by_name(
209
- page_id, attachment_name, space_key=space_key
210
- )
276
+ attachment = self.get_attachment_by_name(page_id, attachment_name)
211
277
 
212
278
  if attachment_path is not None:
213
279
  if not force and attachment.file_size == attachment_path.stat().st_size:
@@ -226,7 +292,7 @@ class ConfluenceSession:
226
292
  except ConfluenceError:
227
293
  path = f"/content/{page_id}/child/attachment"
228
294
 
229
- url = self._build_url(path)
295
+ url = self._build_url(ConfluenceVersion.VERSION_1, path)
230
296
 
231
297
  if attachment_path is not None:
232
298
  with open(attachment_path, "rb") as attachment_file:
@@ -304,7 +370,7 @@ class ConfluenceSession:
304
370
  }
305
371
 
306
372
  LOGGER.info("Updating attachment: %s", attachment_id)
307
- self._save(path, data)
373
+ self._save(ConfluenceVersion.VERSION_1, path, data)
308
374
 
309
375
  def get_page_id_by_title(
310
376
  self,
@@ -321,82 +387,47 @@ class ConfluenceSession:
321
387
  """
322
388
 
323
389
  LOGGER.info("Looking up page with title: %s", title)
324
- path = "/content"
325
- query = {"title": title, "spaceKey": space_key or self.space_key}
326
- data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
390
+ path = "/pages"
391
+ query = {
392
+ "space-id": self.space_key_to_id(space_key or self.space_key),
393
+ "title": title,
394
+ }
395
+ payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
396
+ payload = typing.cast(dict[str, JsonType], payload)
327
397
 
328
- results = typing.cast(List[JsonType], data["results"])
398
+ results = typing.cast(list[JsonType], payload["results"])
329
399
  if len(results) != 1:
330
- raise ConfluenceError(f"page not found with title: {title}")
400
+ raise ConfluenceError(f"unique page not found with title: {title}")
331
401
 
332
- result = typing.cast(Dict[str, JsonType], results[0])
402
+ result = typing.cast(dict[str, JsonType], results[0])
333
403
  id = typing.cast(str, result["id"])
334
404
  return id
335
405
 
336
- def get_page(
337
- self, page_id: str, *, space_key: Optional[str] = None
338
- ) -> ConfluencePage:
406
+ def get_page(self, page_id: str) -> ConfluencePage:
339
407
  """
340
408
  Retrieve Confluence wiki page details.
341
409
 
342
410
  :param page_id: The Confluence page ID.
343
- :param space_key: The Confluence space key (unless the default space is to be used).
344
411
  :returns: Confluence page info.
345
412
  """
346
413
 
347
- path = f"/content/{page_id}"
348
- query = {
349
- "spaceKey": space_key or self.space_key,
350
- "expand": "body.storage,version",
351
- }
352
-
353
- data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
354
- version = typing.cast(Dict[str, JsonType], data["version"])
355
- body = typing.cast(Dict[str, JsonType], data["body"])
356
- storage = typing.cast(Dict[str, JsonType], body["storage"])
414
+ path = f"/pages/{page_id}"
415
+ query = {"body-format": "storage"}
416
+ payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
417
+ data = typing.cast(dict[str, JsonType], payload)
418
+ version = typing.cast(dict[str, JsonType], data["version"])
419
+ body = typing.cast(dict[str, JsonType], data["body"])
420
+ storage = typing.cast(dict[str, JsonType], body["storage"])
357
421
 
358
422
  return ConfluencePage(
359
423
  id=page_id,
360
- space_key=space_key or self.space_key,
424
+ space_id=typing.cast(str, data["spaceId"]),
361
425
  title=typing.cast(str, data["title"]),
362
426
  version=typing.cast(int, version["number"]),
363
427
  content=typing.cast(str, storage["value"]),
364
428
  )
365
429
 
366
- def get_page_ancestors(
367
- self, page_id: str, *, space_key: Optional[str] = None
368
- ) -> Dict[str, str]:
369
- """
370
- Retrieve Confluence wiki page ancestors.
371
-
372
- :param page_id: The Confluence page ID.
373
- :param space_key: The Confluence space key (unless the default space is to be used).
374
- :returns: Dictionary of ancestor page ID to title, with topmost ancestor first.
375
- """
376
-
377
- path = f"/content/{page_id}"
378
- query = {
379
- "spaceKey": space_key or self.space_key,
380
- "expand": "ancestors",
381
- }
382
- data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
383
- ancestors = typing.cast(List[JsonType], data["ancestors"])
384
-
385
- # from the JSON array of ancestors, extract the "id" and "title"
386
- results: Dict[str, str] = {}
387
- for node in ancestors:
388
- ancestor = typing.cast(Dict[str, JsonType], node)
389
- id = typing.cast(str, ancestor["id"])
390
- title = typing.cast(str, ancestor["title"])
391
- results[id] = title
392
- return results
393
-
394
- def get_page_version(
395
- self,
396
- page_id: str,
397
- *,
398
- space_key: Optional[str] = None,
399
- ) -> int:
430
+ def get_page_version(self, page_id: str) -> int:
400
431
  """
401
432
  Retrieve a Confluence wiki page version.
402
433
 
@@ -405,13 +436,10 @@ class ConfluenceSession:
405
436
  :returns: Confluence page version.
406
437
  """
407
438
 
408
- path = f"/content/{page_id}"
409
- query = {
410
- "spaceKey": space_key or self.space_key,
411
- "expand": "version",
412
- }
413
- data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
414
- version = typing.cast(Dict[str, JsonType], data["version"])
439
+ path = f"/pages/{page_id}"
440
+ payload = self._invoke(ConfluenceVersion.VERSION_2, path)
441
+ data = typing.cast(dict[str, JsonType], payload)
442
+ version = typing.cast(dict[str, JsonType], data["version"])
415
443
  return typing.cast(int, version["number"])
416
444
 
417
445
  def update_page(
@@ -419,30 +447,39 @@ class ConfluenceSession:
419
447
  page_id: str,
420
448
  new_content: str,
421
449
  *,
422
- space_key: Optional[str] = None,
450
+ title: Optional[str] = None,
423
451
  ) -> None:
424
- page = self.get_page(page_id, space_key=space_key)
452
+ """
453
+ Update a page via the Confluence API.
454
+
455
+ :param page_id: The Confluence page ID.
456
+ :param new_content: Confluence Storage Format XHTML.
457
+ :param space_key: The Confluence space key (unless the default space is to be used).
458
+ :param title: New title to assign to the page. Needs to be unique within a space.
459
+ """
460
+
461
+ page = self.get_page(page_id)
462
+ new_title = title or page.title
425
463
 
426
464
  try:
427
465
  old_content = sanitize_confluence(page.content)
428
- if old_content == new_content:
466
+ if page.title == new_title and old_content == new_content:
429
467
  LOGGER.info("Up-to-date page: %s", page_id)
430
468
  return
431
469
  except ParseError as exc:
432
470
  LOGGER.warning(exc)
433
471
 
434
- path = f"/content/{page_id}"
472
+ path = f"/pages/{page_id}"
435
473
  data = {
436
474
  "id": page_id,
437
- "type": "page",
438
- "title": page.title, # title needs to be unique within a space so the original title is maintained
439
- "space": {"key": space_key or self.space_key},
475
+ "status": "current",
476
+ "title": new_title,
440
477
  "body": {"storage": {"value": new_content, "representation": "storage"}},
441
478
  "version": {"minorEdit": True, "number": page.version + 1},
442
479
  }
443
480
 
444
481
  LOGGER.info("Updating page: %s", page_id)
445
- self._save(path, data)
482
+ self._save(ConfluenceVersion.VERSION_2, path, data)
446
483
 
447
484
  def create_page(
448
485
  self,
@@ -452,18 +489,22 @@ class ConfluenceSession:
452
489
  *,
453
490
  space_key: Optional[str] = None,
454
491
  ) -> ConfluencePage:
455
- path = "/content/"
492
+ """
493
+ Create a new page via Confluence API.
494
+ """
495
+
496
+ path = "/pages/"
456
497
  query = {
457
- "type": "page",
498
+ "spaceId": self.space_key_to_id(space_key or self.space_key),
499
+ "status": "current",
458
500
  "title": title,
459
- "space": {"key": space_key or self.space_key},
501
+ "parentId": parent_page_id,
460
502
  "body": {"storage": {"value": new_content, "representation": "storage"}},
461
- "ancestors": [{"type": "page", "id": parent_page_id}],
462
503
  }
463
504
 
464
505
  LOGGER.info("Creating page: %s", title)
465
506
 
466
- url = self._build_url(path)
507
+ url = self._build_url(ConfluenceVersion.VERSION_2, path)
467
508
  response = self.session.post(
468
509
  url,
469
510
  data=json.dumps(query),
@@ -471,43 +512,66 @@ class ConfluenceSession:
471
512
  )
472
513
  response.raise_for_status()
473
514
 
474
- data = typing.cast(Dict[str, JsonType], response.json())
475
- version = typing.cast(Dict[str, JsonType], data["version"])
476
- body = typing.cast(Dict[str, JsonType], data["body"])
477
- storage = typing.cast(Dict[str, JsonType], body["storage"])
515
+ data = typing.cast(dict[str, JsonType], response.json())
516
+ version = typing.cast(dict[str, JsonType], data["version"])
517
+ body = typing.cast(dict[str, JsonType], data["body"])
518
+ storage = typing.cast(dict[str, JsonType], body["storage"])
478
519
 
479
520
  return ConfluencePage(
480
521
  id=typing.cast(str, data["id"]),
481
- space_key=space_key or self.space_key,
522
+ space_id=typing.cast(str, data["spaceId"]),
482
523
  title=typing.cast(str, data["title"]),
483
524
  version=typing.cast(int, version["number"]),
484
525
  content=typing.cast(str, storage["value"]),
485
526
  )
486
527
 
528
+ def delete_page(self, page_id: str, *, purge: bool = False) -> None:
529
+ """
530
+ Delete a page via Confluence API.
531
+
532
+ :param page_id: The Confluence page ID.
533
+ :param purge: True to completely purge the page, False to move to trash only.
534
+ """
535
+
536
+ path = f"/pages/{page_id}"
537
+
538
+ # move to trash
539
+ url = self._build_url(ConfluenceVersion.VERSION_2, path)
540
+ LOGGER.info("Moving page to trash: %s", page_id)
541
+ response = self.session.delete(url)
542
+ response.raise_for_status()
543
+
544
+ if purge:
545
+ # purge from trash
546
+ query = {"purge": "true"}
547
+ url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
548
+ LOGGER.info("Permanently deleting page: %s", page_id)
549
+ response = self.session.delete(url)
550
+ response.raise_for_status()
551
+
487
552
  def page_exists(
488
553
  self, title: str, *, space_key: Optional[str] = None
489
554
  ) -> Optional[str]:
490
- path = "/content"
555
+ path = "/pages"
491
556
  query = {
492
- "type": "page",
493
557
  "title": title,
494
- "spaceKey": space_key or self.space_key,
558
+ "space-id": self.space_key_to_id(space_key or self.space_key),
495
559
  }
496
560
 
497
561
  LOGGER.info("Checking if page exists with title: %s", title)
498
562
 
499
- url = self._build_url(path)
563
+ url = self._build_url(ConfluenceVersion.VERSION_2, path)
500
564
  response = self.session.get(
501
565
  url, params=query, headers={"Content-Type": "application/json"}
502
566
  )
503
567
  response.raise_for_status()
504
568
 
505
- data = typing.cast(Dict[str, JsonType], response.json())
506
- results = typing.cast(List, data["results"])
569
+ data = typing.cast(dict[str, JsonType], response.json())
570
+ results = typing.cast(list[JsonType], data["results"])
507
571
 
508
572
  if len(results) == 1:
509
- page_info = typing.cast(Dict[str, JsonType], results[0])
510
- return typing.cast(str, page_info["id"])
573
+ result = typing.cast(dict[str, JsonType], results[0])
574
+ return typing.cast(str, result["id"])
511
575
  else:
512
576
  return None
513
577
 
md2conf/application.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2024, Levente Hunyadi
4
+ Copyright 2022-2025, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
@@ -9,9 +9,7 @@ Copyright 2022-2024, Levente Hunyadi
9
9
  import logging
10
10
  import os.path
11
11
  from pathlib import Path
12
- from typing import Dict, List, Optional
13
-
14
- import yaml
12
+ from typing import Optional
15
13
 
16
14
  from .api import ConfluencePage, ConfluenceSession
17
15
  from .converter import (
@@ -20,7 +18,7 @@ from .converter import (
20
18
  ConfluencePageMetadata,
21
19
  ConfluenceQualifiedID,
22
20
  attachment_name,
23
- extract_frontmatter,
21
+ extract_frontmatter_title,
24
22
  extract_qualified_id,
25
23
  read_qualified_id,
26
24
  )
@@ -79,7 +77,7 @@ class Application:
79
77
  LOGGER.info("Synchronizing directory: %s", local_dir)
80
78
 
81
79
  # Step 1: build index of all page metadata
82
- page_metadata: Dict[Path, ConfluencePageMetadata] = {}
80
+ page_metadata: dict[Path, ConfluencePageMetadata] = {}
83
81
  root_id = (
84
82
  ConfluenceQualifiedID(self.options.root_page_id, self.api.space_key)
85
83
  if self.options.root_page_id
@@ -96,24 +94,19 @@ class Application:
96
94
  self,
97
95
  page_path: Path,
98
96
  root_dir: Path,
99
- page_metadata: Dict[Path, ConfluencePageMetadata],
97
+ page_metadata: dict[Path, ConfluencePageMetadata],
100
98
  ) -> None:
101
99
  base_path = page_path.parent
102
100
 
103
101
  LOGGER.info("Synchronizing page: %s", page_path)
104
102
  document = ConfluenceDocument(page_path, self.options, root_dir, page_metadata)
105
-
106
- if document.id.space_key:
107
- with self.api.switch_space(document.id.space_key):
108
- self._update_document(document, base_path)
109
- else:
110
- self._update_document(document, base_path)
103
+ self._update_document(document, base_path)
111
104
 
112
105
  def _index_directory(
113
106
  self,
114
107
  local_dir: Path,
115
108
  root_id: Optional[ConfluenceQualifiedID],
116
- page_metadata: Dict[Path, ConfluencePageMetadata],
109
+ page_metadata: dict[Path, ConfluencePageMetadata],
117
110
  ) -> None:
118
111
  "Indexes Markdown files in a directory recursively."
119
112
 
@@ -121,8 +114,8 @@ class Application:
121
114
 
122
115
  matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
123
116
 
124
- files: List[Path] = []
125
- directories: List[Path] = []
117
+ files: list[Path] = []
118
+ directories: list[Path] = []
126
119
  for entry in os.scandir(local_dir):
127
120
  if matcher.is_excluded(entry.name, entry.is_dir()):
128
121
  continue
@@ -174,12 +167,10 @@ class Application:
174
167
  document = f.read()
175
168
 
176
169
  qualified_id, document = extract_qualified_id(document)
177
- frontmatter, document = extract_frontmatter(document)
170
+ frontmatter_title, _ = extract_frontmatter_title(document)
178
171
 
179
172
  if qualified_id is not None:
180
- confluence_page = self.api.get_page(
181
- qualified_id.page_id, space_key=qualified_id.space_key
182
- )
173
+ confluence_page = self.api.get_page(qualified_id.page_id)
183
174
  else:
184
175
  if parent_id is None:
185
176
  raise ValueError(
@@ -187,22 +178,21 @@ class Application:
187
178
  )
188
179
 
189
180
  # assign title from frontmatter if present
190
- if title is None and frontmatter is not None:
191
- properties = yaml.safe_load(frontmatter)
192
- if isinstance(properties, dict):
193
- property_title = properties.get("title")
194
- if isinstance(property_title, str):
195
- title = property_title
196
-
197
181
  confluence_page = self._create_page(
198
- absolute_path, document, title, parent_id
182
+ absolute_path, document, title or frontmatter_title, parent_id
199
183
  )
200
184
 
185
+ space_key = (
186
+ self.api.space_id_to_key(confluence_page.space_id)
187
+ if confluence_page.space_id
188
+ else self.api.space_key
189
+ )
190
+
201
191
  return ConfluencePageMetadata(
202
192
  domain=self.api.domain,
203
193
  base_path=self.api.base_path,
204
194
  page_id=confluence_page.id,
205
- space_key=confluence_page.space_key or self.api.space_key,
195
+ space_key=space_key,
206
196
  title=confluence_page.title or "",
207
197
  )
208
198
 
@@ -226,7 +216,7 @@ class Application:
226
216
  absolute_path,
227
217
  document,
228
218
  confluence_page.id,
229
- confluence_page.space_key,
219
+ self.api.space_id_to_key(confluence_page.space_id),
230
220
  )
231
221
  return confluence_page
232
222
 
@@ -249,7 +239,7 @@ class Application:
249
239
 
250
240
  content = document.xhtml()
251
241
  LOGGER.debug("Generated Confluence Storage Format document:\n%s", content)
252
- self.api.update_page(document.id.page_id, content)
242
+ self.api.update_page(document.id.page_id, content, title=document.title)
253
243
 
254
244
  def _update_markdown(
255
245
  self,
@@ -260,7 +250,7 @@ class Application:
260
250
  ) -> None:
261
251
  "Writes the Confluence page ID and space key at the beginning of the Markdown file."
262
252
 
263
- content: List[str] = []
253
+ content: list[str] = []
264
254
 
265
255
  # check if the file has frontmatter
266
256
  index = 0