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.
Files changed (19) hide show
  1. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/PKG-INFO +2 -1
  2. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/PKG-INFO +2 -1
  3. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/__init__.py +1 -1
  4. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/__main__.py +31 -2
  5. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/api.py +23 -4
  6. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/application.py +5 -3
  7. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/converter.py +24 -4
  8. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/setup.cfg +1 -0
  9. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/tests/test_api.py +7 -3
  10. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/LICENSE +0 -0
  11. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/README.md +0 -0
  12. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/SOURCES.txt +0 -0
  13. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
  14. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/requires.txt +0 -0
  15. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/top_level.txt +0 -0
  16. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/markdown_to_confluence.egg-info/zip-safe +0 -0
  17. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/md2conf/py.typed +0 -0
  18. {markdown-to-confluence-0.1.5 → markdown-to-confluence-0.1.6}/pyproject.toml +0 -0
  19. {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.5
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.5
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.5"
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
- with ConfluenceAPI(args.domain, args.username, args.apikey, args.space) as api:
59
- synchronize_page(api, args.mdfile)
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.removeprefix("att")
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 = attachment_id.removeprefix("att")
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
- pass
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(api: ConfluenceSession, path: str) -> None:
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.root = elements_from_strings(
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)
@@ -25,6 +25,7 @@ classifiers =
25
25
  zip_safe = True
26
26
  include_package_data = True
27
27
  packages = find:
28
+ python_requires = >=3.8
28
29
  install_requires =
29
30
  lxml >= 4.9
30
31
  markdown >= 3.4
@@ -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 ConfluenceDocument, sanitize_confluence
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__":