markdown-to-confluence 0.4.0__py3-none-any.whl → 0.4.1__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.
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.1.dist-info}/METADATA +33 -11
- markdown_to_confluence-0.4.1.dist-info/RECORD +25 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +4 -12
- md2conf/api.py +189 -62
- md2conf/application.py +24 -47
- md2conf/converter.py +83 -69
- md2conf/extra.py +13 -0
- md2conf/local.py +4 -12
- md2conf/matcher.py +1 -3
- md2conf/mermaid.py +2 -7
- md2conf/processor.py +16 -34
- md2conf/properties.py +45 -12
- md2conf/scanner.py +10 -9
- markdown_to_confluence-0.4.0.dist-info/RECORD +0 -25
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.1.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.1.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.1.dist-info}/zip-safe +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Author-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
6
|
+
Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/hunyadi/md2conf
|
|
9
|
+
Project-URL: Source, https://github.com/hunyadi/md2conf
|
|
10
|
+
Keywords: markdown,converter,confluence
|
|
9
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
12
|
Classifier: Environment :: Console
|
|
11
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Programming Language :: Python :: 3
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -17,21 +18,26 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
22
|
Classifier: Typing :: Typed
|
|
21
23
|
Requires-Python: >=3.9
|
|
22
24
|
Description-Content-Type: text/markdown
|
|
23
25
|
License-File: LICENSE
|
|
24
26
|
Requires-Dist: json_strong_typing>=0.3.9
|
|
25
|
-
Requires-Dist: lxml>=
|
|
26
|
-
Requires-Dist: types-lxml>=2025.3.30
|
|
27
|
+
Requires-Dist: lxml>=6.0
|
|
27
28
|
Requires-Dist: markdown>=3.8
|
|
28
|
-
Requires-Dist: types-markdown>=3.8
|
|
29
29
|
Requires-Dist: pymdown-extensions>=10.16
|
|
30
30
|
Requires-Dist: PyYAML>=6.0
|
|
31
|
-
Requires-Dist: types-PyYAML>=6.0
|
|
32
31
|
Requires-Dist: requests>=2.32
|
|
33
|
-
Requires-Dist: types-requests>=2.32
|
|
34
32
|
Requires-Dist: typing_extensions>=4.14; python_version < "3.12"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: markdown_doc>=0.1.4; python_version >= "3.10" and extra == "dev"
|
|
35
|
+
Requires-Dist: types-lxml>=2025.3.30; extra == "dev"
|
|
36
|
+
Requires-Dist: types-markdown>=3.8; extra == "dev"
|
|
37
|
+
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
|
|
38
|
+
Requires-Dist: types-requests>=2.32; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=1.16; extra == "dev"
|
|
40
|
+
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
35
41
|
Dynamic: license-file
|
|
36
42
|
|
|
37
43
|
# Publish Markdown files to Confluence wiki
|
|
@@ -326,6 +332,22 @@ Any previously assigned labels are discarded. As per Confluence terminology, new
|
|
|
326
332
|
|
|
327
333
|
If a document has no `tags` attribute, existing Confluence labels are left intact.
|
|
328
334
|
|
|
335
|
+
### Content properties
|
|
336
|
+
|
|
337
|
+
The front-matter attribute `properties` in a Markdown document allows setting Confluence content properties on a page. Confluence content properties are a way to store structured metadata in the form of key-value pairs directly on Confluence content. The values in content properties are represented as JSON objects.
|
|
338
|
+
|
|
339
|
+
Some content properties have special meaning to Confluence. For example, the following properties cause Confluence to display a wiki page with content confined to a fixed width in regular view mode, and taking the full page width in draft mode:
|
|
340
|
+
|
|
341
|
+
```yaml
|
|
342
|
+
---
|
|
343
|
+
properties:
|
|
344
|
+
content-appearance-published: fixed-width
|
|
345
|
+
content-appearance-draft: full-width
|
|
346
|
+
---
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The attribute `properties` is parsed as a dictionary with keys of type string and values of type JSON. *md2conf* passes JSON values to Confluence REST API unchanged.
|
|
350
|
+
|
|
329
351
|
### Converting diagrams
|
|
330
352
|
|
|
331
353
|
You can include [Mermaid diagrams](https://mermaid.js.org/) in your Markdown documents to create visual representations of systems, processes, and relationships. When a Markdown document contains a code block with the language specifier `mermaid`, *md2conf* offers two options to publish the diagram:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
markdown_to_confluence-0.4.1.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
+
md2conf/__init__.py,sha256=K6ZE42N5KJjN5o2GqIFa_lcPZvMMCXPMMRWEkvlmcp0,402
|
|
3
|
+
md2conf/__main__.py,sha256=MJm9U75savKWKYm4dLREqlsyBWEqkTtaM4YTWkEeo0E,8388
|
|
4
|
+
md2conf/api.py,sha256=RQ_nb0Z0VnhJma1BU9ABeb4MQZvZEfFS5mTXXKcY6bk,37584
|
|
5
|
+
md2conf/application.py,sha256=cXYXYdEdmMXwhxF69eUiPPG2Ixt4xtlWHXa28wTq150,7571
|
|
6
|
+
md2conf/collection.py,sha256=EAXuIFcIRBO-Giic2hdU2d4Hpj0_ZFBAWI3aKQ2fjrI,775
|
|
7
|
+
md2conf/converter.py,sha256=x2LAY1Hpw5mTVFNJE5_Zm-o7p5y6TTds6KfrpdM5qQk,38823
|
|
8
|
+
md2conf/emoji.py,sha256=UzDrxqFo59wHmbbJmMNdn0rYFDXbZE4qirOM-_egzXc,2603
|
|
9
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
10
|
+
md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
|
|
11
|
+
md2conf/local.py,sha256=MVwGxy_n00uqCInLK8FVGaaVnaOp1nfn28PVrWz3cCQ,3496
|
|
12
|
+
md2conf/matcher.py,sha256=izgL_MAMqbXjKPvAz3KpFv5OTDsaJ9GplTJbixrT3oY,4918
|
|
13
|
+
md2conf/mermaid.py,sha256=f0x7ISj-41ZMh4zTAFPhIWwr94SDcsVZUc1NWqmH_G4,2508
|
|
14
|
+
md2conf/metadata.py,sha256=TxgUrskqsWor_pvlQx-p86C0-0qRJ2aeQhuDcXU9Dpc,886
|
|
15
|
+
md2conf/processor.py,sha256=yWVRYtbc9UHSUfRxqyPDsgeVqO7gx0s3RiGL1GzMotE,9405
|
|
16
|
+
md2conf/properties.py,sha256=RC1jY_TKVbOv2bJxXn27Fj4fNWzyoNUQt6ltgUyVQAQ,3987
|
|
17
|
+
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
18
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
md2conf/scanner.py,sha256=qXfnJkaEwDbz6G6Z9llqifBp2TLAlrXAIP4qkCbGdWo,4964
|
|
20
|
+
markdown_to_confluence-0.4.1.dist-info/METADATA,sha256=rAXtL2mR1LHmc_pwkmnwrGpIDMEw-7kZjIJOnMi-NLA,24864
|
|
21
|
+
markdown_to_confluence-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
markdown_to_confluence-0.4.1.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
23
|
+
markdown_to_confluence-0.4.1.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
24
|
+
markdown_to_confluence-0.4.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
25
|
+
markdown_to_confluence-0.4.1.dist-info/RECORD,,
|
md2conf/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ Parses Markdown files, converts Markdown content into the Confluence Storage For
|
|
|
5
5
|
Confluence API endpoints to upload images and content.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.4.
|
|
8
|
+
__version__ = "0.4.1"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/__main__.py
CHANGED
|
@@ -26,11 +26,7 @@ from .converter import ConfluenceDocumentOptions, ConfluencePageID
|
|
|
26
26
|
from .extra import override
|
|
27
27
|
from .local import LocalConverter
|
|
28
28
|
from .metadata import ConfluenceSiteMetadata
|
|
29
|
-
from .properties import
|
|
30
|
-
ArgumentError,
|
|
31
|
-
ConfluenceConnectionProperties,
|
|
32
|
-
ConfluenceSiteProperties,
|
|
33
|
-
)
|
|
29
|
+
from .properties import ArgumentError, ConfluenceConnectionProperties, ConfluenceSiteProperties
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
class Arguments(argparse.Namespace):
|
|
@@ -71,7 +67,7 @@ class KwargsAppendAction(argparse.Action):
|
|
|
71
67
|
raise argparse.ArgumentError(
|
|
72
68
|
self,
|
|
73
69
|
f'Could not parse argument "{values}". It should follow the format: k1=v1 k2=v2 ...',
|
|
74
|
-
)
|
|
70
|
+
) from None
|
|
75
71
|
setattr(namespace, self.dest, d)
|
|
76
72
|
|
|
77
73
|
|
|
@@ -79,13 +75,9 @@ def main() -> None:
|
|
|
79
75
|
parser = argparse.ArgumentParser()
|
|
80
76
|
parser.prog = os.path.basename(os.path.dirname(__file__))
|
|
81
77
|
parser.add_argument("--version", action="version", version=__version__)
|
|
82
|
-
parser.add_argument(
|
|
83
|
-
"mdpath", help="Path to Markdown file or directory to convert and publish."
|
|
84
|
-
)
|
|
78
|
+
parser.add_argument("mdpath", help="Path to Markdown file or directory to convert and publish.")
|
|
85
79
|
parser.add_argument("-d", "--domain", help="Confluence organization domain.")
|
|
86
|
-
parser.add_argument(
|
|
87
|
-
"-p", "--path", help="Base path for Confluence (default: '/wiki/')."
|
|
88
|
-
)
|
|
80
|
+
parser.add_argument("-p", "--path", help="Base path for Confluence (default: '/wiki/').")
|
|
89
81
|
parser.add_argument(
|
|
90
82
|
"--api-url",
|
|
91
83
|
dest="api_url",
|
md2conf/api.py
CHANGED
|
@@ -8,7 +8,6 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
10
|
import enum
|
|
11
|
-
import functools
|
|
12
11
|
import io
|
|
13
12
|
import logging
|
|
14
13
|
import mimetypes
|
|
@@ -16,26 +15,16 @@ import typing
|
|
|
16
15
|
from dataclasses import dataclass
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from types import TracebackType
|
|
19
|
-
from typing import Optional, TypeVar
|
|
18
|
+
from typing import Any, Optional, TypeVar
|
|
20
19
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
21
20
|
|
|
22
21
|
import requests
|
|
23
22
|
from strong_typing.core import JsonType
|
|
24
|
-
from strong_typing.serialization import
|
|
25
|
-
DeserializerOptions,
|
|
26
|
-
json_dump_string,
|
|
27
|
-
json_to_object,
|
|
28
|
-
object_to_json,
|
|
29
|
-
)
|
|
23
|
+
from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
|
|
30
24
|
|
|
31
25
|
from .converter import ParseError, sanitize_confluence
|
|
32
26
|
from .metadata import ConfluenceSiteMetadata
|
|
33
|
-
from .properties import
|
|
34
|
-
ArgumentError,
|
|
35
|
-
ConfluenceConnectionProperties,
|
|
36
|
-
ConfluenceError,
|
|
37
|
-
PageError,
|
|
38
|
-
)
|
|
27
|
+
from .properties import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
|
|
39
28
|
|
|
40
29
|
T = TypeVar("T")
|
|
41
30
|
|
|
@@ -106,6 +95,7 @@ class ConfluenceRepresentation(enum.Enum):
|
|
|
106
95
|
class ConfluenceStatus(enum.Enum):
|
|
107
96
|
CURRENT = "current"
|
|
108
97
|
DRAFT = "draft"
|
|
98
|
+
ARCHIVED = "archived"
|
|
109
99
|
|
|
110
100
|
|
|
111
101
|
@enum.unique
|
|
@@ -265,6 +255,41 @@ class ConfluenceIdentifiedLabel(ConfluenceLabel):
|
|
|
265
255
|
id: str
|
|
266
256
|
|
|
267
257
|
|
|
258
|
+
@dataclass(frozen=True)
|
|
259
|
+
class ConfluenceContentProperty:
|
|
260
|
+
"""
|
|
261
|
+
Represents a content property.
|
|
262
|
+
|
|
263
|
+
:param key: Property key.
|
|
264
|
+
:param value: Property value as JSON.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
key: str
|
|
268
|
+
value: JsonType
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class ConfluenceVersionedContentProperty(ConfluenceContentProperty):
|
|
273
|
+
"""
|
|
274
|
+
Represents a content property.
|
|
275
|
+
|
|
276
|
+
:param version: Version information about the property.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
version: ConfluenceContentVersion
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True)
|
|
283
|
+
class ConfluenceIdentifiedContentProperty(ConfluenceVersionedContentProperty):
|
|
284
|
+
"""
|
|
285
|
+
Represents a content property.
|
|
286
|
+
|
|
287
|
+
:param id: Property ID.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
id: str
|
|
291
|
+
|
|
292
|
+
|
|
268
293
|
@dataclass(frozen=True)
|
|
269
294
|
class ConfluenceCreatePageRequest:
|
|
270
295
|
spaceId: str
|
|
@@ -300,9 +325,7 @@ class ConfluenceAPI:
|
|
|
300
325
|
properties: ConfluenceConnectionProperties
|
|
301
326
|
session: Optional["ConfluenceSession"] = None
|
|
302
327
|
|
|
303
|
-
def __init__(
|
|
304
|
-
self, properties: Optional[ConfluenceConnectionProperties] = None
|
|
305
|
-
) -> None:
|
|
328
|
+
def __init__(self, properties: Optional[ConfluenceConnectionProperties] = None) -> None:
|
|
306
329
|
self.properties = properties or ConfluenceConnectionProperties()
|
|
307
330
|
|
|
308
331
|
def __enter__(self) -> "ConfluenceSession":
|
|
@@ -310,9 +333,7 @@ class ConfluenceAPI:
|
|
|
310
333
|
if self.properties.user_name:
|
|
311
334
|
session.auth = (self.properties.user_name, self.properties.api_key)
|
|
312
335
|
else:
|
|
313
|
-
session.headers.update(
|
|
314
|
-
{"Authorization": f"Bearer {self.properties.api_key}"}
|
|
315
|
-
)
|
|
336
|
+
session.headers.update({"Authorization": f"Bearer {self.properties.api_key}"})
|
|
316
337
|
|
|
317
338
|
if self.properties.headers:
|
|
318
339
|
session.headers.update(self.properties.headers)
|
|
@@ -366,9 +387,7 @@ class ConfluenceSession:
|
|
|
366
387
|
self.api_url = api_url
|
|
367
388
|
|
|
368
389
|
if not domain or not base_path:
|
|
369
|
-
payload = self._invoke(
|
|
370
|
-
ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"}
|
|
371
|
-
)
|
|
390
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"})
|
|
372
391
|
data = json_to_object(ConfluenceResultSet, payload)
|
|
373
392
|
base_url = data._links.base
|
|
374
393
|
|
|
@@ -377,13 +396,9 @@ class ConfluenceSession:
|
|
|
377
396
|
base_path = f"{base_path}/"
|
|
378
397
|
|
|
379
398
|
if not domain:
|
|
380
|
-
raise ArgumentError(
|
|
381
|
-
"Confluence domain not specified and cannot be inferred"
|
|
382
|
-
)
|
|
399
|
+
raise ArgumentError("Confluence domain not specified and cannot be inferred")
|
|
383
400
|
if not base_path:
|
|
384
|
-
raise ArgumentError(
|
|
385
|
-
"Confluence base path not specified and cannot be inferred"
|
|
386
|
-
)
|
|
401
|
+
raise ArgumentError("Confluence base path not specified and cannot be inferred")
|
|
387
402
|
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
388
403
|
if not api_url:
|
|
389
404
|
self.api_url = f"https://{self.site.domain}{self.site.base_path}"
|
|
@@ -425,9 +440,7 @@ class ConfluenceSession:
|
|
|
425
440
|
response.raise_for_status()
|
|
426
441
|
return typing.cast(JsonType, response.json())
|
|
427
442
|
|
|
428
|
-
def _fetch(
|
|
429
|
-
self, path: str, query: Optional[dict[str, str]] = None
|
|
430
|
-
) -> list[JsonType]:
|
|
443
|
+
def _fetch(self, path: str, query: Optional[dict[str, str]] = None) -> list[JsonType]:
|
|
431
444
|
"Retrieves all results of a REST API v2 paginated result-set."
|
|
432
445
|
|
|
433
446
|
items: list[JsonType] = []
|
|
@@ -506,9 +519,7 @@ class ConfluenceSession:
|
|
|
506
519
|
|
|
507
520
|
return id
|
|
508
521
|
|
|
509
|
-
def get_space_id(
|
|
510
|
-
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
511
|
-
) -> Optional[str]:
|
|
522
|
+
def get_space_id(self, *, space_id: Optional[str] = None, space_key: Optional[str] = None) -> Optional[str]:
|
|
512
523
|
"""
|
|
513
524
|
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
514
525
|
|
|
@@ -529,9 +540,7 @@ class ConfluenceSession:
|
|
|
529
540
|
# space ID and key are unset, and no default space is configured
|
|
530
541
|
return None
|
|
531
542
|
|
|
532
|
-
def get_attachment_by_name(
|
|
533
|
-
self, page_id: str, filename: str
|
|
534
|
-
) -> ConfluenceAttachment:
|
|
543
|
+
def get_attachment_by_name(self, page_id: str, filename: str) -> ConfluenceAttachment:
|
|
535
544
|
"""
|
|
536
545
|
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
537
546
|
"""
|
|
@@ -583,6 +592,9 @@ class ConfluenceSession:
|
|
|
583
592
|
name = attachment_name
|
|
584
593
|
content_type, _ = mimetypes.guess_type(name, strict=True)
|
|
585
594
|
|
|
595
|
+
if content_type is None:
|
|
596
|
+
content_type = "application/octet-stream"
|
|
597
|
+
|
|
586
598
|
if attachment_path is not None and not attachment_path.is_file():
|
|
587
599
|
raise PageError(f"file not found: {attachment_path}")
|
|
588
600
|
|
|
@@ -610,8 +622,13 @@ class ConfluenceSession:
|
|
|
610
622
|
|
|
611
623
|
if attachment_path is not None:
|
|
612
624
|
with open(attachment_path, "rb") as attachment_file:
|
|
613
|
-
file_to_upload = {
|
|
614
|
-
"comment":
|
|
625
|
+
file_to_upload: dict[str, tuple[Optional[str], Any, str, dict[str, str]]] = {
|
|
626
|
+
"comment": (
|
|
627
|
+
None,
|
|
628
|
+
comment,
|
|
629
|
+
"text/plain; charset=utf-8",
|
|
630
|
+
{},
|
|
631
|
+
),
|
|
615
632
|
"file": (
|
|
616
633
|
attachment_name, # will truncate path component
|
|
617
634
|
attachment_file,
|
|
@@ -622,7 +639,7 @@ class ConfluenceSession:
|
|
|
622
639
|
LOGGER.info("Uploading attachment: %s", attachment_name)
|
|
623
640
|
response = self.session.post(
|
|
624
641
|
url,
|
|
625
|
-
files=file_to_upload,
|
|
642
|
+
files=file_to_upload,
|
|
626
643
|
headers={
|
|
627
644
|
"X-Atlassian-Token": "no-check",
|
|
628
645
|
"Accept": "application/json",
|
|
@@ -634,17 +651,22 @@ class ConfluenceSession:
|
|
|
634
651
|
raw_file = io.BytesIO(raw_data)
|
|
635
652
|
raw_file.name = attachment_name
|
|
636
653
|
file_to_upload = {
|
|
637
|
-
"comment":
|
|
654
|
+
"comment": (
|
|
655
|
+
None,
|
|
656
|
+
comment,
|
|
657
|
+
"text/plain; charset=utf-8",
|
|
658
|
+
{},
|
|
659
|
+
),
|
|
638
660
|
"file": (
|
|
639
661
|
attachment_name, # will truncate path component
|
|
640
|
-
raw_file,
|
|
662
|
+
raw_file,
|
|
641
663
|
content_type,
|
|
642
664
|
{"Expires": "0"},
|
|
643
665
|
),
|
|
644
666
|
}
|
|
645
667
|
response = self.session.post(
|
|
646
668
|
url,
|
|
647
|
-
files=file_to_upload,
|
|
669
|
+
files=file_to_upload,
|
|
648
670
|
headers={
|
|
649
671
|
"X-Atlassian-Token": "no-check",
|
|
650
672
|
"Accept": "application/json",
|
|
@@ -667,9 +689,7 @@ class ConfluenceSession:
|
|
|
667
689
|
# ensure path component is retained in attachment name
|
|
668
690
|
self._update_attachment(page_id, attachment_id, version, attachment_name)
|
|
669
691
|
|
|
670
|
-
def _update_attachment(
|
|
671
|
-
self, page_id: str, attachment_id: str, version: int, attachment_title: str
|
|
672
|
-
) -> None:
|
|
692
|
+
def _update_attachment(self, page_id: str, attachment_id: str, version: int, attachment_title: str) -> None:
|
|
673
693
|
id = attachment_id.removeprefix("att")
|
|
674
694
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
675
695
|
request = ConfluenceUpdateAttachmentRequest(
|
|
@@ -730,7 +750,6 @@ class ConfluenceSession:
|
|
|
730
750
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
731
751
|
return _json_to_object(ConfluencePage, payload)
|
|
732
752
|
|
|
733
|
-
@functools.cache
|
|
734
753
|
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
735
754
|
"""
|
|
736
755
|
Retrieves Confluence wiki page details.
|
|
@@ -784,14 +803,8 @@ class ConfluenceSession:
|
|
|
784
803
|
id=page_id,
|
|
785
804
|
status=ConfluenceStatus.CURRENT,
|
|
786
805
|
title=new_title,
|
|
787
|
-
body=ConfluencePageBody(
|
|
788
|
-
|
|
789
|
-
representation=ConfluenceRepresentation.STORAGE, value=new_content
|
|
790
|
-
)
|
|
791
|
-
),
|
|
792
|
-
version=ConfluenceContentVersion(
|
|
793
|
-
number=page.version.number + 1, minorEdit=True
|
|
794
|
-
),
|
|
806
|
+
body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=new_content)),
|
|
807
|
+
version=ConfluenceContentVersion(number=page.version.number + 1, minorEdit=True),
|
|
795
808
|
)
|
|
796
809
|
LOGGER.info("Updating page: %s", page_id)
|
|
797
810
|
self._save(ConfluenceVersion.VERSION_2, path, object_to_json(request))
|
|
@@ -973,7 +986,7 @@ class ConfluenceSession:
|
|
|
973
986
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
974
987
|
response.raise_for_status()
|
|
975
988
|
|
|
976
|
-
def update_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
989
|
+
def update_labels(self, page_id: str, labels: list[ConfluenceLabel], *, keep_existing: bool = False) -> None:
|
|
977
990
|
"""
|
|
978
991
|
Assigns the specified labels to a Confluence page. Existing labels are removed.
|
|
979
992
|
|
|
@@ -982,10 +995,7 @@ class ConfluenceSession:
|
|
|
982
995
|
"""
|
|
983
996
|
|
|
984
997
|
new_labels = set(labels)
|
|
985
|
-
old_labels = set(
|
|
986
|
-
ConfluenceLabel(name=label.name, prefix=label.prefix)
|
|
987
|
-
for label in self.get_labels(page_id)
|
|
988
|
-
)
|
|
998
|
+
old_labels = set(ConfluenceLabel(name=label.name, prefix=label.prefix) for label in self.get_labels(page_id))
|
|
989
999
|
|
|
990
1000
|
add_labels = list(new_labels - old_labels)
|
|
991
1001
|
remove_labels = list(old_labels - new_labels)
|
|
@@ -993,6 +1003,123 @@ class ConfluenceSession:
|
|
|
993
1003
|
if add_labels:
|
|
994
1004
|
add_labels.sort()
|
|
995
1005
|
self.add_labels(page_id, add_labels)
|
|
996
|
-
if remove_labels:
|
|
1006
|
+
if not keep_existing and remove_labels:
|
|
997
1007
|
remove_labels.sort()
|
|
998
1008
|
self.remove_labels(page_id, remove_labels)
|
|
1009
|
+
|
|
1010
|
+
def get_content_properties_for_page(self, page_id: str) -> list[ConfluenceIdentifiedContentProperty]:
|
|
1011
|
+
"""
|
|
1012
|
+
Retrieves content properties for a Confluence page.
|
|
1013
|
+
|
|
1014
|
+
:param page_id: The Confluence page ID.
|
|
1015
|
+
:returns: A list of content properties.
|
|
1016
|
+
"""
|
|
1017
|
+
|
|
1018
|
+
path = f"/pages/{page_id}/properties"
|
|
1019
|
+
results = self._fetch(path)
|
|
1020
|
+
return _json_to_object(list[ConfluenceIdentifiedContentProperty], results)
|
|
1021
|
+
|
|
1022
|
+
def add_content_property_to_page(self, page_id: str, property: ConfluenceContentProperty) -> ConfluenceIdentifiedContentProperty:
|
|
1023
|
+
"""
|
|
1024
|
+
Adds a new content property to a Confluence page.
|
|
1025
|
+
|
|
1026
|
+
:param page_id: The Confluence page ID.
|
|
1027
|
+
:param property: Content property to add.
|
|
1028
|
+
"""
|
|
1029
|
+
|
|
1030
|
+
path = f"/pages/{page_id}/properties"
|
|
1031
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1032
|
+
response = self.session.post(
|
|
1033
|
+
url,
|
|
1034
|
+
data=json_dump_string(object_to_json(property)),
|
|
1035
|
+
headers={
|
|
1036
|
+
"Content-Type": "application/json",
|
|
1037
|
+
"Accept": "application/json",
|
|
1038
|
+
},
|
|
1039
|
+
)
|
|
1040
|
+
if response.text:
|
|
1041
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
1042
|
+
response.raise_for_status()
|
|
1043
|
+
return _json_to_object(ConfluenceIdentifiedContentProperty, response.json())
|
|
1044
|
+
|
|
1045
|
+
def remove_content_property_from_page(self, page_id: str, property_id: str) -> None:
|
|
1046
|
+
"""
|
|
1047
|
+
Removes a content property from a Confluence page.
|
|
1048
|
+
|
|
1049
|
+
:param page_id: The Confluence page ID.
|
|
1050
|
+
:param property_id: Property ID, which uniquely identifies the property.
|
|
1051
|
+
"""
|
|
1052
|
+
|
|
1053
|
+
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1054
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1055
|
+
response = self.session.delete(url)
|
|
1056
|
+
response.raise_for_status()
|
|
1057
|
+
|
|
1058
|
+
def update_content_property_for_page(
|
|
1059
|
+
self, page_id: str, property_id: str, version: int, property: ConfluenceContentProperty
|
|
1060
|
+
) -> ConfluenceIdentifiedContentProperty:
|
|
1061
|
+
"""
|
|
1062
|
+
Updates an existing content property associated with a Confluence page.
|
|
1063
|
+
|
|
1064
|
+
:param page_id: The Confluence page ID.
|
|
1065
|
+
:param property_id: Property ID, which uniquely identifies the property.
|
|
1066
|
+
:param version: Version number to assign.
|
|
1067
|
+
:param property: Content property data to assign.
|
|
1068
|
+
:returns: Updated content property data.
|
|
1069
|
+
"""
|
|
1070
|
+
|
|
1071
|
+
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1072
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1073
|
+
response = self.session.put(
|
|
1074
|
+
url,
|
|
1075
|
+
data=json_dump_string(
|
|
1076
|
+
object_to_json(
|
|
1077
|
+
ConfluenceVersionedContentProperty(
|
|
1078
|
+
key=property.key,
|
|
1079
|
+
value=property.value,
|
|
1080
|
+
version=ConfluenceContentVersion(number=version),
|
|
1081
|
+
)
|
|
1082
|
+
)
|
|
1083
|
+
),
|
|
1084
|
+
headers={"Content-Type": "application/json"},
|
|
1085
|
+
)
|
|
1086
|
+
if response.text:
|
|
1087
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
1088
|
+
response.raise_for_status()
|
|
1089
|
+
return json_to_object(ConfluenceIdentifiedContentProperty, response.json())
|
|
1090
|
+
|
|
1091
|
+
def update_content_properties_for_page(self, page_id: str, properties: list[ConfluenceContentProperty], *, keep_existing: bool = False) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
Updates content properties associated with a Confluence page.
|
|
1094
|
+
|
|
1095
|
+
:param page_id: The Confluence page ID.
|
|
1096
|
+
:param properties: A list of content property data to update.
|
|
1097
|
+
:param keep_existing: Whether to keep content property data whose key is not included in the list of properties passed as an argument.
|
|
1098
|
+
"""
|
|
1099
|
+
|
|
1100
|
+
old_mapping = {p.key: p for p in self.get_content_properties_for_page(page_id)}
|
|
1101
|
+
new_mapping = {p.key: p for p in properties}
|
|
1102
|
+
|
|
1103
|
+
new_props = set(p.key for p in properties)
|
|
1104
|
+
old_props = set(old_mapping.keys())
|
|
1105
|
+
|
|
1106
|
+
add_props = list(new_props - old_props)
|
|
1107
|
+
remove_props = list(old_props - new_props)
|
|
1108
|
+
update_props = list(old_props & new_props)
|
|
1109
|
+
|
|
1110
|
+
if add_props:
|
|
1111
|
+
add_props.sort()
|
|
1112
|
+
for key in add_props:
|
|
1113
|
+
self.add_content_property_to_page(page_id, new_mapping[key])
|
|
1114
|
+
if not keep_existing and remove_props:
|
|
1115
|
+
remove_props.sort()
|
|
1116
|
+
for key in remove_props:
|
|
1117
|
+
self.remove_content_property_from_page(page_id, old_mapping[key].id)
|
|
1118
|
+
if update_props:
|
|
1119
|
+
update_props.sort()
|
|
1120
|
+
for key in update_props:
|
|
1121
|
+
old_prop = old_mapping[key]
|
|
1122
|
+
new_prop = new_mapping[key]
|
|
1123
|
+
if old_prop.value == new_prop.value:
|
|
1124
|
+
continue
|
|
1125
|
+
self.update_content_property_for_page(page_id, old_prop.id, old_prop.version.number + 1, new_prop)
|