markdown-to-confluence 0.3.1__tar.gz → 0.3.3__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 (30) hide show
  1. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/PKG-INFO +25 -7
  2. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/README.md +22 -5
  3. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/PKG-INFO +25 -7
  4. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/__init__.py +1 -1
  5. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/__main__.py +36 -11
  6. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/api.py +48 -18
  7. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/application.py +34 -18
  8. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/converter.py +115 -26
  9. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/emoji.py +3 -1
  10. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/mermaid.py +1 -1
  11. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/processor.py +11 -10
  12. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/properties.py +40 -16
  13. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_conversion.py +50 -15
  14. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_matcher.py +1 -0
  15. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_mermaid.py +3 -3
  16. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_processor.py +5 -10
  17. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/LICENSE +0 -0
  18. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/SOURCES.txt +0 -0
  19. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
  20. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/entry_points.txt +0 -0
  21. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/requires.txt +0 -0
  22. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/top_level.txt +0 -0
  23. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/zip-safe +0 -0
  24. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/entities.dtd +0 -0
  25. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/matcher.py +0 -0
  26. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/puppeteer-config.json +0 -0
  27. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/py.typed +0 -0
  28. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/pyproject.toml +0 -0
  29. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/setup.cfg +0 -0
  30. {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: markdown-to-confluence
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -30,6 +30,7 @@ Requires-Dist: pyyaml>=6.0
30
30
  Requires-Dist: types-PyYAML>=6.0
31
31
  Requires-Dist: requests>=2.32
32
32
  Requires-Dist: types-requests>=2.32
33
+ Dynamic: license-file
33
34
 
34
35
  # Publish Markdown files to Confluence wiki
35
36
 
@@ -166,7 +167,7 @@ The concepts above are illustrated in the following sections.
166
167
 
167
168
  #### File-system directory hierarchy
168
169
 
169
- The title of each Markdown file (either the text of the first heading (`#`), or the title specified in front-matter) is shown next to the file name.
170
+ The title of each Markdown file (either the text of the topmost unique heading (`#`), or the title specified in front-matter) is shown next to the file name.
170
171
 
171
172
  ```
172
173
  .
@@ -197,12 +198,30 @@ root
197
198
  └── Mean vs. median
198
199
  ```
199
200
 
201
+ ### Publishing images
202
+
203
+ Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
204
+
205
+ Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
206
+
207
+ External images referenced with an absolute URL retain the original URL.
208
+
200
209
  ### Ignoring files
201
210
 
202
211
  Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
203
212
 
204
213
  Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
205
214
 
215
+ ### Page title
216
+
217
+ *md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
218
+
219
+ 1. The `title` attribute set in the [front-matter](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/). Front-matter is a block delimited by `---` at the beginning of a Markdown document. Currently, only YAML syntax is supported.
220
+ 2. The text of the topmost unique Markdown heading (`#`). For example, if a document has a single first-level heading (e.g. `# My document`), its text is used. However, if there are multiple first-level headings, this step is skipped.
221
+ 3. The file name (without the extension `.md`).
222
+
223
+ If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
224
+
206
225
  ### Running the tool
207
226
 
208
227
  You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
@@ -215,10 +234,8 @@ Use the `--help` switch to get a full list of supported command-line options:
215
234
 
216
235
  ```console
217
236
  $ python3 -m md2conf --help
218
- usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE]
219
- [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--generated-by GENERATED_BY] [--no-generated-by]
220
- [--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors]
221
- [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
237
+ usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--keep-hierarchy] [--generated-by GENERATED_BY] [--no-generated-by]
238
+ [--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors] [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
222
239
  mdpath
223
240
 
224
241
  positional arguments:
@@ -239,6 +256,7 @@ options:
239
256
  -l {debug,info,warning,error,critical}, --loglevel {debug,info,warning,error,critical}
240
257
  Use this option to set the log verbosity.
241
258
  -r ROOT_PAGE Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.
259
+ --keep-hierarchy Maintain source directory structure when exporting to Confluence.
242
260
  --generated-by GENERATED_BY
243
261
  Add prompt to pages (default: 'This page has been generated with a tool.').
244
262
  --no-generated-by Do not add 'generated by a tool' prompt to pages.
@@ -133,7 +133,7 @@ The concepts above are illustrated in the following sections.
133
133
 
134
134
  #### File-system directory hierarchy
135
135
 
136
- The title of each Markdown file (either the text of the first heading (`#`), or the title specified in front-matter) is shown next to the file name.
136
+ The title of each Markdown file (either the text of the topmost unique heading (`#`), or the title specified in front-matter) is shown next to the file name.
137
137
 
138
138
  ```
139
139
  .
@@ -164,12 +164,30 @@ root
164
164
  └── Mean vs. median
165
165
  ```
166
166
 
167
+ ### Publishing images
168
+
169
+ Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
170
+
171
+ Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
172
+
173
+ External images referenced with an absolute URL retain the original URL.
174
+
167
175
  ### Ignoring files
168
176
 
169
177
  Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
170
178
 
171
179
  Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
172
180
 
181
+ ### Page title
182
+
183
+ *md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
184
+
185
+ 1. The `title` attribute set in the [front-matter](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/). Front-matter is a block delimited by `---` at the beginning of a Markdown document. Currently, only YAML syntax is supported.
186
+ 2. The text of the topmost unique Markdown heading (`#`). For example, if a document has a single first-level heading (e.g. `# My document`), its text is used. However, if there are multiple first-level headings, this step is skipped.
187
+ 3. The file name (without the extension `.md`).
188
+
189
+ If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
190
+
173
191
  ### Running the tool
174
192
 
175
193
  You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
@@ -182,10 +200,8 @@ Use the `--help` switch to get a full list of supported command-line options:
182
200
 
183
201
  ```console
184
202
  $ python3 -m md2conf --help
185
- usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE]
186
- [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--generated-by GENERATED_BY] [--no-generated-by]
187
- [--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors]
188
- [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
203
+ usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--keep-hierarchy] [--generated-by GENERATED_BY] [--no-generated-by]
204
+ [--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors] [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
189
205
  mdpath
190
206
 
191
207
  positional arguments:
@@ -206,6 +222,7 @@ options:
206
222
  -l {debug,info,warning,error,critical}, --loglevel {debug,info,warning,error,critical}
207
223
  Use this option to set the log verbosity.
208
224
  -r ROOT_PAGE Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.
225
+ --keep-hierarchy Maintain source directory structure when exporting to Confluence.
209
226
  --generated-by GENERATED_BY
210
227
  Add prompt to pages (default: 'This page has been generated with a tool.').
211
228
  --no-generated-by Do not add 'generated by a tool' prompt to pages.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: markdown-to-confluence
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -30,6 +30,7 @@ Requires-Dist: pyyaml>=6.0
30
30
  Requires-Dist: types-PyYAML>=6.0
31
31
  Requires-Dist: requests>=2.32
32
32
  Requires-Dist: types-requests>=2.32
33
+ Dynamic: license-file
33
34
 
34
35
  # Publish Markdown files to Confluence wiki
35
36
 
@@ -166,7 +167,7 @@ The concepts above are illustrated in the following sections.
166
167
 
167
168
  #### File-system directory hierarchy
168
169
 
169
- The title of each Markdown file (either the text of the first heading (`#`), or the title specified in front-matter) is shown next to the file name.
170
+ The title of each Markdown file (either the text of the topmost unique heading (`#`), or the title specified in front-matter) is shown next to the file name.
170
171
 
171
172
  ```
172
173
  .
@@ -197,12 +198,30 @@ root
197
198
  └── Mean vs. median
198
199
  ```
199
200
 
201
+ ### Publishing images
202
+
203
+ Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
204
+
205
+ Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
206
+
207
+ External images referenced with an absolute URL retain the original URL.
208
+
200
209
  ### Ignoring files
201
210
 
202
211
  Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
203
212
 
204
213
  Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
205
214
 
215
+ ### Page title
216
+
217
+ *md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
218
+
219
+ 1. The `title` attribute set in the [front-matter](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/). Front-matter is a block delimited by `---` at the beginning of a Markdown document. Currently, only YAML syntax is supported.
220
+ 2. The text of the topmost unique Markdown heading (`#`). For example, if a document has a single first-level heading (e.g. `# My document`), its text is used. However, if there are multiple first-level headings, this step is skipped.
221
+ 3. The file name (without the extension `.md`).
222
+
223
+ If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
224
+
206
225
  ### Running the tool
207
226
 
208
227
  You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
@@ -215,10 +234,8 @@ Use the `--help` switch to get a full list of supported command-line options:
215
234
 
216
235
  ```console
217
236
  $ python3 -m md2conf --help
218
- usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE]
219
- [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--generated-by GENERATED_BY] [--no-generated-by]
220
- [--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors]
221
- [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
237
+ usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--keep-hierarchy] [--generated-by GENERATED_BY] [--no-generated-by]
238
+ [--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors] [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
222
239
  mdpath
223
240
 
224
241
  positional arguments:
@@ -239,6 +256,7 @@ options:
239
256
  -l {debug,info,warning,error,critical}, --loglevel {debug,info,warning,error,critical}
240
257
  Use this option to set the log verbosity.
241
258
  -r ROOT_PAGE Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.
259
+ --keep-hierarchy Maintain source directory structure when exporting to Confluence.
242
260
  --generated-by GENERATED_BY
243
261
  Add prompt to pages (default: 'This page has been generated with a tool.').
244
262
  --no-generated-by Do not add 'generated by a tool' prompt to pages.
@@ -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.3.1"
8
+ __version__ = "0.3.3"
9
9
  __author__ = "Levente Hunyadi"
10
10
  __copyright__ = "Copyright 2022-2025, Levente Hunyadi"
11
11
  __license__ = "MIT"
@@ -22,18 +22,22 @@ import requests
22
22
  from . import __version__
23
23
  from .api import ConfluenceAPI
24
24
  from .application import Application
25
- from .converter import ConfluenceDocumentOptions
25
+ from .converter import ConfluenceDocumentOptions, ConfluenceSiteMetadata
26
26
  from .processor import Processor
27
- from .properties import ConfluenceProperties
27
+ from .properties import (
28
+ ArgumentError,
29
+ ConfluenceConnectionProperties,
30
+ ConfluenceSiteProperties,
31
+ )
28
32
 
29
33
 
30
34
  class Arguments(argparse.Namespace):
31
35
  mdpath: Path
32
- domain: str
33
- path: str
34
- username: str
35
- apikey: str
36
- space: str
36
+ domain: Optional[str]
37
+ path: Optional[str]
38
+ username: Optional[str]
39
+ apikey: Optional[str]
40
+ space: Optional[str]
37
41
  loglevel: str
38
42
  ignore_invalid_url: bool
39
43
  heading_anchors: bool
@@ -201,12 +205,33 @@ def main() -> None:
201
205
  diagram_output_format=args.diagram_output_format,
202
206
  webui_links=args.webui_links,
203
207
  )
204
- properties = ConfluenceProperties(
205
- args.domain, args.path, args.username, args.apikey, args.space, args.headers
206
- )
207
208
  if args.local:
208
- Processor(options, properties).process(args.mdpath)
209
+ try:
210
+ site_properties = ConfluenceSiteProperties(
211
+ domain=args.domain,
212
+ base_path=args.path,
213
+ space_key=args.space,
214
+ )
215
+ except ArgumentError as e:
216
+ parser.error(str(e))
217
+ site_metadata = ConfluenceSiteMetadata(
218
+ domain=site_properties.domain,
219
+ base_path=site_properties.base_path,
220
+ space_key=site_properties.space_key,
221
+ )
222
+ Processor(options, site_metadata).process(args.mdpath)
209
223
  else:
224
+ try:
225
+ properties = ConfluenceConnectionProperties(
226
+ args.domain,
227
+ args.path,
228
+ args.username,
229
+ args.apikey,
230
+ args.space,
231
+ args.headers,
232
+ )
233
+ except ArgumentError as e:
234
+ parser.error(str(e))
210
235
  try:
211
236
  with ConfluenceAPI(properties) as api:
212
237
  Application(
@@ -21,7 +21,12 @@ from urllib.parse import urlencode, urlparse, urlunparse
21
21
  import requests
22
22
 
23
23
  from .converter import ParseError, sanitize_confluence
24
- from .properties import ConfluenceError, ConfluenceProperties
24
+ from .properties import (
25
+ ArgumentError,
26
+ ConfluenceConnectionProperties,
27
+ ConfluenceError,
28
+ PageError,
29
+ )
25
30
 
26
31
  # a JSON type with possible `null` values
27
32
  JsonType = Union[
@@ -40,6 +45,18 @@ class ConfluenceVersion(enum.Enum):
40
45
  VERSION_2 = "api/v2"
41
46
 
42
47
 
48
+ class ConfluencePageParentContentType(enum.Enum):
49
+ """
50
+ Content types that can be a parent to a Confluence page
51
+ """
52
+
53
+ PAGE = "page"
54
+ WHITEBOARD = "whiteboard"
55
+ DATABASE = "database"
56
+ EMBED = "embed"
57
+ FOLDER = "folder"
58
+
59
+
43
60
  def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
44
61
  "Builds a URL with scheme, host, port, path and query string parameters."
45
62
 
@@ -71,17 +88,21 @@ class ConfluenceAttachment:
71
88
  class ConfluencePage:
72
89
  id: str
73
90
  space_id: str
91
+ parent_id: str
92
+ parent_type: Optional[ConfluencePageParentContentType]
74
93
  title: str
75
94
  version: int
76
95
  content: str
77
96
 
78
97
 
79
98
  class ConfluenceAPI:
80
- properties: ConfluenceProperties
99
+ properties: ConfluenceConnectionProperties
81
100
  session: Optional["ConfluenceSession"] = None
82
101
 
83
- def __init__(self, properties: Optional[ConfluenceProperties] = None) -> None:
84
- self.properties = properties or ConfluenceProperties()
102
+ def __init__(
103
+ self, properties: Optional[ConfluenceConnectionProperties] = None
104
+ ) -> None:
105
+ self.properties = properties or ConfluenceConnectionProperties()
85
106
 
86
107
  def __enter__(self) -> "ConfluenceSession":
87
108
  session = requests.Session()
@@ -128,7 +149,7 @@ class ConfluenceSession:
128
149
  session: requests.Session,
129
150
  domain: str,
130
151
  base_path: str,
131
- space_key: Optional[str],
152
+ space_key: Optional[str] = None,
132
153
  ) -> None:
133
154
  self.session = session
134
155
  self.domain = domain
@@ -170,11 +191,9 @@ class ConfluenceSession:
170
191
 
171
192
  url = self._build_url(version, path, query)
172
193
  response = self.session.get(url)
173
- response.raise_for_status()
174
- if len(response.text) > 240:
175
- LOGGER.debug("Received HTTP payload (truncated):\n%.240s...", response.text)
176
- else:
194
+ if response.text:
177
195
  LOGGER.debug("Received HTTP payload:\n%s", response.text)
196
+ response.raise_for_status()
178
197
  return response.json()
179
198
 
180
199
  def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
@@ -184,6 +203,8 @@ class ConfluenceSession:
184
203
  data=json.dumps(data),
185
204
  headers={"Content-Type": "application/json"},
186
205
  )
206
+ if response.text:
207
+ LOGGER.debug("Received HTTP payload:\n%s", response.text)
187
208
  response.raise_for_status()
188
209
 
189
210
  def space_id_to_key(self, id: str) -> str:
@@ -194,7 +215,7 @@ class ConfluenceSession:
194
215
  payload = self._invoke(
195
216
  ConfluenceVersion.VERSION_2,
196
217
  "/spaces",
197
- {"ids": id, "type": "global", "status": "current"},
218
+ {"ids": id, "status": "current"},
198
219
  )
199
220
  payload = typing.cast(dict[str, JsonType], payload)
200
221
  results = typing.cast(list[JsonType], payload["results"])
@@ -216,7 +237,7 @@ class ConfluenceSession:
216
237
  payload = self._invoke(
217
238
  ConfluenceVersion.VERSION_2,
218
239
  "/spaces",
219
- {"keys": key, "type": "global", "status": "current"},
240
+ {"keys": key, "status": "current"},
220
241
  )
221
242
  payload = typing.cast(dict[str, JsonType], payload)
222
243
  results = typing.cast(list[JsonType], payload["results"])
@@ -261,12 +282,11 @@ class ConfluenceSession:
261
282
  comment: Optional[str] = None,
262
283
  force: bool = False,
263
284
  ) -> None:
264
-
265
285
  if attachment_path is None and raw_data is None:
266
- raise ConfluenceError("required: `attachment_path` or `raw_data`")
286
+ raise ArgumentError("required: `attachment_path` or `raw_data`")
267
287
 
268
288
  if attachment_path is not None and raw_data is not None:
269
- raise ConfluenceError("expected: either `attachment_path` or `raw_data`")
289
+ raise ArgumentError("expected: either `attachment_path` or `raw_data`")
270
290
 
271
291
  if content_type is None:
272
292
  if attachment_path is not None:
@@ -276,7 +296,7 @@ class ConfluenceSession:
276
296
  content_type, _ = mimetypes.guess_type(name, strict=True)
277
297
 
278
298
  if attachment_path is not None and not attachment_path.is_file():
279
- raise ConfluenceError(f"file not found: {attachment_path}")
299
+ raise PageError(f"file not found: {attachment_path}")
280
300
 
281
301
  try:
282
302
  attachment = self.get_attachment_by_name(page_id, attachment_name)
@@ -422,6 +442,12 @@ class ConfluenceSession:
422
442
  return ConfluencePage(
423
443
  id=page_id,
424
444
  space_id=typing.cast(str, data["spaceId"]),
445
+ parent_id=typing.cast(str, data["parentId"]),
446
+ parent_type=(
447
+ ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
448
+ if data["parentType"] is not None
449
+ else None
450
+ ),
425
451
  title=typing.cast(str, data["title"]),
426
452
  version=typing.cast(int, version["number"]),
427
453
  content=typing.cast(str, storage["value"]),
@@ -493,9 +519,7 @@ class ConfluenceSession:
493
519
 
494
520
  coalesced_space_key = space_key or self.space_key
495
521
  if coalesced_space_key is None:
496
- raise ConfluenceError(
497
- "Confluence space key required for creating a new page"
498
- )
522
+ raise ArgumentError("Confluence space key required for creating a new page")
499
523
 
500
524
  path = "/pages/"
501
525
  query = {
@@ -524,6 +548,12 @@ class ConfluenceSession:
524
548
  return ConfluencePage(
525
549
  id=typing.cast(str, data["id"]),
526
550
  space_id=typing.cast(str, data["spaceId"]),
551
+ parent_id=typing.cast(str, data["parentId"]),
552
+ parent_type=(
553
+ ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
554
+ if data["parentType"] is not None
555
+ else None
556
+ ),
527
557
  title=typing.cast(str, data["title"]),
528
558
  version=typing.cast(int, version["number"]),
529
559
  content=typing.cast(str, storage["value"]),
@@ -6,8 +6,9 @@ Copyright 2022-2025, Levente Hunyadi
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
8
8
 
9
+ import hashlib
9
10
  import logging
10
- import os.path
11
+ import os
11
12
  from pathlib import Path
12
13
  from typing import Optional
13
14
 
@@ -17,12 +18,14 @@ from .converter import (
17
18
  ConfluenceDocumentOptions,
18
19
  ConfluencePageMetadata,
19
20
  ConfluenceQualifiedID,
21
+ ConfluenceSiteMetadata,
20
22
  attachment_name,
21
23
  extract_frontmatter_title,
22
24
  extract_qualified_id,
23
25
  read_qualified_id,
24
26
  )
25
27
  from .matcher import Matcher, MatcherOptions
28
+ from .properties import ArgumentError, PageError
26
29
 
27
30
  LOGGER = logging.getLogger(__name__)
28
31
 
@@ -48,7 +51,7 @@ class Application:
48
51
  elif path.is_file():
49
52
  self.synchronize_page(path)
50
53
  else:
51
- raise ValueError(f"expected: valid file or directory path; got: {path}")
54
+ raise ArgumentError(f"expected: valid file or directory path; got: {path}")
52
55
 
53
56
  def synchronize_page(
54
57
  self, page_path: Path, root_dir: Optional[Path] = None
@@ -83,7 +86,7 @@ class Application:
83
86
  if self.options.root_page_id
84
87
  else None
85
88
  )
86
- self._index_directory(local_dir, root_id, page_metadata)
89
+ self._index_directory(local_dir, root_dir, root_id, page_metadata)
87
90
  LOGGER.info("Indexed %d page(s)", len(page_metadata))
88
91
 
89
92
  # Step 2: convert each page
@@ -99,12 +102,21 @@ class Application:
99
102
  base_path = page_path.parent
100
103
 
101
104
  LOGGER.info("Synchronizing page: %s", page_path)
102
- document = ConfluenceDocument(page_path, self.options, root_dir, page_metadata)
105
+ site_metadata = ConfluenceSiteMetadata(
106
+ domain=self.api.domain,
107
+ base_path=self.api.base_path,
108
+ space_key=self.api.space_key,
109
+ )
110
+
111
+ document = ConfluenceDocument.create(
112
+ page_path, self.options, root_dir, site_metadata, page_metadata
113
+ )
103
114
  self._update_document(document, base_path)
104
115
 
105
116
  def _index_directory(
106
117
  self,
107
118
  local_dir: Path,
119
+ root_dir: Path,
108
120
  root_id: Optional[ConfluenceQualifiedID],
109
121
  page_metadata: dict[Path, ConfluencePageMetadata],
110
122
  ) -> None:
@@ -144,7 +156,7 @@ class Application:
144
156
  if parent_doc is not None:
145
157
  files.remove(parent_doc)
146
158
 
147
- metadata = self._get_or_create_page(parent_doc, root_id)
159
+ metadata = self._get_or_create_page(parent_doc, root_dir, root_id)
148
160
  LOGGER.debug("Indexed parent %s with metadata: %s", parent_doc, metadata)
149
161
  page_metadata[parent_doc] = metadata
150
162
 
@@ -153,16 +165,17 @@ class Application:
153
165
  parent_id = root_id
154
166
 
155
167
  for doc in files:
156
- metadata = self._get_or_create_page(doc, parent_id)
168
+ metadata = self._get_or_create_page(doc, root_dir, parent_id)
157
169
  LOGGER.debug("Indexed %s with metadata: %s", doc, metadata)
158
170
  page_metadata[doc] = metadata
159
171
 
160
172
  for directory in directories:
161
- self._index_directory(directory, parent_id, page_metadata)
173
+ self._index_directory(directory, root_dir, parent_id, page_metadata)
162
174
 
163
175
  def _get_or_create_page(
164
176
  self,
165
177
  absolute_path: Path,
178
+ root_dir: Path,
166
179
  parent_id: Optional[ConfluenceQualifiedID],
167
180
  *,
168
181
  title: Optional[str] = None,
@@ -176,19 +189,28 @@ class Application:
176
189
  document = f.read()
177
190
 
178
191
  qualified_id, document = extract_qualified_id(document)
179
- frontmatter_title, _ = extract_frontmatter_title(document)
180
192
 
181
193
  if qualified_id is not None:
182
194
  confluence_page = self.api.get_page(qualified_id.page_id)
183
195
  else:
184
196
  if parent_id is None:
185
- raise ValueError(
197
+ raise PageError(
186
198
  f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
187
199
  )
188
200
 
189
- # assign title from frontmatter if present
201
+ # assign title from front-matter if present
202
+ if title is None:
203
+ title, _ = extract_frontmatter_title(document)
204
+
205
+ # use file name (without extension) and path hash if no title is supplied
206
+ if title is None:
207
+ relative_path = absolute_path.relative_to(root_dir)
208
+ hash = hashlib.md5(relative_path.as_posix().encode("utf-8"))
209
+ digest = "".join(f"{c:x}" for c in hash.digest())
210
+ title = f"{absolute_path.stem} [{digest}]"
211
+
190
212
  confluence_page = self._create_page(
191
- absolute_path, document, title or frontmatter_title, parent_id
213
+ absolute_path, document, title, parent_id
192
214
  )
193
215
 
194
216
  space_key = (
@@ -198,8 +220,6 @@ class Application:
198
220
  )
199
221
 
200
222
  return ConfluencePageMetadata(
201
- domain=self.api.domain,
202
- base_path=self.api.base_path,
203
223
  page_id=confluence_page.id,
204
224
  space_key=space_key,
205
225
  title=confluence_page.title or "",
@@ -209,15 +229,11 @@ class Application:
209
229
  self,
210
230
  absolute_path: Path,
211
231
  document: str,
212
- title: Optional[str],
232
+ title: str,
213
233
  parent_id: ConfluenceQualifiedID,
214
234
  ) -> ConfluencePage:
215
235
  "Creates a new Confluence page when Markdown file doesn't have an embedded page ID yet."
216
236
 
217
- # use file name without extension if no title is supplied
218
- if title is None:
219
- title = absolute_path.stem
220
-
221
237
  confluence_page = self.api.get_or_create_page(
222
238
  title, parent_id.page_id, space_key=parent_id.space_key
223
239
  )