cds2md 0.0.0__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.
- cds2md-0.0.0/LICENSE +22 -0
- cds2md-0.0.0/PKG-INFO +130 -0
- cds2md-0.0.0/README.md +101 -0
- cds2md-0.0.0/cds2md/__init__.py +4 -0
- cds2md-0.0.0/cds2md/__main__.py +42 -0
- cds2md-0.0.0/cds2md/cache.py +41 -0
- cds2md-0.0.0/cds2md/convert.py +210 -0
- cds2md-0.0.0/cds2md/defs.py +27 -0
- cds2md-0.0.0/cds2md/engine.py +103 -0
- cds2md-0.0.0/cds2md/fetch.py +613 -0
- cds2md-0.0.0/cds2md/logger.py +54 -0
- cds2md-0.0.0/cds2md/opts.py +417 -0
- cds2md-0.0.0/cds2md/py.typed +0 -0
- cds2md-0.0.0/cds2md/translator.py +925 -0
- cds2md-0.0.0/cds2md/validate_connection.py +24 -0
- cds2md-0.0.0/cds2md/validate_space.py +33 -0
- cds2md-0.0.0/pyproject.toml +47 -0
cds2md-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright 2026 jdknight
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
4
|
+
are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
14
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
15
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
17
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
18
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
19
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
20
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
21
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
22
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
cds2md-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cds2md
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Confluence Data Center to Markdown Utility
|
|
5
|
+
Author: James Knight
|
|
6
|
+
Author-email: git@jdknight.me
|
|
7
|
+
Maintainer-email: James Knight <git@jdknight.me>
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-Expression: BSD-2-Clause
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Classifier: Topic :: Documentation
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: beautifulsoup4
|
|
23
|
+
Requires-Dist: httpx
|
|
24
|
+
Requires-Dist: python-slugify
|
|
25
|
+
Project-URL: Download, https://pypi.python.org/pypi/cds2md
|
|
26
|
+
Project-URL: Homepage, https://github.com/jdknight/cds2md
|
|
27
|
+
Project-URL: Source, https://github.com/jdknight/cds2md
|
|
28
|
+
|
|
29
|
+
# CDS2MD
|
|
30
|
+
|
|
31
|
+
[](https://pypi.python.org/pypi/cds2md)
|
|
32
|
+
[](https://github.com/jdknight/cds2md/actions/workflows/build.yml)
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
This utility can be used to convert Confluence® Data Center pages to Markdown.
|
|
37
|
+
Atlassian's Confluence Data Center is [scheduled for end-of-life in 2029][eol].
|
|
38
|
+
This tool can be used in an attempt migrate data from Confluence Data Center
|
|
39
|
+
instances to another location.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
* [Python][python] 3.11+
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
This tool can be installed using [pip][pip] or [pipx][pipx]:
|
|
48
|
+
|
|
49
|
+
```shell
|
|
50
|
+
pipx install cds2md
|
|
51
|
+
(or)
|
|
52
|
+
pip install -U cds2md
|
|
53
|
+
(or)
|
|
54
|
+
python -m pip install -U cds2md
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
# Usage
|
|
58
|
+
|
|
59
|
+
This tool can be invoked from a command line using:
|
|
60
|
+
|
|
61
|
+
```none
|
|
62
|
+
cds2md --help
|
|
63
|
+
(or)
|
|
64
|
+
python -m cds2md --help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A minimal example run is as follows:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
export CDS2MD_CONFLUENCE_TOKEN=TOKEN
|
|
71
|
+
export CDS2MD_CONFLUENCE_SPACE_KEY=MYSPACE
|
|
72
|
+
export CDS2MD_CONFLUENCE_URL=https://wiki.example.com/
|
|
73
|
+
cds2md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This will fetch page information from a Confluence space and store this data
|
|
77
|
+
into a local cache folder (`.cds2md-cache/`). Once all pages are fetched,
|
|
78
|
+
content will be converted to Markdown and placed in the output directory
|
|
79
|
+
(`output/`).
|
|
80
|
+
|
|
81
|
+
# Options
|
|
82
|
+
|
|
83
|
+
The following is a list of available options:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
positional arguments:
|
|
87
|
+
{all,connection-check,fetch,process}
|
|
88
|
+
action to perform (default: all)
|
|
89
|
+
|
|
90
|
+
options:
|
|
91
|
+
-h, --help show this help message and exit
|
|
92
|
+
--ca-certificate CA_CERTIFICATE
|
|
93
|
+
ca certificate to use when communicating with Confluence
|
|
94
|
+
--cache CACHE path to store fetch cached data
|
|
95
|
+
--confluence-space-key CONFLUENCE_SPACE_KEY
|
|
96
|
+
space to query for pages
|
|
97
|
+
--confluence-token CONFLUENCE_TOKEN
|
|
98
|
+
token used to query the Confluence instance
|
|
99
|
+
--confluence-token-file CONFLUENCE_TOKEN_FILE
|
|
100
|
+
file which holds a Confluence token to use
|
|
101
|
+
--confluence-token-stdin
|
|
102
|
+
pass in the token via the standard input stream
|
|
103
|
+
--confluence-url CONFLUENCE_URL
|
|
104
|
+
url of the Confluence data center instance to interact
|
|
105
|
+
with
|
|
106
|
+
--debug enabling debugging logs
|
|
107
|
+
--development configure to operate in a development state
|
|
108
|
+
--header, -H HEADER http headers to include in requests
|
|
109
|
+
--ignore-tls ignore all TLS checks when communicating with Confluence
|
|
110
|
+
--flat ignore any Confluence-detected hierarchy structure
|
|
111
|
+
--output OUTPUT path to output markdown content into
|
|
112
|
+
--timeout TIMEOUT timeout for connection REST requests to Confluence
|
|
113
|
+
--tweak-admonition-type {default,docusaurus,github,gitlab,myst,plain}
|
|
114
|
+
admonition formatting to use (default: default)
|
|
115
|
+
--tweak-html-images force all images to be generated as html-based images
|
|
116
|
+
--tweak-html-tables force all tables to be generated as html-based tables
|
|
117
|
+
--version show program's version number and exit
|
|
118
|
+
--wrap WRAP hint to the number of characters to wrap to
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
----
|
|
122
|
+
|
|
123
|
+
The CDS2MD project is unaffiliated with Atlassian.\
|
|
124
|
+
Atlassian is a registered trademark of Atlassian Pty Ltd.\
|
|
125
|
+
Confluence is a registered trademark of Atlassian Pty Ltd.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
[eol]: https://www.atlassian.com/licensing/data-center-end-of-life
|
|
129
|
+
[python]: https://www.python.org/
|
|
130
|
+
|
cds2md-0.0.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# CDS2MD
|
|
2
|
+
|
|
3
|
+
[](https://pypi.python.org/pypi/cds2md)
|
|
4
|
+
[](https://github.com/jdknight/cds2md/actions/workflows/build.yml)
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
This utility can be used to convert Confluence® Data Center pages to Markdown.
|
|
9
|
+
Atlassian's Confluence Data Center is [scheduled for end-of-life in 2029][eol].
|
|
10
|
+
This tool can be used in an attempt migrate data from Confluence Data Center
|
|
11
|
+
instances to another location.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
* [Python][python] 3.11+
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
This tool can be installed using [pip][pip] or [pipx][pipx]:
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
pipx install cds2md
|
|
23
|
+
(or)
|
|
24
|
+
pip install -U cds2md
|
|
25
|
+
(or)
|
|
26
|
+
python -m pip install -U cds2md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
# Usage
|
|
30
|
+
|
|
31
|
+
This tool can be invoked from a command line using:
|
|
32
|
+
|
|
33
|
+
```none
|
|
34
|
+
cds2md --help
|
|
35
|
+
(or)
|
|
36
|
+
python -m cds2md --help
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
A minimal example run is as follows:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
export CDS2MD_CONFLUENCE_TOKEN=TOKEN
|
|
43
|
+
export CDS2MD_CONFLUENCE_SPACE_KEY=MYSPACE
|
|
44
|
+
export CDS2MD_CONFLUENCE_URL=https://wiki.example.com/
|
|
45
|
+
cds2md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This will fetch page information from a Confluence space and store this data
|
|
49
|
+
into a local cache folder (`.cds2md-cache/`). Once all pages are fetched,
|
|
50
|
+
content will be converted to Markdown and placed in the output directory
|
|
51
|
+
(`output/`).
|
|
52
|
+
|
|
53
|
+
# Options
|
|
54
|
+
|
|
55
|
+
The following is a list of available options:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
positional arguments:
|
|
59
|
+
{all,connection-check,fetch,process}
|
|
60
|
+
action to perform (default: all)
|
|
61
|
+
|
|
62
|
+
options:
|
|
63
|
+
-h, --help show this help message and exit
|
|
64
|
+
--ca-certificate CA_CERTIFICATE
|
|
65
|
+
ca certificate to use when communicating with Confluence
|
|
66
|
+
--cache CACHE path to store fetch cached data
|
|
67
|
+
--confluence-space-key CONFLUENCE_SPACE_KEY
|
|
68
|
+
space to query for pages
|
|
69
|
+
--confluence-token CONFLUENCE_TOKEN
|
|
70
|
+
token used to query the Confluence instance
|
|
71
|
+
--confluence-token-file CONFLUENCE_TOKEN_FILE
|
|
72
|
+
file which holds a Confluence token to use
|
|
73
|
+
--confluence-token-stdin
|
|
74
|
+
pass in the token via the standard input stream
|
|
75
|
+
--confluence-url CONFLUENCE_URL
|
|
76
|
+
url of the Confluence data center instance to interact
|
|
77
|
+
with
|
|
78
|
+
--debug enabling debugging logs
|
|
79
|
+
--development configure to operate in a development state
|
|
80
|
+
--header, -H HEADER http headers to include in requests
|
|
81
|
+
--ignore-tls ignore all TLS checks when communicating with Confluence
|
|
82
|
+
--flat ignore any Confluence-detected hierarchy structure
|
|
83
|
+
--output OUTPUT path to output markdown content into
|
|
84
|
+
--timeout TIMEOUT timeout for connection REST requests to Confluence
|
|
85
|
+
--tweak-admonition-type {default,docusaurus,github,gitlab,myst,plain}
|
|
86
|
+
admonition formatting to use (default: default)
|
|
87
|
+
--tweak-html-images force all images to be generated as html-based images
|
|
88
|
+
--tweak-html-tables force all tables to be generated as html-based tables
|
|
89
|
+
--version show program's version number and exit
|
|
90
|
+
--wrap WRAP hint to the number of characters to wrap to
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
----
|
|
94
|
+
|
|
95
|
+
The CDS2MD project is unaffiliated with Atlassian.\
|
|
96
|
+
Atlassian is a registered trademark of Atlassian Pty Ltd.\
|
|
97
|
+
Confluence is a registered trademark of Atlassian Pty Ltd.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
[eol]: https://www.atlassian.com/licensing/data-center-end-of-life
|
|
101
|
+
[python]: https://www.python.org/
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
2
|
+
# Copyright jdknight
|
|
3
|
+
|
|
4
|
+
from cds2md.engine import engine
|
|
5
|
+
from cds2md.logger import logger
|
|
6
|
+
from cds2md.opts import process_options
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
"""
|
|
11
|
+
publish helper mainline
|
|
12
|
+
|
|
13
|
+
The mainline for cds2md's publisher helper utility.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# process command line/environment options
|
|
17
|
+
opts = process_options()
|
|
18
|
+
|
|
19
|
+
# if running a debugging, output the configuration used
|
|
20
|
+
if opts.debug:
|
|
21
|
+
debug_opts = vars(opts).copy()
|
|
22
|
+
|
|
23
|
+
# sanitize options which may expose token/etc. information
|
|
24
|
+
if debug_opts.get('confluence_token'):
|
|
25
|
+
debug_opts['confluence_token'] = '(set)' # noqa: S105
|
|
26
|
+
if debug_opts.get('header'):
|
|
27
|
+
debug_opts['header'] = '(set)'
|
|
28
|
+
if debug_opts.get('headers'):
|
|
29
|
+
debug_opts['headers'] = '(set)'
|
|
30
|
+
|
|
31
|
+
# pretty paths
|
|
32
|
+
debug_opts['cache'] = debug_opts['cache'].as_posix() + '/'
|
|
33
|
+
debug_opts['output'] = debug_opts['output'].as_posix() + '/'
|
|
34
|
+
|
|
35
|
+
logger.debug(f'configuration: {debug_opts}')
|
|
36
|
+
|
|
37
|
+
# run the utility
|
|
38
|
+
engine(opts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == '__main__':
|
|
42
|
+
main() # pragma: no cover
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
2
|
+
# Copyright jdknight
|
|
3
|
+
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from hashlib import sha1
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AttachmentMetaCache:
|
|
11
|
+
id: int
|
|
12
|
+
page_id: int
|
|
13
|
+
size: int
|
|
14
|
+
title: str
|
|
15
|
+
type: str
|
|
16
|
+
url: str
|
|
17
|
+
version: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PageMetaCache:
|
|
22
|
+
id: int
|
|
23
|
+
version: int
|
|
24
|
+
parent_id: int | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cache_key(opts: Namespace) -> str:
|
|
28
|
+
"""
|
|
29
|
+
generate a cache string
|
|
30
|
+
|
|
31
|
+
Build a cache string based on the configured runtime options. This will
|
|
32
|
+
be used to populate a cache path based on the Confluence URL and space.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
opts: the runtime options
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
the cache string
|
|
39
|
+
"""
|
|
40
|
+
value = f'{opts.confluence_url}--{opts.confluence_space_key}'
|
|
41
|
+
return sha1(value.encode()).hexdigest() # noqa: S324
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
2
|
+
# Copyright jdknight
|
|
3
|
+
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
from cds2md.cache import cache_key
|
|
7
|
+
from cds2md.defs import CACHE_DB
|
|
8
|
+
from cds2md.logger import logger
|
|
9
|
+
from cds2md.translator import Translator
|
|
10
|
+
from cds2md.translator import TranslatorContext
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from slugify import slugify
|
|
14
|
+
import json
|
|
15
|
+
import shutil
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AttachmentCache:
|
|
20
|
+
id: int
|
|
21
|
+
name: str
|
|
22
|
+
version: int
|
|
23
|
+
target_path: Path | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PageCache:
|
|
28
|
+
attachments: list[AttachmentCache]
|
|
29
|
+
body: str
|
|
30
|
+
id: int
|
|
31
|
+
title: str
|
|
32
|
+
version: int
|
|
33
|
+
parent_id: int | None = None
|
|
34
|
+
target_path: Path | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def convert(opts: Namespace) -> bool:
|
|
38
|
+
# ensure cache directory is prepared
|
|
39
|
+
cache_dir = opts.cache / cache_key(opts)
|
|
40
|
+
cache_db = cache_dir / CACHE_DB
|
|
41
|
+
if not cache_db.is_file():
|
|
42
|
+
logger.error('no cache database found')
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
# extract cached fetch data to be processed
|
|
46
|
+
pages_cache = []
|
|
47
|
+
pageid2cache = {}
|
|
48
|
+
|
|
49
|
+
with cache_db.open() as f:
|
|
50
|
+
pages = json.load(f)['pages']
|
|
51
|
+
|
|
52
|
+
for page_id_str, page_data in pages.items():
|
|
53
|
+
page_atachments = page_data['attachments']
|
|
54
|
+
page_id = int(page_id_str)
|
|
55
|
+
page_parent = page_data['parent']
|
|
56
|
+
page_version = page_data['version']
|
|
57
|
+
page_cache_dir = cache_dir / page_id_str
|
|
58
|
+
page_cache_filename = f'{page_id}-{page_version}.json'
|
|
59
|
+
page_cache_file = page_cache_dir / page_cache_filename
|
|
60
|
+
|
|
61
|
+
attachments = []
|
|
62
|
+
for attachment_name, attachment_data in page_atachments.items():
|
|
63
|
+
attachment_id = attachment_data['id']
|
|
64
|
+
attachment_version = attachment_data['version']
|
|
65
|
+
|
|
66
|
+
attachment_cache = AttachmentCache(
|
|
67
|
+
id=attachment_id,
|
|
68
|
+
name=attachment_name,
|
|
69
|
+
version=attachment_version,
|
|
70
|
+
)
|
|
71
|
+
attachments.append(attachment_cache)
|
|
72
|
+
|
|
73
|
+
with page_cache_file.open() as f2:
|
|
74
|
+
page_cache_data = json.load(f2)
|
|
75
|
+
page_body = page_cache_data['body']
|
|
76
|
+
page_title = page_cache_data['title']
|
|
77
|
+
|
|
78
|
+
page_cache = PageCache(
|
|
79
|
+
attachments=attachments,
|
|
80
|
+
body=page_body,
|
|
81
|
+
id=page_id,
|
|
82
|
+
parent_id=page_parent,
|
|
83
|
+
title=page_title,
|
|
84
|
+
version=page_version,
|
|
85
|
+
)
|
|
86
|
+
pageid2cache[page_id] = page_cache
|
|
87
|
+
pages_cache.append(page_cache)
|
|
88
|
+
|
|
89
|
+
# determine the paths for each page cache to be stored
|
|
90
|
+
attachment2target: dict[tuple[str, str], str] = {}
|
|
91
|
+
title2target: dict[str, str] = {}
|
|
92
|
+
reserved_attachment_targets = set()
|
|
93
|
+
|
|
94
|
+
for page_cache in pages_cache:
|
|
95
|
+
title_slug = slugify(page_cache.title)
|
|
96
|
+
md_filename = f'{title_slug}.md'
|
|
97
|
+
|
|
98
|
+
# if a flat setup, place the page at the root of the output
|
|
99
|
+
if opts.flat:
|
|
100
|
+
page_path = opts.output / md_filename
|
|
101
|
+
# if using hierarchy hints, determine based with parent data
|
|
102
|
+
else:
|
|
103
|
+
path_parts = [
|
|
104
|
+
md_filename,
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
target_parent = page_cache.parent_id
|
|
108
|
+
while target_parent:
|
|
109
|
+
parent = pageid2cache.get(target_parent)
|
|
110
|
+
assert parent
|
|
111
|
+
parent_slug = slugify(parent.title)
|
|
112
|
+
path_parts.append(parent_slug)
|
|
113
|
+
target_parent = parent.parent_id
|
|
114
|
+
|
|
115
|
+
page_path = opts.output
|
|
116
|
+
for s in reversed(path_parts):
|
|
117
|
+
page_path = page_path / s
|
|
118
|
+
|
|
119
|
+
page_cache.target_path = page_path
|
|
120
|
+
|
|
121
|
+
root_path = page_path.relative_to(opts.output)
|
|
122
|
+
page_full_slug = root_path.as_posix()
|
|
123
|
+
title2target[page_cache.title] = page_full_slug
|
|
124
|
+
|
|
125
|
+
for attachment_cache in page_cache.attachments:
|
|
126
|
+
attachment_pathname = Path(attachment_cache.name)
|
|
127
|
+
attachment_name = slugify(attachment_pathname.stem)
|
|
128
|
+
attachment_ext = slugify(attachment_pathname.suffix)
|
|
129
|
+
|
|
130
|
+
attachment_suffix = 1
|
|
131
|
+
while True:
|
|
132
|
+
sfx = attachment_suffix if attachment_suffix > 1 else ''
|
|
133
|
+
attachment_filename = f'{attachment_name}{sfx}.{attachment_ext}'
|
|
134
|
+
attachment_path = page_path.parent / attachment_filename
|
|
135
|
+
|
|
136
|
+
# check if we have a conflicting attachment
|
|
137
|
+
if attachment_path in reserved_attachment_targets:
|
|
138
|
+
attachment_suffix += 1
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
attachment_cache.target_path = attachment_path
|
|
144
|
+
reserved_attachment_targets.add(attachment_path)
|
|
145
|
+
|
|
146
|
+
attachment_ref = (page_full_slug, attachment_cache.name)
|
|
147
|
+
|
|
148
|
+
attachment_root_path = attachment_path.relative_to(opts.output)
|
|
149
|
+
attachment_full_slug = attachment_root_path.as_posix()
|
|
150
|
+
attachment2target[attachment_ref] = attachment_full_slug
|
|
151
|
+
|
|
152
|
+
# process each page
|
|
153
|
+
ctx = TranslatorContext(
|
|
154
|
+
attachment2target=attachment2target,
|
|
155
|
+
opts=opts,
|
|
156
|
+
title2target=title2target,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
for page_cache in pages_cache:
|
|
160
|
+
assert page_cache.target_path
|
|
161
|
+
doc_path = page_cache.target_path.as_posix()
|
|
162
|
+
|
|
163
|
+
# parse the page's body
|
|
164
|
+
logger.debug(f'processing body of page: {doc_path}')
|
|
165
|
+
soup = BeautifulSoup(page_cache.body, 'html.parser')
|
|
166
|
+
|
|
167
|
+
# translate the parsed content into a markdown output
|
|
168
|
+
translator = Translator(ctx, page_cache.title)
|
|
169
|
+
logger.debug(f'walking page: {doc_path}')
|
|
170
|
+
translator.walk(soup)
|
|
171
|
+
|
|
172
|
+
# build the final markdown document contents
|
|
173
|
+
final_body = ''.join(translator.output).strip() + '\n'
|
|
174
|
+
|
|
175
|
+
# write the markdown content to its output file
|
|
176
|
+
assert page_cache.target_path
|
|
177
|
+
md_file = page_cache.target_path
|
|
178
|
+
md_file.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
logger.debug(f'writing to markdown file: {md_file.as_posix()}')
|
|
180
|
+
|
|
181
|
+
if md_file.is_dir():
|
|
182
|
+
logger.error('cannot write markdown into path '
|
|
183
|
+
f'that is a directory: {md_file.as_posix()}')
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
md_file.write_text(final_body)
|
|
187
|
+
|
|
188
|
+
# process each attachment
|
|
189
|
+
if page_cache.attachments:
|
|
190
|
+
logger.debug(f'processing attachments for page: {doc_path}')
|
|
191
|
+
page_id = page_cache.id
|
|
192
|
+
attachment_cache_dir = cache_dir / f'{page_id}' / 'attachments'
|
|
193
|
+
|
|
194
|
+
for attachment_cache in page_cache.attachments:
|
|
195
|
+
attachment_id = attachment_cache.id
|
|
196
|
+
attachment_version = attachment_cache.version
|
|
197
|
+
attachment_fname = f'{attachment_id}-{attachment_version}.dat'
|
|
198
|
+
attachment_data = attachment_cache_dir / attachment_fname
|
|
199
|
+
|
|
200
|
+
assert attachment_cache.target_path
|
|
201
|
+
attachment_folder = attachment_cache.target_path.parent
|
|
202
|
+
attachment_folder.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
|
|
204
|
+
attachment_desc = attachment_cache.target_path.as_posix()
|
|
205
|
+
logger.debug(f'copying attachment (source): {attachment_data}')
|
|
206
|
+
logger.debug(f'copying attachment (target): {attachment_desc}')
|
|
207
|
+
shutil.copy2(attachment_data, attachment_cache.target_path)
|
|
208
|
+
|
|
209
|
+
return True
|
|
210
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
2
|
+
# Copyright jdknight
|
|
3
|
+
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# name of the cache database for a cached dataset
|
|
8
|
+
CACHE_DB = 'cache.json'
|
|
9
|
+
|
|
10
|
+
# name of the cache directory
|
|
11
|
+
CACHE_DIRNAME = '.cds2md-cache'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Action(StrEnum):
|
|
15
|
+
ALL = 'all'
|
|
16
|
+
CONNECTION_CHECK = 'connection-check'
|
|
17
|
+
FETCH = 'fetch'
|
|
18
|
+
PROCESS = 'process'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AdmonitionType(StrEnum):
|
|
22
|
+
DEFAULT = 'default'
|
|
23
|
+
DOCUSAURUS = 'docusaurus'
|
|
24
|
+
GITHUB = 'github'
|
|
25
|
+
GITLAB = 'gitlab'
|
|
26
|
+
MYST = 'myst'
|
|
27
|
+
PLAIN = 'plain'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
2
|
+
# Copyright jdknight
|
|
3
|
+
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from cds2md import __version__ as cds2md_version
|
|
6
|
+
from cds2md.defs import Action
|
|
7
|
+
from cds2md.convert import convert
|
|
8
|
+
from cds2md.fetch import fetch
|
|
9
|
+
from cds2md.logger import logger
|
|
10
|
+
from cds2md.validate_connection import validate_connection
|
|
11
|
+
from cds2md.validate_space import validate_space
|
|
12
|
+
import httpx
|
|
13
|
+
import ssl
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def engine(opts: Namespace) -> None:
|
|
17
|
+
# perform any action requiring remote work
|
|
18
|
+
if opts.action in [Action.ALL, Action.CONNECTION_CHECK, Action.FETCH]:
|
|
19
|
+
perform_web_action(opts)
|
|
20
|
+
|
|
21
|
+
# perform process action
|
|
22
|
+
if opts.action in [Action.ALL, Action.PROCESS]:
|
|
23
|
+
process(opts)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def perform_web_action(opts: Namespace) -> None:
|
|
27
|
+
# prepare an http client for requests
|
|
28
|
+
timeout = httpx.Timeout(
|
|
29
|
+
connect=opts.timeout,
|
|
30
|
+
read=15.0,
|
|
31
|
+
write=5.0,
|
|
32
|
+
pool=5.0,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
headers: dict[str, str] = {}
|
|
36
|
+
headers |= opts.headers
|
|
37
|
+
headers |= {
|
|
38
|
+
# will only process json
|
|
39
|
+
'Accept': 'application/json',
|
|
40
|
+
# configure confluence pat to perform queries with
|
|
41
|
+
'Authorization': f'Bearer {opts.confluence_token}',
|
|
42
|
+
# user agent for our interactions
|
|
43
|
+
'User-Agent': f'Cds2md/{cds2md_version}',
|
|
44
|
+
# ignore csrf
|
|
45
|
+
'X-Atlassian-Token': 'no-check',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# setup ssl context
|
|
49
|
+
ssl_context = ssl.create_default_context()
|
|
50
|
+
if opts.ca_certificate:
|
|
51
|
+
ssl_context.load_verify_locations(cafile=opts.ca_certificate)
|
|
52
|
+
elif opts.ignore_tls:
|
|
53
|
+
ssl_context = False # type: ignore[assignment]
|
|
54
|
+
|
|
55
|
+
with httpx.Client(
|
|
56
|
+
headers=headers,
|
|
57
|
+
timeout=timeout,
|
|
58
|
+
verify=ssl_context,
|
|
59
|
+
) as client:
|
|
60
|
+
|
|
61
|
+
# perform connection check action
|
|
62
|
+
if opts.action in [Action.ALL, Action.CONNECTION_CHECK]:
|
|
63
|
+
connection_check(opts, client)
|
|
64
|
+
|
|
65
|
+
# perform data fetch action
|
|
66
|
+
if opts.action in [Action.ALL, Action.FETCH]:
|
|
67
|
+
fetch_attempt(opts, client)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def connection_check(opts: Namespace, client: httpx.Client) -> None:
|
|
71
|
+
logger.info('validating availability of confluence instance...')
|
|
72
|
+
logger.debug(f'instance: {opts.confluence_url}')
|
|
73
|
+
|
|
74
|
+
if not validate_connection(opts, client):
|
|
75
|
+
raise SystemExit(3)
|
|
76
|
+
|
|
77
|
+
logger.info('confluence instance appears to be online')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def fetch_attempt(opts: Namespace, client: httpx.Client) -> None:
|
|
81
|
+
logger.info('validating the confluence space can be found...')
|
|
82
|
+
logger.debug(f'space-key: {opts.confluence_space_key}')
|
|
83
|
+
|
|
84
|
+
# verify the space exists
|
|
85
|
+
if not validate_space(opts, client):
|
|
86
|
+
raise SystemExit(4)
|
|
87
|
+
|
|
88
|
+
logger.info('confluence space has been detected')
|
|
89
|
+
|
|
90
|
+
# try to pull space content
|
|
91
|
+
logger.info('attempting to fetch content...')
|
|
92
|
+
|
|
93
|
+
if not fetch(opts, client):
|
|
94
|
+
raise SystemExit(5)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def process(opts: Namespace) -> None:
|
|
98
|
+
logger.info('converting cached data into markdown files')
|
|
99
|
+
|
|
100
|
+
if not convert(opts):
|
|
101
|
+
raise SystemExit(3)
|
|
102
|
+
|
|
103
|
+
logger.info('conversion completed')
|