Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into adrdir
Browse files Browse the repository at this point in the history
  • Loading branch information
colindean committed Sep 29, 2023
2 parents 6a1bf63 + 763e536 commit 341d179
Show file tree
Hide file tree
Showing 12 changed files with 474 additions and 205 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/pre-merge-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jobs:
fail-fast: false
matrix:
python_version:
- '3.7'
- '3.8'
- '3.9'
- '3.10'
Expand All @@ -30,6 +29,8 @@ jobs:
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Check formatting
run: black --check adr_viewer
- name: Run tests
run: pytest
- name: Run typecheck
Expand Down
115 changes: 25 additions & 90 deletions adr_viewer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,12 @@
from typing import List, Iterator, Optional, Dict

import glob
from jinja2.loaders import FileSystemLoader
import mistune
import os
from bs4 import BeautifulSoup
from jinja2 import Environment, PackageLoader, select_autoescape
import click
from bottle import Bottle, run


def extract_statuses_from_adr(page_object) -> Iterator[str]:
status_section = page_object.find('h2', text='Status')

if status_section and status_section.nextSibling:
current_node = status_section.nextSibling

while current_node.name != 'h2' and current_node.nextSibling:
current_node = current_node.nextSibling

if current_node.name == 'p':
yield current_node.text
elif current_node.name == 'ul':
yield from (li.text for li in current_node.children if li.name == "li")
else:
continue
from typing import List

from click import option, command

def parse_adr_to_config(path) -> Optional[Dict]:
adr_as_html = mistune.markdown(open(path).read())

soup = BeautifulSoup(adr_as_html, features='html.parser')

statuses = list(extract_statuses_from_adr(soup))

if any([line.startswith("Amended by") for line in statuses]):
status = 'amended'
elif any([line.startswith("Accepted") for line in statuses]):
status = 'accepted'
elif any([line.startswith("Superseded by") for line in statuses]):
status = 'superseded'
elif any([line.startswith("Proposed") or line.startswith("Pending") for line in statuses]):
status = 'pending'
else:
status = 'unknown'

header = soup.find('h1')

if header:
return {
'status': status,
'body': adr_as_html,
'title': header.text
}
else:
return None


def render_html(config, template_dir_override=None) -> str:

env = Environment(
loader=PackageLoader('adr_viewer', 'templates') if template_dir_override is None else FileSystemLoader(template_dir_override),
autoescape=select_autoescape(['html', 'xml'])
)

template = env.get_template('index.html')

return template.render(config=config)
from adr_viewer.parse import parse_adr
from adr_viewer.render import render_html
from adr_viewer.server import run_server


def get_adr_files(path) -> List[str]:
Expand All @@ -75,74 +15,69 @@ def get_adr_files(path) -> List[str]:
return files


def run_server(content, port) -> None:
print(f'Starting server at http://localhost:{port}/')
app = Bottle()
app.route('/', 'GET', lambda: content)
run(app, host='localhost', port=port, quiet=True)


def generate_content(path, template_dir_override=None, title=None) -> str:

files = get_adr_files("%s/*.md" % path)

config = {
'project_title': title if title else os.path.basename(os.getcwd()),
'records': []
"project_title": title if title else os.path.basename(os.getcwd()),
"records": [],
}

for index, adr_file in enumerate(files):

adr_attributes = parse_adr_to_config(adr_file)
markdown = open(adr_file).read()
adr_attributes = parse_adr(markdown)

if adr_attributes:
adr_attributes['index'] = index
adr_attributes.index = index

config['records'].append(adr_attributes)
config["records"].append(adr_attributes)
else:
print("Could not parse %s in ADR format, ignoring." % adr_file)

return render_html(config, template_dir_override)


CONVENTIONAL_ADR_DIR = 'doc/adr/'
DEFAULT_ADR_DIR_FILE = '.adr-dir'
CONVENTIONAL_ADR_DIR = "doc/adr/"
DEFAULT_ADR_DIR_FILE = ".adr-dir"


def resolve_adr_dir(maybe_dir=None, adr_dir_file=DEFAULT_ADR_DIR_FILE):
"""
If passed something, blindly use it. Otherwise, resolve based on
conventions in the ADR tooling ecosystem.
"""

def lookup():
adr_dir = CONVENTIONAL_ADR_DIR
if os.path.exists(adr_dir_file):
with open(adr_dir_file, 'r') as file:
with open(adr_dir_file, "r") as file:
adr_dir = file.read().strip()
return adr_dir

return maybe_dir if maybe_dir else lookup()


@click.command()
@click.option('--adr-path',
# fmt: off
@command()
@option('--adr-path',
default=None,
help=f"""
Directory containing ADR files; pass explicitly,
read {DEFAULT_ADR_DIR_FILE} if it exists or uses {CONVENTIONAL_ADR_DIR}
""",
show_default=True)
@click.option('--output', default='index.html', help='File to write output to.', show_default=True)
@click.option('--title', default=None, help='Set the project title', show_default=True)
@click.option('--serve', default=False, help='Serve content at http://localhost:8000/', is_flag=True)
@click.option('--port', default=8000, help='Change port for the server', show_default=True)
@click.option('--template-dir', default=None, help='Template directory.', show_default=True)
@option('--output', default='index.html', help='File to write output to.', show_default=True)
@option('--title', default=None, help='Set the project title', show_default=True)
@option('--serve', default=False, help='Serve content at http://localhost:8000/', is_flag=True)
@option('--port', default=8000, help='Change port for the server', show_default=True)
@option('--template-dir', default=None, help='Template directory.', show_default=True)
# fmt: on
def main(adr_path, output, title, serve, port, template_dir) -> None:
adr_path = resolve_adr_dir(adr_path)
content = generate_content(adr_path, template_dir, title)

if serve:
run_server(content, port)
else:
with open(output, 'w') as out:
with open(output, "w") as out:
out.write(content)
58 changes: 58 additions & 0 deletions adr_viewer/parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import Iterator, Optional, Dict
from bs4 import BeautifulSoup
from dataclasses import dataclass

import mistune


@dataclass
class Adr:
title: str
status: str
body: str
index: int = 0


def extract_statuses_from_adr(page_object) -> Iterator[str]:
status_section = page_object.find("h2", text="Status")

if status_section and status_section.nextSibling:
current_node = status_section.nextSibling

while current_node.name != "h2" and current_node.nextSibling:
current_node = current_node.nextSibling

if current_node.name == "p":
yield current_node.text
elif current_node.name == "ul":
yield from (li.text for li in current_node.children if li.name == "li")
else:
continue


def parse_adr(content: str) -> Optional[Adr]:
adr_as_html = mistune.markdown(content)

soup = BeautifulSoup(adr_as_html, features="html.parser")

statuses = list(extract_statuses_from_adr(soup))

if any([line.startswith("Amended by") for line in statuses]):
status = "amended"
elif any([line.startswith("Accepted") for line in statuses]):
status = "accepted"
elif any([line.startswith("Superseded by") for line in statuses]):
status = "superseded"
elif any(
[line.startswith("Proposed") or line.startswith("Pending") for line in statuses]
):
status = "pending"
else:
status = "unknown"

header = soup.find("h1")

if header:
return Adr(header.text, status, adr_as_html)
else:
return None
20 changes: 20 additions & 0 deletions adr_viewer/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from jinja2 import Environment, PackageLoader, select_autoescape
from jinja2.loaders import FileSystemLoader, BaseLoader


def render_html(config, template_dir_override=None) -> str:
loader: BaseLoader

if template_dir_override:
loader = FileSystemLoader(template_dir_override)
else:
loader = PackageLoader("adr_viewer", "templates")

env = Environment(
loader=loader,
autoescape=select_autoescape(["html", "xml"]),
)

template = env.get_template("index.html")

return template.render(config=config)
8 changes: 8 additions & 0 deletions adr_viewer/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from bottle import Bottle, run


def run_server(content, port) -> None:
print(f"Starting server at http://localhost:{port}/")
app = Bottle()
app.route("/", "GET", lambda: content)
run(app, host="localhost", port=port, quiet=True)
110 changes: 0 additions & 110 deletions adr_viewer/test_adr_viewer.py

This file was deleted.

Loading

0 comments on commit 341d179

Please sign in to comment.