markdown-to-confluence 0.1.5__tar.gz → 0.1.6__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.
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/PKG-INFO +2 -1
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/PKG-INFO +2 -1
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/__init__.py +1 -1
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/__main__.py +31 -2
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/api.py +23 -4
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/application.py +5 -3
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/converter.py +24 -4
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/setup.cfg +1 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/tests/test_api.py +7 -3
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/LICENSE +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/README.md +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/SOURCES.txt +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/requires.txt +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/top_level.txt +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/zip-safe +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/py.typed +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/pyproject.toml +0 -0
- {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.8
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
License-File: LICENSE
|
|
22
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.8
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
License-File: LICENSE
|
|
22
23
|
|
|
@@ -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.1.
|
|
8
|
+
__version__ = "0.1.6"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2023, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import logging
|
|
3
3
|
import os.path
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import requests
|
|
4
7
|
|
|
5
8
|
from .api import ConfluenceAPI
|
|
6
9
|
from .application import synchronize_page
|
|
10
|
+
from .converter import ConfluenceDocumentOptions
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
class Arguments(argparse.Namespace):
|
|
@@ -13,6 +17,7 @@ class Arguments(argparse.Namespace):
|
|
|
13
17
|
apikey: str
|
|
14
18
|
space: str
|
|
15
19
|
loglevel: str
|
|
20
|
+
generated_by: bool
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
parser = argparse.ArgumentParser()
|
|
@@ -46,6 +51,18 @@ parser.add_argument(
|
|
|
46
51
|
default=logging.getLevelName(logging.INFO),
|
|
47
52
|
help="Use this option to set the log verbosity.",
|
|
48
53
|
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--generated-by",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Add 'generated by a tool' prompt to pages.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--no-generated-by",
|
|
61
|
+
dest="generated_by",
|
|
62
|
+
action="store_false",
|
|
63
|
+
help="Do not add 'generated by a tool' prompt to pages.",
|
|
64
|
+
)
|
|
65
|
+
parser.set_defaults(generated_by=True)
|
|
49
66
|
|
|
50
67
|
args = Arguments()
|
|
51
68
|
parser.parse_args(namespace=args)
|
|
@@ -55,5 +72,17 @@ logging.basicConfig(
|
|
|
55
72
|
format="%(asctime)s - %(levelname)s - %(funcName)s [%(lineno)d] - %(message)s",
|
|
56
73
|
)
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
try:
|
|
76
|
+
with ConfluenceAPI(args.domain, args.username, args.apikey, args.space) as api:
|
|
77
|
+
synchronize_page(api, args.mdfile, ConfluenceDocumentOptions(args.generated_by))
|
|
78
|
+
except requests.exceptions.HTTPError as err:
|
|
79
|
+
logging.error(err)
|
|
80
|
+
|
|
81
|
+
# print details for a response with JSON body
|
|
82
|
+
try:
|
|
83
|
+
response: requests.Response = err.response
|
|
84
|
+
logging.error(response.json())
|
|
85
|
+
except requests.exceptions.JSONDecodeError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
sys.exit(1)
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import mimetypes
|
|
4
4
|
import os
|
|
5
5
|
import os.path
|
|
6
|
+
import sys
|
|
6
7
|
import typing
|
|
7
8
|
from contextlib import contextmanager
|
|
8
9
|
from dataclasses import dataclass
|
|
@@ -42,6 +43,24 @@ def build_url(base_url: str, query: Optional[Dict[str, str]] = None) -> str:
|
|
|
42
43
|
return urlunparse(url_parts)
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
if sys.version_info >= (3, 9):
|
|
47
|
+
|
|
48
|
+
def removeprefix(string: str, prefix: str) -> str:
|
|
49
|
+
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
50
|
+
|
|
51
|
+
return string.removeprefix(prefix)
|
|
52
|
+
|
|
53
|
+
else:
|
|
54
|
+
|
|
55
|
+
def removeprefix(string: str, prefix: str) -> str:
|
|
56
|
+
"If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
|
|
57
|
+
|
|
58
|
+
if string.startswith(prefix):
|
|
59
|
+
return string[len(prefix) :]
|
|
60
|
+
else:
|
|
61
|
+
return string
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
LOGGER = logging.getLogger(__name__)
|
|
46
65
|
|
|
47
66
|
|
|
@@ -200,7 +219,7 @@ class ConfluenceSession:
|
|
|
200
219
|
LOGGER.info("Up-to-date attachment: %s", attachment_name)
|
|
201
220
|
return
|
|
202
221
|
|
|
203
|
-
id = attachment.id
|
|
222
|
+
id = removeprefix(attachment.id, "att")
|
|
204
223
|
path = f"/content/{page_id}/child/attachment/{id}/data"
|
|
205
224
|
|
|
206
225
|
except ConfluenceError:
|
|
@@ -242,7 +261,7 @@ class ConfluenceSession:
|
|
|
242
261
|
def _update_attachment(
|
|
243
262
|
self, page_id: str, attachment_id: str, version: int, attachment_title: str
|
|
244
263
|
) -> None:
|
|
245
|
-
id =
|
|
264
|
+
id = removeprefix(attachment_id, "att")
|
|
246
265
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
247
266
|
data = {
|
|
248
267
|
"id": attachment_id,
|
|
@@ -313,8 +332,8 @@ class ConfluenceSession:
|
|
|
313
332
|
if old_content == new_content:
|
|
314
333
|
LOGGER.info("Up-to-date page: %s", page_id)
|
|
315
334
|
return
|
|
316
|
-
except ParseError:
|
|
317
|
-
|
|
335
|
+
except ParseError as exc:
|
|
336
|
+
LOGGER.warning(exc)
|
|
318
337
|
|
|
319
338
|
path = f"/content/{page_id}"
|
|
320
339
|
data = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os.path
|
|
2
2
|
|
|
3
3
|
from .api import ConfluenceSession
|
|
4
|
-
from .converter import ConfluenceDocument
|
|
4
|
+
from .converter import ConfluenceDocument, ConfluenceDocumentOptions
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def update_document(
|
|
@@ -15,11 +15,13 @@ def update_document(
|
|
|
15
15
|
api.update_page(document.page_id, document.xhtml())
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def synchronize_page(
|
|
18
|
+
def synchronize_page(
|
|
19
|
+
api: ConfluenceSession, path: str, options: ConfluenceDocumentOptions
|
|
20
|
+
) -> None:
|
|
19
21
|
page_path = os.path.abspath(path)
|
|
20
22
|
base_path = os.path.dirname(page_path)
|
|
21
23
|
|
|
22
|
-
document = ConfluenceDocument(path)
|
|
24
|
+
document = ConfluenceDocument(path, options)
|
|
23
25
|
|
|
24
26
|
if document.space_key:
|
|
25
27
|
with api.switch_space(document.space_key):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os.path
|
|
2
2
|
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from typing import List, Optional, Tuple
|
|
4
5
|
from urllib.parse import urlparse
|
|
5
6
|
|
|
@@ -229,15 +230,29 @@ def _extract_value(pattern: str, string: str) -> Tuple[Optional[str], str]:
|
|
|
229
230
|
return value, string
|
|
230
231
|
|
|
231
232
|
|
|
233
|
+
@dataclass
|
|
234
|
+
class ConfluenceDocumentOptions:
|
|
235
|
+
"""
|
|
236
|
+
Options that control the generated page content.
|
|
237
|
+
|
|
238
|
+
:param show_generated: Whether to display a prompt "This page has been generated with a tool."
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
generated_by: bool = True
|
|
242
|
+
|
|
243
|
+
|
|
232
244
|
class ConfluenceDocument:
|
|
233
245
|
page_id: str
|
|
234
246
|
space_key: Optional[str] = None
|
|
235
247
|
links: List[str]
|
|
236
248
|
images: List[str]
|
|
237
249
|
|
|
250
|
+
options: ConfluenceDocumentOptions
|
|
238
251
|
root: ET.Element
|
|
239
252
|
|
|
240
|
-
def __init__(self, path: str) -> None:
|
|
253
|
+
def __init__(self, path: str, options: ConfluenceDocumentOptions) -> None:
|
|
254
|
+
self.options = options
|
|
255
|
+
|
|
241
256
|
path = os.path.abspath(path)
|
|
242
257
|
|
|
243
258
|
with open(path, "r") as f:
|
|
@@ -259,14 +274,16 @@ class ConfluenceDocument:
|
|
|
259
274
|
)
|
|
260
275
|
|
|
261
276
|
# parse Markdown document
|
|
262
|
-
self.
|
|
263
|
-
[
|
|
277
|
+
if self.options.generated_by:
|
|
278
|
+
content = [
|
|
264
279
|
'<ac:structured-macro ac:name="info" ac:schema-version="1">',
|
|
265
280
|
"<ac:rich-text-body><p>This page has been generated with a tool.</p></ac:rich-text-body>",
|
|
266
281
|
"</ac:structured-macro>",
|
|
267
282
|
html,
|
|
268
283
|
]
|
|
269
|
-
|
|
284
|
+
else:
|
|
285
|
+
content = [html]
|
|
286
|
+
self.root = elements_from_strings(content)
|
|
270
287
|
|
|
271
288
|
converter = ConfluenceStorageFormatConverter(os.path.dirname(path))
|
|
272
289
|
converter.visit(self.root)
|
|
@@ -280,6 +297,9 @@ class ConfluenceDocument:
|
|
|
280
297
|
def sanitize_confluence(html: str) -> str:
|
|
281
298
|
"Generates a sanitized version of a Confluence storage format XHTML document with no volatile attributes."
|
|
282
299
|
|
|
300
|
+
if not html:
|
|
301
|
+
return ""
|
|
302
|
+
|
|
283
303
|
root = elements_from_strings([html])
|
|
284
304
|
ConfluenceStorageFormatCleaner().visit(root)
|
|
285
305
|
return _content_to_string(root)
|
|
@@ -5,7 +5,11 @@ import unittest
|
|
|
5
5
|
|
|
6
6
|
from md2conf.api import ConfluenceAPI, ConfluenceAttachment, ConfluencePage
|
|
7
7
|
from md2conf.application import synchronize_page
|
|
8
|
-
from md2conf.converter import
|
|
8
|
+
from md2conf.converter import (
|
|
9
|
+
ConfluenceDocument,
|
|
10
|
+
ConfluenceDocumentOptions,
|
|
11
|
+
sanitize_confluence,
|
|
12
|
+
)
|
|
9
13
|
|
|
10
14
|
logging.basicConfig(
|
|
11
15
|
level=logging.INFO,
|
|
@@ -15,7 +19,7 @@ logging.basicConfig(
|
|
|
15
19
|
|
|
16
20
|
class TestAPI(unittest.TestCase):
|
|
17
21
|
def test_markdown(self) -> None:
|
|
18
|
-
document = ConfluenceDocument("example.md")
|
|
22
|
+
document = ConfluenceDocument("example.md", ConfluenceDocumentOptions())
|
|
19
23
|
self.assertListEqual(document.links, [])
|
|
20
24
|
self.assertListEqual(
|
|
21
25
|
document.images,
|
|
@@ -62,7 +66,7 @@ class TestAPI(unittest.TestCase):
|
|
|
62
66
|
|
|
63
67
|
def test_synchronize_page(self) -> None:
|
|
64
68
|
with ConfluenceAPI() as api:
|
|
65
|
-
synchronize_page(api, "example.md")
|
|
69
|
+
synchronize_page(api, "example.md", ConfluenceDocumentOptions())
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|