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.
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/PKG-INFO +25 -7
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/README.md +22 -5
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/PKG-INFO +25 -7
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/__init__.py +1 -1
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/__main__.py +36 -11
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/api.py +48 -18
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/application.py +34 -18
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/converter.py +115 -26
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/emoji.py +3 -1
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/mermaid.py +1 -1
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/processor.py +11 -10
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/properties.py +40 -16
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_conversion.py +50 -15
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_matcher.py +1 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_mermaid.py +3 -3
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/tests/test_processor.py +5 -10
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/LICENSE +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/SOURCES.txt +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/requires.txt +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/top_level.txt +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/markdown_to_confluence.egg-info/zip-safe +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/entities.dtd +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/matcher.py +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/puppeteer-config.json +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/md2conf/py.typed +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/pyproject.toml +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/setup.cfg +0 -0
- {markdown_to_confluence-0.3.1 → markdown_to_confluence-0.3.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
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
|
-
[-
|
|
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
|
|
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
|
-
[-
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
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
|
-
[-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
99
|
+
properties: ConfluenceConnectionProperties
|
|
81
100
|
session: Optional["ConfluenceSession"] = None
|
|
82
101
|
|
|
83
|
-
def __init__(
|
|
84
|
-
self
|
|
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.
|
|
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, "
|
|
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, "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
)
|