diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7f35593 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +bin/ +lib/ +test/ +__pycache__/ +Dockerfile +pyvenv.cfg diff --git a/.gitignore b/.gitignore index 082ffd7..30108cf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ lib64/ parts/ sdist/ var/ +bin/ wheels/ pip-wheel-metadata/ share/python-wheels/ @@ -128,4 +129,5 @@ dmypy.json # Pyre type checker .pyre/ -.vscode/ \ No newline at end of file +.vscode/ +pyvenv.cfg diff --git a/Dockerfile b/Dockerfile index 14c70f2..ca6bd43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,16 @@ FROM python:3-alpine -WORKDIR /usr/src/app - -RUN apk add --no-cache git +ENV CONFLUENCE_USERNAME="" +ENV CONFLUENCE_PASSWORD="" +ENV CONFLUENCE_API_URL="" +ENV CONFLUENCE_SPACE="" +ENV CONFLUENCE_ANCESTOR_ID="" -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt +WORKDIR /usr/src/app COPY . . +RUN apk add --no-cache git \ + && pip install --no-cache-dir -r requirements.txt + CMD [ "python", "./markdown-to-confluence.py" ] \ No newline at end of file diff --git a/confluence.py b/confluence.py index ebf09d9..1c6c877 100644 --- a/confluence.py +++ b/confluence.py @@ -7,6 +7,7 @@ API_HEADERS = { 'User-Agent': 'markdown-to-confluence', + 'X-Atlassian-Token': 'no-check' } MULTIPART_HEADERS = { @@ -32,7 +33,7 @@ def __init__(self, dry_run=False, _client=None): """Creates a new Confluence API client. - + Arguments: api_url {str} -- The URL to the Confluence API root (e.g. https://wiki.example.com/api/rest/) username {str} -- The Confluence service account username @@ -64,7 +65,7 @@ def __init__(self, def _require_kwargs(self, kwargs): """Ensures that certain kwargs have been provided - + Arguments: kwargs {dict} -- The dict of required kwargs """ @@ -115,16 +116,16 @@ def _request(self, if not response.ok: log.info('''{method} {url}: {status_code} {reason} - Params: {params} - Data: {data} - Files: {files}'''.format(method=method, - url=url, - status_code=response.status_code, - reason=response.reason, - params=params, - data=data, - files=files)) - print(response.content) + Params: {params} + Data: {data} + Files: {files}'''.format(method=method, + url=url, + status_code=response.status_code, + reason=response.reason, + params=params, + data=data, + files=files)) + return response.content # Will probably want to be more robust here, but this should work for now @@ -149,7 +150,7 @@ def exists(self, space=None, slug=None, ancestor_id=None): Specifically, this leverages a Confluence Query Language (CQL) query against the Confluence API. We assume that each slug is unique, at least to the provided space/ancestor_id. - + Arguments: space {str} -- The Confluence space to use for filtering posts slug {str} -- The page slug @@ -179,7 +180,7 @@ def create_labels(self, page_id=None, slug=None, tags=[]): We specifically require a slug to be provided, since this is how we determine if a page exists. Any other tags are optional. - + Keyword Arguments: page_id {str} -- The ID of the existing page to which the label should apply slug {str} -- The page slug to use as the label value @@ -242,7 +243,7 @@ def _create_page_payload(self, def get_attachments(self, post_id): """Gets the attachments for a particular Confluence post - + Arguments: post_id {str} -- The Confluence post ID """ @@ -251,7 +252,7 @@ def get_attachments(self, post_id): def upload_attachment(self, post_id=None, attachment_path=None): """Uploads an attachment to a Confluence post - + Keyword Arguments: post_id {str} -- The Confluence post ID attachment_path {str} -- The absolute path to the attachment @@ -276,12 +277,21 @@ def get_author(self, username): username {str} -- The Confluence username """ log.info('Looking up Confluence user key for {}'.format(username)) - response = self.get(path='user', params={'username': username}) - if not isinstance(response, dict) or not response.get('userKey'): + response = self.get(path='user', params={'accountId': username}) + if not isinstance(response, dict) or not response.get('accountId'): log.error('No Confluence user key for {}'.format(username)) return {} return response + def get_current_user(self): + """Returns the Confluence author profile for the connected username + """ + log.info('Looking up current Confluence user key') + response = self.get(path='user/current') + if not isinstance(response, dict) or not response.get('accountId'): + return {} + return response + def create(self, content=None, space=None, @@ -295,7 +305,7 @@ def create(self, If an ancestor_id is specified, then the page will be created as a child of that ancestor page. - + Keyword Arguments: content {str} -- The HTML content to upload (required) space {str} -- The Confluence space where the page should reside @@ -318,9 +328,13 @@ def create(self, ancestor_id=ancestor_id, space=space, type=type) - response = self.post(path='content/', data=page) + response = self.post(path='content/', data=page) + # print(response) page_id = response['id'] + # to resuem, we can update the labels on the page + self.create_labels(page_id=page_id, slug=slug, tags=tags) + page_url = urljoin(self.api_url, response['_links']['webui']) log.info('Page "{title}" (id {page_id}) created successfully at {url}'. @@ -353,7 +367,7 @@ def update(self, This involves updating the attachments stored on Confluence, uploading the page content, and finally updating the labels. - + Keyword Arguments: post_id {str} -- The ID of the Confluence post content {str} -- The page represented in Confluence storage format diff --git a/convert.py b/convert.py index 0e82ddb..4cd00b1 100644 --- a/convert.py +++ b/convert.py @@ -39,13 +39,14 @@ def convtoconf(markdown, front_matter={}): author_keys = front_matter.get('author_keys', []) renderer = ConfluenceRenderer(authors=author_keys) - content_html = mistune.markdown(markdown, renderer=renderer) - page_html = renderer.layout(content_html) + markdown_html = mistune.create_markdown(renderer=renderer) + # content_html = mistune.html(markdown, renderer=renderer) + page_html = markdown_html(markdown) return page_html, renderer.attachments -class ConfluenceRenderer(mistune.Renderer): +class ConfluenceRenderer(mistune.HTMLRenderer): def __init__(self, authors=[]): self.attachments = [] if authors is None: @@ -68,7 +69,7 @@ def layout(self, content): | (30% width) | (800px width) | | | | ------------------------------------------ - + Arguments: content {str} -- The HTML of the content """ @@ -91,24 +92,24 @@ def layout(self, content): main_content = column.format(width='800px', content=content) return sidebar + main_content - def header(self, text, level, raw=None): + def heading(self, text, level): """Processes a Markdown header. In our case, this just tells us that we need to render a TOC. We don't actually do any special rendering for headers. """ self.has_toc = True - return super().header(text, level, raw) + return super().heading(text, level) def render_authors(self): """Renders a header that details which author(s) published the post. This is used since Confluence will show the post published as our service account. - + Arguments: author_keys {str} -- The Confluence user keys for each post author - + Returns: str -- The HTML to prepend to the post specifying the authors """ @@ -121,7 +122,7 @@ def render_authors(self): for user_key in self.authors) return '
{}
'.format(author_content) - def block_code(self, code, lang): + def block_code(self, code, lang=None): return textwrap.dedent('''\