├── .coveragerc
├── .gitignore
├── .pre-commit-config.yaml
├── .travis.yml
├── LICENSE
├── README.md
├── codecov.yml
├── mkdocs_with_confluence
├── __init__.py
└── plugin.py
├── requirements.txt
├── requirements_dev.txt
├── setup.py
└── tests
└── test.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = mkdocs_with_confluence
4 | include = */mkdocs_with_confluence/*
5 |
6 | [report]
7 | exclude_lines =
8 | if self.debug:
9 | pragma: no cover
10 | raise NotImplementedError
11 | if __name__ == .__main__.:
12 | ignore_errors = True
13 | omit =
14 | tests/*
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | mkdocs_with_confluence/__pycache__/*
2 | mkdocs_with_confluence.egg-info/*
3 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: local
3 | hooks:
4 | - id: black
5 | name: black
6 | entry: black
7 | language: python
8 | types: [python]
9 | language_version: python3.7
10 | args: [--line-length=120]
11 | - repo: local
12 | hooks:
13 | - id: mypy
14 | name: mypy
15 | entry: mypy
16 | language: system
17 | types: [python]
18 | args: [--ignore-missing-imports, --namespace-packages, --show-error-codes, --pretty]
19 | - repo: local
20 | hooks:
21 | - id: flake8
22 | name: flake8
23 | entry: flake8
24 | language: system
25 | types: [python]
26 | args: [--max-line-length=120, "--ignore=D101,D104,D212,D200,E203,W293,D412,W503"]
27 | # D100 requires all Python files (modules) to have a "public" docstring even if all functions within have a docstring.
28 | # D104 requires __init__ files to have a docstring
29 | # D212
30 | # D200
31 | # D412 No blank lines allowed between a section header and its content
32 | # E203
33 | # W293 blank line contains whitespace
34 | # W503 line break before binary operator (for compatibility with black)
35 |
36 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | script:
3 | - pip install -r requirements_dev.txt
4 | - python setup.py install
5 | - flake8 --max-line-length=120 --ignore=D101,D104,D212,D200,E203,W293,D412,W503 mkdocs_with_confluence/
6 | - black --check --line-length=120 mkdocs_with_confluence/
7 | - nosetests --with-coverage
8 | after_success:
9 | - bash <(curl -s https://codecov.io/bash)
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Pawel Sikora
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://app.travis-ci.com/pawelsikora/mkdocs-with-confluence)
3 | [](https://codecov.io/gh/pawelsikora/mkdocs-with-confluence)
4 | 
5 | 
6 | 
7 | 
8 | # mkdocs-with-confluence
9 |
10 | MkDocs plugin that converts markdown pages into confluence markup
11 | and export it to the Confluence page
12 |
13 | ## Setup
14 | Install the plugin using pip:
15 |
16 | `pip install mkdocs-with-confluence`
17 |
18 | Activate the plugin in `mkdocs.yml`:
19 |
20 | ```yaml
21 | plugins:
22 | - search
23 | - mkdocs-with-confluence
24 | ```
25 |
26 | More information about plugins in the [MkDocs documentation: mkdocs-plugins](https://www.mkdocs.org/user-guide/plugins/).
27 |
28 | ## Usage
29 |
30 | Use following config and adjust it according to your needs:
31 |
32 | ```yaml
33 | - mkdocs-with-confluence:
34 | host_url: https:///rest/api/content
35 | space:
36 | parent_page_name:
37 | username:
38 | password:
39 | enabled_if_env: MKDOCS_TO_CONFLUENCE
40 | #verbose: true
41 | #debug: true
42 | dryrun: true
43 | ```
44 |
45 | ## Parameters:
46 |
47 | ### Requirements
48 | - md2cf
49 | - mimetypes
50 | - mistune
51 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 |
3 | coverage:
4 | status:
5 | project:
6 | default:
7 | # Commits pushed to master should not make the overall
8 | # project coverage decrease by more than x%:
9 | target: auto
10 | threshold: 20%
11 | patch:
12 | default:
13 | # Be tolerant on slight code coverage diff on PRs to limit
14 | # noisy red coverage status on github PRs.
15 | # Note: The coverage stats are still uploaded
16 | # to codecov so that PR reviewers can see uncovered lines
17 | target: auto
18 | threshold: 20%
19 |
20 | codecov:
21 | notify:
22 | # Prevent coverage status to upload multiple times for parallel and long
23 | # running CI pipelines. This configuration is particularly useful on PRs
24 | # to avoid confusion. Note that this value is set to the number of Azure
25 | # Pipeline jobs uploading coverage reports.
26 | after_n_builds: 6
27 | token: 21ccebfb-1239-4ec6-a01d-039067b9ab30
28 |
29 |
--------------------------------------------------------------------------------
/mkdocs_with_confluence/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelsikora/mkdocs-with-confluence/ac0b21edda0d295a1c50aac7606a80437c84deba/mkdocs_with_confluence/__init__.py
--------------------------------------------------------------------------------
/mkdocs_with_confluence/plugin.py:
--------------------------------------------------------------------------------
1 | import time
2 | import os
3 | import hashlib
4 | import sys
5 | import re
6 | import tempfile
7 | import shutil
8 | import requests
9 | import mimetypes
10 | import mistune
11 | import contextlib
12 | from time import sleep
13 | from mkdocs.config import config_options
14 | from mkdocs.plugins import BasePlugin
15 | from md2cf.confluence_renderer import ConfluenceRenderer
16 | from os import environ
17 | from pathlib import Path
18 |
19 | TEMPLATE_BODY = " TEMPLATE
"
20 |
21 |
22 | @contextlib.contextmanager
23 | def nostdout():
24 | save_stdout = sys.stdout
25 | sys.stdout = DummyFile()
26 | yield
27 | sys.stdout = save_stdout
28 |
29 |
30 | class DummyFile(object):
31 | def write(self, x):
32 | pass
33 |
34 |
35 | class MkdocsWithConfluence(BasePlugin):
36 | _id = 0
37 | config_scheme = (
38 | ("host_url", config_options.Type(str, default=None)),
39 | ("space", config_options.Type(str, default=None)),
40 | ("parent_page_name", config_options.Type(str, default=None)),
41 | ("username", config_options.Type(str, default=environ.get("JIRA_USERNAME", None))),
42 | ("api_token", config_options.Type(str, default=environ.get("CONFLUENCE_API_TOKEN", None))), # If specified, password is ignored
43 | ("password", config_options.Type(str, default=environ.get("JIRA_PASSWORD", None))),
44 | ("enabled_if_env", config_options.Type(str, default=None)),
45 | ("verbose", config_options.Type(bool, default=False)),
46 | ("debug", config_options.Type(bool, default=False)),
47 | ("dryrun", config_options.Type(bool, default=False)),
48 | )
49 |
50 | def __init__(self):
51 | self.enabled = True
52 | self.confluence_renderer = ConfluenceRenderer(use_xhtml=True)
53 | self.confluence_mistune = mistune.Markdown(renderer=self.confluence_renderer)
54 | self.simple_log = False
55 | self.flen = 1
56 | self.session = requests.Session()
57 | self.page_attachments = {}
58 |
59 | def on_nav(self, nav, config, files):
60 | MkdocsWithConfluence.tab_nav = []
61 | navigation_items = nav.__repr__()
62 |
63 | for n in navigation_items.split("\n"):
64 | leading_spaces = len(n) - len(n.lstrip(" "))
65 | spaces = leading_spaces * " "
66 | if "Page" in n:
67 | try:
68 | self.page_title = self.__get_page_title(n)
69 | if self.page_title is None:
70 | raise AttributeError
71 | except AttributeError:
72 | self.page_local_path = self.__get_page_url(n)
73 | print(
74 | f"WARN - Page from path {self.page_local_path} has no"
75 | f" entity in the mkdocs.yml nav section. It will be uploaded"
76 | f" to the Confluence, but you may not see it on the web server!"
77 | )
78 | self.page_local_name = self.__get_page_name(n)
79 | self.page_title = self.page_local_name
80 |
81 | p = spaces + self.page_title
82 | MkdocsWithConfluence.tab_nav.append(p)
83 | if "Section" in n:
84 | try:
85 | self.section_title = self.__get_section_title(n)
86 | if self.section_title is None:
87 | raise AttributeError
88 | except AttributeError:
89 | self.section_local_path = self.__get_page_url(n)
90 | print(
91 | f"WARN - Section from path {self.section_local_path} has no"
92 | f" entity in the mkdocs.yml nav section. It will be uploaded"
93 | f" to the Confluence, but you may not see it on the web server!"
94 | )
95 | self.section_local_name = self.__get_section_title(n)
96 | self.section_title = self.section_local_name
97 | s = spaces + self.section_title
98 | MkdocsWithConfluence.tab_nav.append(s)
99 |
100 | def on_files(self, files, config):
101 | pages = files.documentation_pages()
102 | try:
103 | self.flen = len(pages)
104 | print(f"Number of Files in directory tree: {self.flen}")
105 | except 0:
106 | print("ERR: You have no documentation pages" "in the directory tree, please add at least one!")
107 |
108 | def on_post_template(self, output_content, template_name, config):
109 | if self.config["verbose"] is False and self.config["debug"] is False:
110 | self.simple_log = True
111 | print("INFO - Mkdocs With Confluence: Start exporting markdown pages... (simple logging)")
112 | else:
113 | self.simple_log = False
114 |
115 | def on_config(self, config):
116 | if "enabled_if_env" in self.config:
117 | env_name = self.config["enabled_if_env"]
118 | if env_name:
119 | self.enabled = os.environ.get(env_name) == "1"
120 | if not self.enabled:
121 | print(
122 | "WARNING - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned OFF: "
123 | f"(set environment variable {env_name} to 1 to enable)"
124 | )
125 | return
126 | else:
127 | print(
128 | "INFO - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence "
129 | f"turned ON by var {env_name}==1!"
130 | )
131 | self.enabled = True
132 | else:
133 | print(
134 | "WARNING - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned OFF: "
135 | f"(set environment variable {env_name} to 1 to enable)"
136 | )
137 | return
138 | else:
139 | print("INFO - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned ON by default!")
140 | self.enabled = True
141 |
142 | if self.config["dryrun"]:
143 | print("WARNING - Mkdocs With Confluence - DRYRUN MODE turned ON")
144 | self.dryrun = True
145 | else:
146 | self.dryrun = False
147 |
148 | def on_page_markdown(self, markdown, page, config, files):
149 | MkdocsWithConfluence._id += 1
150 | if self.config["api_token"]:
151 | self.session.auth = (self.config["username"], self.config["api_token"])
152 | else:
153 | self.session.auth = (self.config["username"], self.config["password"])
154 |
155 | if self.enabled:
156 | if self.simple_log is True:
157 | print("INFO - Mkdocs With Confluence: Page export progress: [", end="", flush=True)
158 | for i in range(MkdocsWithConfluence._id):
159 | print("#", end="", flush=True)
160 | for j in range(self.flen - MkdocsWithConfluence._id):
161 | print("-", end="", flush=True)
162 | print(f"] ({MkdocsWithConfluence._id} / {self.flen})", end="\r", flush=True)
163 |
164 | if self.config["debug"]:
165 | print(f"\nDEBUG - Handling Page '{page.title}' (And Parent Nav Pages if necessary):\n")
166 | if not all(self.config_scheme):
167 | print("DEBUG - ERR: YOU HAVE EMPTY VALUES IN YOUR CONFIG. ABORTING")
168 | return markdown
169 |
170 | try:
171 | if self.config["debug"]:
172 | print("DEBUG - Get section first parent title...: ")
173 | try:
174 |
175 | parent = self.__get_section_title(page.ancestors[0].__repr__())
176 | except IndexError as e:
177 | if self.config["debug"]:
178 | print(
179 | f"DEBUG - WRN({e}): No first parent! Assuming "
180 | f"DEBUG - {self.config['parent_page_name']}..."
181 | )
182 | parent = None
183 | if self.config["debug"]:
184 | print(f"DEBUG - {parent}")
185 | if not parent:
186 | parent = self.config["parent_page_name"]
187 |
188 | if self.config["parent_page_name"] is not None:
189 | main_parent = self.config["parent_page_name"]
190 | else:
191 | main_parent = self.config["space"]
192 |
193 | if self.config["debug"]:
194 | print("DEBUG - Get section second parent title...: ")
195 | try:
196 | parent1 = self.__get_section_title(page.ancestors[1].__repr__())
197 | except IndexError as e:
198 | if self.config["debug"]:
199 | print(
200 | f"DEBUG - ERR({e}) No second parent! Assuming "
201 | f"second parent is main parent: {main_parent}..."
202 | )
203 | parent1 = None
204 | if self.config["debug"]:
205 | print(f"{parent}")
206 |
207 | if not parent1:
208 | parent1 = main_parent
209 | if self.config["debug"]:
210 | print(
211 | f"DEBUG - ONLY ONE PARENT FOUND. ASSUMING AS A "
212 | f"FIRST NODE after main parent config {main_parent}"
213 | )
214 |
215 | if self.config["debug"]:
216 | print(f"DEBUG - PARENT0: {parent}, PARENT1: {parent1}, MAIN PARENT: {main_parent}")
217 |
218 | tf = tempfile.NamedTemporaryFile(delete=False)
219 | f = open(tf.name, "w")
220 |
221 | attachments = []
222 | try:
223 | for match in re.finditer(r'img src="file://(.*)" s', markdown):
224 | if self.config["debug"]:
225 | print(f"DEBUG - FOUND IMAGE: {match.group(1)}")
226 | attachments.append(match.group(1))
227 | for match in re.finditer(r"!\[[\w\. -]*\]\((?!http|file)([^\s,]*).*\)", markdown):
228 | file_path = match.group(1).lstrip("./\\")
229 | attachments.append(file_path)
230 |
231 | if self.config["debug"]:
232 | print(f"DEBUG - FOUND IMAGE: {file_path}")
233 | attachments.append("docs/" + file_path.replace("../", ""))
234 |
235 | except AttributeError as e:
236 | if self.config["debug"]:
237 | print(f"DEBUG - WARN(({e}): No images found in markdown. Proceed..")
238 | new_markdown = re.sub(
239 | r'
', '"/>
', new_markdown)
242 | confluence_body = self.confluence_mistune(new_markdown)
243 | f.write(confluence_body)
244 | if self.config["debug"]:
245 | print(confluence_body)
246 | page_name = page.title
247 | new_name = "confluence_page_" + page_name.replace(" ", "_") + ".html"
248 | shutil.copy(f.name, new_name)
249 | f.close()
250 |
251 | if self.config["debug"]:
252 | print(
253 | f"\nDEBUG - UPDATING PAGE TO CONFLUENCE, DETAILS:\n"
254 | f"DEBUG - HOST: {self.config['host_url']}\n"
255 | f"DEBUG - SPACE: {self.config['space']}\n"
256 | f"DEBUG - TITLE: {page.title}\n"
257 | f"DEBUG - PARENT: {parent}\n"
258 | f"DEBUG - BODY: {confluence_body}\n"
259 | )
260 |
261 | page_id = self.find_page_id(page.title)
262 | if page_id is not None:
263 | if self.config["debug"]:
264 | print(
265 | f"DEBUG - JUST ONE STEP FROM UPDATE OF PAGE '{page.title}' \n"
266 | f"DEBUG - CHECKING IF PARENT PAGE ON CONFLUENCE IS THE SAME AS HERE"
267 | )
268 |
269 | parent_name = self.find_parent_name_of_page(page.title)
270 |
271 | if parent_name == parent:
272 | if self.config["debug"]:
273 | print("DEBUG - Parents match. Continue...")
274 | else:
275 | if self.config["debug"]:
276 | print(f"DEBUG - ERR, Parents does not match: '{parent}' =/= '{parent_name}' Aborting...")
277 | return markdown
278 | self.update_page(page.title, confluence_body)
279 | for i in MkdocsWithConfluence.tab_nav:
280 | if page.title in i:
281 | print(f"INFO - Mkdocs With Confluence: {i} *UPDATE*")
282 | else:
283 | if self.config["debug"]:
284 | print(
285 | f"DEBUG - PAGE: {page.title}, PARENT0: {parent}, "
286 | f"PARENT1: {parent1}, MAIN PARENT: {main_parent}"
287 | )
288 | parent_id = self.find_page_id(parent)
289 | self.wait_until(parent_id, 1, 20)
290 | second_parent_id = self.find_page_id(parent1)
291 | self.wait_until(second_parent_id, 1, 20)
292 | main_parent_id = self.find_page_id(main_parent)
293 | if not parent_id:
294 | if not second_parent_id:
295 | main_parent_id = self.find_page_id(main_parent)
296 | if not main_parent_id:
297 | print("ERR: MAIN PARENT UNKNOWN. ABORTING!")
298 | return markdown
299 |
300 | if self.config["debug"]:
301 | print(
302 | f"DEBUG - Trying to ADD page '{parent1}' to "
303 | f"main parent({main_parent}) ID: {main_parent_id}"
304 | )
305 | body = TEMPLATE_BODY.replace("TEMPLATE", parent1)
306 | self.add_page(parent1, main_parent_id, body)
307 | for i in MkdocsWithConfluence.tab_nav:
308 | if parent1 in i:
309 | print(f"INFO - Mkdocs With Confluence: {i} *NEW PAGE*")
310 | time.sleep(1)
311 |
312 | if self.config["debug"]:
313 | print(
314 | f"DEBUG - Trying to ADD page '{parent}' "
315 | f"to parent1({parent1}) ID: {second_parent_id}"
316 | )
317 | body = TEMPLATE_BODY.replace("TEMPLATE", parent)
318 | self.add_page(parent, second_parent_id, body)
319 | for i in MkdocsWithConfluence.tab_nav:
320 | if parent in i:
321 | print(f"INFO - Mkdocs With Confluence: {i} *NEW PAGE*")
322 | time.sleep(1)
323 |
324 | if parent_id is None:
325 | for i in range(11):
326 | while parent_id is None:
327 | try:
328 | self.add_page(page.title, parent_id, confluence_body)
329 | except requests.exceptions.HTTPError:
330 | print(
331 | f"ERR - HTTP error on adding page. It probably occured due to "
332 | f"parent ID('{parent_id}') page is not YET synced on server. Retry nb {i}/10..."
333 | )
334 | sleep(5)
335 | parent_id = self.find_page_id(parent)
336 | break
337 |
338 | self.add_page(page.title, parent_id, confluence_body)
339 |
340 | print(f"Trying to ADD page '{page.title}' to parent0({parent}) ID: {parent_id}")
341 | for i in MkdocsWithConfluence.tab_nav:
342 | if page.title in i:
343 | print(f"INFO - Mkdocs With Confluence: {i} *NEW PAGE*")
344 |
345 | if attachments:
346 | self.page_attachments[page.title] = attachments
347 |
348 | except IndexError as e:
349 | if self.config["debug"]:
350 | print(f"DEBUG - ERR({e}): Exception error!")
351 | return markdown
352 |
353 | return markdown
354 |
355 | def on_post_page(self, output, page, config):
356 | site_dir = config.get("site_dir")
357 | attachments = self.page_attachments.get(page.title, [])
358 |
359 | if self.config["debug"]:
360 | print(f"\nDEBUG - UPLOADING ATTACHMENTS TO CONFLUENCE FOR {page.title}, DETAILS:")
361 | print(f"FILES: {attachments} \n")
362 | for attachment in attachments:
363 | if self.config["debug"]:
364 | print(f"DEBUG - looking for {attachment} in {site_dir}")
365 | for p in Path(site_dir).rglob(f"*{attachment}"):
366 | self.add_or_update_attachment(page.title, p)
367 | return output
368 |
369 | def on_page_content(self, html, page, config, files):
370 | return html
371 |
372 | def __get_page_url(self, section):
373 | return re.search("url='(.*)'\\)", section).group(1)[:-1] + ".md"
374 |
375 | def __get_page_name(self, section):
376 | return os.path.basename(re.search("url='(.*)'\\)", section).group(1)[:-1])
377 |
378 | def __get_section_name(self, section):
379 | if self.config["debug"]:
380 | print(f"DEBUG - SECTION name: {section}")
381 | return os.path.basename(re.search("url='(.*)'\\/", section).group(1)[:-1])
382 |
383 | def __get_section_title(self, section):
384 | if self.config["debug"]:
385 | print(f"DEBUG - SECTION title: {section}")
386 | try:
387 | r = re.search("Section\\(title='(.*)'\\)", section)
388 | return r.group(1)
389 | except AttributeError:
390 | name = self.__get_section_name(section)
391 | print(f"WRN - Section '{name}' doesn't exist in the mkdocs.yml nav section!")
392 | return name
393 |
394 | def __get_page_title(self, section):
395 | try:
396 | r = re.search("\\s*Page\\(title='(.*)',", section)
397 | return r.group(1)
398 | except AttributeError:
399 | name = self.__get_page_url(section)
400 | print(f"WRN - Page '{name}' doesn't exist in the mkdocs.yml nav section!")
401 | return name
402 |
403 | # Adapted from https://stackoverflow.com/a/3431838
404 | def get_file_sha1(self, file_path):
405 | hash_sha1 = hashlib.sha1()
406 | with open(file_path, "rb") as f:
407 | for chunk in iter(lambda: f.read(4096), b""):
408 | hash_sha1.update(chunk)
409 | return hash_sha1.hexdigest()
410 |
411 | def add_or_update_attachment(self, page_name, filepath):
412 | print(f"INFO - Mkdocs With Confluence * {page_name} *ADD/Update ATTACHMENT if required* {filepath}")
413 | if self.config["debug"]:
414 | print(f" * Mkdocs With Confluence: Add Attachment: PAGE NAME: {page_name}, FILE: {filepath}")
415 | page_id = self.find_page_id(page_name)
416 | if page_id:
417 | file_hash = self.get_file_sha1(filepath)
418 | attachment_message = f"MKDocsWithConfluence [v{file_hash}]"
419 | existing_attachment = self.get_attachment(page_id, filepath)
420 | if existing_attachment:
421 | file_hash_regex = re.compile(r"\[v([a-f0-9]{40})]$")
422 | existing_match = file_hash_regex.search(existing_attachment["version"]["message"])
423 | if existing_match is not None and existing_match.group(1) == file_hash:
424 | if self.config["debug"]:
425 | print(f" * Mkdocs With Confluence * {page_name} * Existing attachment skipping * {filepath}")
426 | else:
427 | self.update_attachment(page_id, filepath, existing_attachment, attachment_message)
428 | else:
429 | self.create_attachment(page_id, filepath, attachment_message)
430 | else:
431 | if self.config["debug"]:
432 | print("PAGE DOES NOT EXISTS")
433 |
434 | def get_attachment(self, page_id, filepath):
435 | name = os.path.basename(filepath)
436 | if self.config["debug"]:
437 | print(f" * Mkdocs With Confluence: Get Attachment: PAGE ID: {page_id}, FILE: {filepath}")
438 |
439 | url = self.config["host_url"] + "/" + page_id + "/child/attachment"
440 | headers = {"X-Atlassian-Token": "no-check"} # no content-type here!
441 | if self.config["debug"]:
442 | print(f"URL: {url}")
443 |
444 | r = self.session.get(url, headers=headers, params={"filename": name, "expand": "version"})
445 | r.raise_for_status()
446 | with nostdout():
447 | response_json = r.json()
448 | if response_json["size"]:
449 | return response_json["results"][0]
450 |
451 | def update_attachment(self, page_id, filepath, existing_attachment, message):
452 | if self.config["debug"]:
453 | print(f" * Mkdocs With Confluence: Update Attachment: PAGE ID: {page_id}, FILE: {filepath}")
454 |
455 | url = self.config["host_url"] + "/" + page_id + "/child/attachment/" + existing_attachment["id"] + "/data"
456 | headers = {"X-Atlassian-Token": "no-check"} # no content-type here!
457 |
458 | if self.config["debug"]:
459 | print(f"URL: {url}")
460 |
461 | filename = os.path.basename(filepath)
462 |
463 | # determine content-type
464 | content_type, encoding = mimetypes.guess_type(filepath)
465 | if content_type is None:
466 | content_type = "multipart/form-data"
467 | files = {"file": (filename, open(Path(filepath), "rb"), content_type), "comment": message}
468 |
469 | if not self.dryrun:
470 | r = self.session.post(url, headers=headers, files=files)
471 | r.raise_for_status()
472 | print(r.json())
473 | if r.status_code == 200:
474 | print("OK!")
475 | else:
476 | print("ERR!")
477 |
478 | def create_attachment(self, page_id, filepath, message):
479 | if self.config["debug"]:
480 | print(f" * Mkdocs With Confluence: Create Attachment: PAGE ID: {page_id}, FILE: {filepath}")
481 |
482 | url = self.config["host_url"] + "/" + page_id + "/child/attachment"
483 | headers = {"X-Atlassian-Token": "no-check"} # no content-type here!
484 |
485 | if self.config["debug"]:
486 | print(f"URL: {url}")
487 |
488 | filename = os.path.basename(filepath)
489 |
490 | # determine content-type
491 | content_type, encoding = mimetypes.guess_type(filepath)
492 | if content_type is None:
493 | content_type = "multipart/form-data"
494 | files = {"file": (filename, open(filepath, "rb"), content_type), "comment": message}
495 | if not self.dryrun:
496 | r = self.session.post(url, headers=headers, files=files)
497 | print(r.json())
498 | r.raise_for_status()
499 | if r.status_code == 200:
500 | print("OK!")
501 | else:
502 | print("ERR!")
503 |
504 | def find_page_id(self, page_name):
505 | if self.config["debug"]:
506 | print(f"INFO - * Mkdocs With Confluence: Find Page ID: PAGE NAME: {page_name}")
507 | name_confl = page_name.replace(" ", "+")
508 | url = self.config["host_url"] + "?title=" + name_confl + "&spaceKey=" + self.config["space"] + "&expand=history"
509 | if self.config["debug"]:
510 | print(f"URL: {url}")
511 | r = self.session.get(url)
512 | r.raise_for_status()
513 | with nostdout():
514 | response_json = r.json()
515 | if response_json["results"]:
516 | if self.config["debug"]:
517 | print(f"ID: {response_json['results'][0]['id']}")
518 | return response_json["results"][0]["id"]
519 | else:
520 | if self.config["debug"]:
521 | print("PAGE DOES NOT EXIST")
522 | return None
523 |
524 | def add_page(self, page_name, parent_page_id, page_content_in_storage_format):
525 | print(f"INFO - * Mkdocs With Confluence: {page_name} - *NEW PAGE*")
526 |
527 | if self.config["debug"]:
528 | print(f" * Mkdocs With Confluence: Adding Page: PAGE NAME: {page_name}, parent ID: {parent_page_id}")
529 | url = self.config["host_url"] + "/"
530 | if self.config["debug"]:
531 | print(f"URL: {url}")
532 | headers = {"Content-Type": "application/json"}
533 | space = self.config["space"]
534 | data = {
535 | "type": "page",
536 | "title": page_name,
537 | "space": {"key": space},
538 | "ancestors": [{"id": parent_page_id}],
539 | "body": {"storage": {"value": page_content_in_storage_format, "representation": "storage"}},
540 | }
541 | if self.config["debug"]:
542 | print(f"DATA: {data}")
543 | if not self.dryrun:
544 | r = self.session.post(url, json=data, headers=headers)
545 | r.raise_for_status()
546 | if r.status_code == 200:
547 | if self.config["debug"]:
548 | print("OK!")
549 | else:
550 | if self.config["debug"]:
551 | print("ERR!")
552 |
553 | def update_page(self, page_name, page_content_in_storage_format):
554 | page_id = self.find_page_id(page_name)
555 | print(f"INFO - * Mkdocs With Confluence: {page_name} - *UPDATE*")
556 | if self.config["debug"]:
557 | print(f" * Mkdocs With Confluence: Update PAGE ID: {page_id}, PAGE NAME: {page_name}")
558 | if page_id:
559 | page_version = self.find_page_version(page_name)
560 | page_version = page_version + 1
561 | url = self.config["host_url"] + "/" + page_id
562 | if self.config["debug"]:
563 | print(f"URL: {url}")
564 | headers = {"Content-Type": "application/json"}
565 | space = self.config["space"]
566 | data = {
567 | "id": page_id,
568 | "title": page_name,
569 | "type": "page",
570 | "space": {"key": space},
571 | "body": {"storage": {"value": page_content_in_storage_format, "representation": "storage"}},
572 | "version": {"number": page_version},
573 | }
574 |
575 | if not self.dryrun:
576 | r = self.session.put(url, json=data, headers=headers)
577 | r.raise_for_status()
578 | if r.status_code == 200:
579 | if self.config["debug"]:
580 | print("OK!")
581 | else:
582 | if self.config["debug"]:
583 | print("ERR!")
584 | else:
585 | if self.config["debug"]:
586 | print("PAGE DOES NOT EXIST YET!")
587 |
588 | def find_page_version(self, page_name):
589 | if self.config["debug"]:
590 | print(f"INFO - * Mkdocs With Confluence: Find PAGE VERSION, PAGE NAME: {page_name}")
591 | name_confl = page_name.replace(" ", "+")
592 | url = self.config["host_url"] + "?title=" + name_confl + "&spaceKey=" + self.config["space"] + "&expand=version"
593 | r = self.session.get(url)
594 | r.raise_for_status()
595 | with nostdout():
596 | response_json = r.json()
597 | if response_json["results"] is not None:
598 | if self.config["debug"]:
599 | print(f"VERSION: {response_json['results'][0]['version']['number']}")
600 | return response_json["results"][0]["version"]["number"]
601 | else:
602 | if self.config["debug"]:
603 | print("PAGE DOES NOT EXISTS")
604 | return None
605 |
606 | def find_parent_name_of_page(self, name):
607 | if self.config["debug"]:
608 | print(f"INFO - * Mkdocs With Confluence: Find PARENT OF PAGE, PAGE NAME: {name}")
609 | idp = self.find_page_id(name)
610 | url = self.config["host_url"] + "/" + idp + "?expand=ancestors"
611 |
612 | r = self.session.get(url)
613 | r.raise_for_status()
614 | with nostdout():
615 | response_json = r.json()
616 | if response_json:
617 | if self.config["debug"]:
618 | print(f"PARENT NAME: {response_json['ancestors'][-1]['title']}")
619 | return response_json["ancestors"][-1]["title"]
620 | else:
621 | if self.config["debug"]:
622 | print("PAGE DOES NOT HAVE PARENT")
623 | return None
624 |
625 | def wait_until(self, condition, interval=0.1, timeout=1):
626 | start = time.time()
627 | while not condition and time.time() - start < timeout:
628 | time.sleep(interval)
629 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pre-commit
2 | mime
3 | mistune
4 | md2cf
5 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | black
2 | flake8
3 | nose
4 | pytest-cov
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="mkdocs-with-confluence",
5 | version="0.2.7",
6 | description="MkDocs plugin for uploading markdown documentation to Confluence via Confluence REST API",
7 | keywords="mkdocs markdown confluence documentation rest python",
8 | url="https://github.com/pawelsikora/mkdocs-with-confluence/",
9 | author="Pawel Sikora",
10 | author_email="sikor6@gmail.com",
11 | license="MIT",
12 | python_requires=">=3.6",
13 | install_requires=["mkdocs>=1.1", "jinja2", "mistune", "md2cf", "requests"],
14 | packages=find_packages(),
15 | entry_points={"mkdocs.plugins": ["mkdocs-with-confluence = mkdocs_with_confluence.plugin:MkdocsWithConfluence"]},
16 | )
17 |
--------------------------------------------------------------------------------
/tests/test.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import mimetypes
3 |
4 | # -----------------------------------------------------------------------------
5 | # Globals
6 |
7 | BASE_URL = "/rest/api/content"
8 | SPACE_NAME = ""
9 | USERNAME = ""
10 | PASSWORD = ""
11 |
12 |
13 | def upload_attachment(page_id, filepath):
14 | url = BASE_URL + "/" + page_id + "/child/attachment/"
15 | headers = {"X-Atlassian-Token": "no-check"} # no content-type here!
16 | print(f"URL: {url}")
17 | filename = filepath
18 |
19 | # determine content-type
20 | content_type, encoding = mimetypes.guess_type(filename)
21 | if content_type is None:
22 | content_type = "multipart/form-data"
23 |
24 | # provide content-type explicitly
25 | files = {"file": (filename, open(filename, "rb"), content_type)}
26 | print(f"FILES: {files}")
27 |
28 | auth = (USERNAME, PASSWORD)
29 | r = requests.post(url, headers=headers, files=files, auth=auth)
30 | r.raise_for_status()
31 |
32 |
33 | def find_parent_name_of_page(name):
34 | idp = find_page_id(name)
35 | url = BASE_URL + "/" + idp + "?expand=ancestors"
36 | print(f"URL: {url}")
37 |
38 | auth = (USERNAME, PASSWORD)
39 | r = requests.get(url, auth=auth)
40 | r.raise_for_status()
41 | response_json = r.json()
42 | if response_json:
43 | print(f"ID: {response_json['ancestors'][0]['title']}")
44 | return response_json
45 | else:
46 | print("PAGE DOES NOT EXIST")
47 | return None
48 |
49 |
50 | def find_page_id(name):
51 | name_confl = name.replace(" ", "+")
52 | url = BASE_URL + "?title=" + name_confl + "&spaceKey=" + SPACE_NAME + "&expand=history"
53 | print(f"URL: {url}")
54 |
55 | auth = (USERNAME, PASSWORD)
56 | r = requests.get(url, auth=auth)
57 | r.raise_for_status()
58 | response_json = r.json()
59 | if response_json["results"]:
60 | print(f"ID: {response_json['results']}")
61 | return response_json["results"]
62 | else:
63 | print("PAGE DOES NOT EXIST")
64 | return None
65 |
66 |
67 | def add_page(page_name, parent_page_id):
68 | url = BASE_URL + "/"
69 | print(f"URL: {url}")
70 | headers = {"Content-Type": "application/json"}
71 | auth = (USERNAME, PASSWORD)
72 | data = {
73 | "type": "page",
74 | "title": page_name,
75 | "space": {"key": SPACE_NAME},
76 | "ancestors": [{"id": parent_page_id}],
77 | "body": {"storage": {"value": "This is a new page
", "representation": "storage"}},
78 | }
79 |
80 | r = requests.post(url, json=data, headers=headers, auth=auth)
81 | r.raise_for_status()
82 | print(r.json())
83 |
84 |
85 | def update_page(page_name):
86 | page_id = find_page_id(page_name)
87 | if page_id:
88 | page_version = find_page_version(page_name)
89 | page_version = page_version + 1
90 | print(f"PAGE ID: {page_id}, PAGE NAME: {page_name}")
91 | url = BASE_URL + "/" + page_id
92 | print(f"URL: {url}")
93 | headers = {"Content-Type": "application/json"}
94 | auth = (USERNAME, PASSWORD)
95 | data = {
96 | "type": "page",
97 | "space": {"key": SPACE_NAME},
98 | "body": {"storage": {"value": "Let the dragons out!
", "representation": "storage"}},
99 | "version": {"number": page_version},
100 | }
101 |
102 | data["id"] = page_id
103 | data["title"] = page_name
104 | print(data)
105 |
106 | r = requests.put(url, json=data, headers=headers, auth=auth)
107 | r.raise_for_status()
108 | print(r.json())
109 | else:
110 | print("PAGE DOES NOT EXIST. CREATING WITH DEFAULT BODY")
111 | add_page(page_name)
112 |
113 |
114 | def find_page_version(name):
115 | name_confl = name.replace(" ", "+")
116 | url = BASE_URL + "?title=" + name_confl + "&spaceKey=" + SPACE_NAME + "&expand=version"
117 |
118 | print(f"URL: {url}")
119 |
120 | auth = (USERNAME, PASSWORD)
121 | r = requests.get(url, auth=auth)
122 | r.raise_for_status()
123 | response_json = r.json()
124 | if response_json["results"]:
125 | print(f"VERSION: {response_json['results'][0]['version']['number']}")
126 | return response_json["results"][0]["version"]["number"]
127 | else:
128 | print("PAGE DOES NOT EXISTS")
129 | return None
130 |
131 |
132 | # add_page()
133 | # update_page("Test Page")
134 | # find_page_version("Test Page")
135 | # find_parent_name_of_page("Test Parent Page")
136 | # find_page_id("Test Page")
137 | # upload_attachment()
138 |
--------------------------------------------------------------------------------