diff --git a/.gitignore b/.gitignore index bee8a64..d9bf8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ __pycache__ +.cache +venv diff --git a/.woodpecker/readme.yml b/.woodpecker/readme.yml index 00b1ef7..142953a 100644 --- a/.woodpecker/readme.yml +++ b/.woodpecker/readme.yml @@ -1,10 +1,11 @@ when: - branch: [main] event: push + repo: fediverse/fep steps: build: - image: python:3.11-alpine + image: python:3.13-alpine commands: - ./scripts/update_issues.py - ./scripts/create_readme.py diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 3818563..bde8ac4 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -3,8 +3,19 @@ when: event: [pull_request, push] steps: - build: - image: python:3.11-alpine + check_create_readme_works: + image: python:3.13-alpine + commands: + - ./scripts/create_readme.py + + check_new_proposal_works: + image: python:3.13-alpine + commands: + - ./scripts/new_proposal.py Your Title Goes Here + - rm -rf fep/7f9d + + run_pytest: + image: python:3.13-alpine commands: - pip install pytest - pytest diff --git a/.woodpecker/website.yml b/.woodpecker/website.yml new file mode 100644 index 0000000..62822d3 --- /dev/null +++ b/.woodpecker/website.yml @@ -0,0 +1,18 @@ +when: + - branch: [main] + event: push + +steps: + build: + image: python:3.13-alpine + commands: + - pip install ./scripts + - pytest + - ./scripts/create_site_pages.py + - mkdocs build --config-file scripts/mkdocs.yml + deploy_pages: + image: codeberg.org/xfix/plugin-codeberg-pages-deploy:1 + settings: + folder: scripts/site + ssh_key: + from_secret: deploy_key diff --git a/README.md b/README.md index 901d2fd..7034c84 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ $ echo -n "The title of my proposal" | sha256sum | cut -c-4 b3f0 ``` -4. Create a subdirectory of [`fep/`](./fep/) using the identifier you just computed. -5. Copy the FEP template ([fep-xxxx-template.md](./fep-xxxx-template.md)) to this subdirectory and change the filename appropriately. +4. Create a subdirectory of [`fep/`](https://codeberg.org/fediverse/fep/src/branch/main/fep/) using the identifier you just computed. +5. Copy the FEP template ([fep-xxxx-template.md](https://codeberg.org/fediverse/fep/src/branch/main/fep-xxxx-template.md)) to this subdirectory and change the filename appropriately. 6. Use the identifer as the "slug" when filling out the frontmatter. - For example, if your computed identifier was `abcd`, then your file would be located at `fep/abcd/fep-abcd.md` and your frontmatter would include `slug: "abcd"`. @@ -33,7 +33,7 @@ b3f0 8. When you are ready to submit the FEP, change the value of `dateReceived` field in the frontmatter to the current date. 9. Create a discussion topic for your FEP. You can use [ActivityPub category](https://socialhub.activitypub.rocks/c/activitypub/5) on the SocialHub forum. 10. Add `discussionsTo` field containing the URL of the discussion topic to the front matter of your FEP. -11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed. +11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](https://codeberg.org/fediverse/fep/src/branch/main/fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed. Alternatively to steps 3. to 6., you can run @@ -45,7 +45,7 @@ that should create a prefilled template for you. ## Facilitators -The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues. +The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](https://codeberg.org/fediverse/fep/src/branch/main/FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues. ## Contributing @@ -53,8 +53,6 @@ Do you have ideas to improve the FEP Process? Post your suggestions to the issue ## FEPs - - | Title | Status | Tracking issue | `dateReceived` | `dateFinalized` (or `dateWithdrawn`) | | --- | --- | ----- | ------- | ------ | | [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md) | `FINAL` | [#201](https://codeberg.org/fediverse/fep/issues/201) | 2020-10-16 | 2021-01-18 | diff --git a/fep/7d8c/fep-7d8c.md b/fep/7d8c/fep-7d8c.md index 99a1591..c7a68ea 100644 --- a/fep/7d8c/fep-7d8c.md +++ b/fep/7d8c/fep-7d8c.md @@ -39,15 +39,22 @@ Automation for the FEP repository uses the [codeberg woodpecker](https://ci.code - test runs basic checks on the contained FEPs - readme updates the `README.md` file and creates the tracking issue +- create a static website Automation code is written in python and is contained in the [script folder](https://codeberg.org/fediverse/fep/src/branch/main/scripts). +### Scripts and tests + +FEP uses python scripts to manage some aspects of it. These should be runnable +without installing packages. In order to ensure these scripts stay working, +include them in [.woodpecker/test.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/test.yml) + ## Configuration variables To be able to do these configuration tasks, you need to be in the __admin__ group. See the first discussion [here](https://socialhub.activitypub.rocks/t/fep-repo-rights/4797/2). -The configuration [.woodpecker/readme.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/woodpecker.yml) uses two secrets: +The configuration [.woodpecker/readme.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/readme.yml) uses two secrets: - codeberg_api_token, a codeberg API token used to create the tracking issues. - deploy_key, an SSH private key used to push to the codeberg repository. @@ -74,7 +81,7 @@ ssh-keygen -t ed25519 -f deploy_key Do not specify a passphrase. Examples: -``` bash +```bash $ cat deploy_key -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW @@ -97,9 +104,56 @@ One can add these secrets on [woodpecker](https://ci.codeberg.org/repos/12388), The deploy_key needs to be end with a new line when pasting into the woodpecker ui. +## Development + +The tools are located in the `scripts` folder, with the `scripts/fep_tools` folder containing the python package `fep_tools`. Most of this tooling only uses python, e.g. +`scripts/new_proposal.py` and `scripts/create_readme.md`, so they can be run from any +environment. + +### Running tests + +To run the tests, one needs [pytest](https://docs.pytest.org/). We note that the +tests run from the base folder. The following commands demonstrate how to setup +a virtual environment, install the dependencies, and run the tests. + +```bash +python -mvenv .venv +pip install ./scripts +pytest +``` + +One can leave the venv, by running `deactivate`. + + +### Building the website + +To build the website, first install the dependencies + +```bash +python -mvenv .venv +pip install ./scripts +./scripts/create_site_pages.py +``` + +Then run mkdocs via + +```bash +mkdocs serve --config-file scripts/mkdocs.yml +``` + +References: + +- The website is build with [mkdocs](https://www.mkdocs.org/) +- and uses [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) for its many plugins + +### Automatic deployment + +See [website.yml](https://codeberg.org/fediverse/fep/src/branch/main/.woodpecker/website.yml). + ## Changes - Added discussion link to tracking issue, use table to format (@helge, 2025-03-07) +- Added static website stuff [fep#673](https://codeberg.org/fediverse/fep/pulls/673) ## References diff --git a/scripts/.gitignore b/scripts/.gitignore index 665f454..fa23a83 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -1 +1,10 @@ config*.json +site +docs/fep +docs/withdrawn.md +docs/final.md +docs/draft.md +docs/index.md +build +fep.egg-info +uv.lock diff --git a/scripts/create_issue.py b/scripts/create_issue.py index ca80976..6961383 100755 --- a/scripts/create_issue.py +++ b/scripts/create_issue.py @@ -1,7 +1,7 @@ #!/usr/bin/env python from argparse import ArgumentParser -from tools.issue import create_issue +from fep_tools.issue import create_issue import json @@ -12,5 +12,4 @@ args = parser.parse_args() with open("scripts/config.json") as f: config = json.load(f) -create_issue(config['owner'], config['repo'], config['token'], args.fep) - +create_issue(config["owner"], config["repo"], config["token"], args.fep) diff --git a/scripts/create_readme.py b/scripts/create_readme.py index 0269bd7..08fd0ba 100755 --- a/scripts/create_readme.py +++ b/scripts/create_readme.py @@ -2,17 +2,11 @@ import json -from tools import index, Readme +from fep_tools import data_for_json_file, Readme with open("README.md", "w") as f1: f1.writelines(Readme().content) -result = [] -for fep in index(): - data = fep.parsed_frontmatter - data["title"] = fep.title - data["implementations"] = fep.implementations - result.append(data) with open("index.json", "w") as index_file: - json.dump(result, index_file, indent=2) + json.dump(data_for_json_file(), index_file, indent=2) diff --git a/scripts/create_site_pages.py b/scripts/create_site_pages.py new file mode 100755 index 0000000..b1ea338 --- /dev/null +++ b/scripts/create_site_pages.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +from fep_tools import index +from fep_tools.site_fep_page import make_page_for_fep +from fep_tools.site_overview_page import write_overview_file + +from fep_tools import Readme + +path_prefix = "scripts/docs" + +with open(f"{path_prefix}/index.md", "w") as fp: + fp.write("""--- +hide: +- navigation +--- + +# Fediverse Enhancement Proposals + +
+ +- [:material-file-document: Final Proposals](./final.md) +- [:material-file-document-edit: Draft Proposals](./draft.md) + +
+ +""") + + fp.writelines(Readme(include_fep_table=False).content) + + +for file, status in [ + ("index.html", "FINAL"), + ("draft.html", "DRAFT"), + ("withdrawn.html", "WITHDRAWN"), +]: + filename = f"{path_prefix}/{status.lower()}.md" + + write_overview_file(filename, status) + + +for fep in index(): + make_page_for_fep(fep) diff --git a/scripts/docs/assets/codeberg.png b/scripts/docs/assets/codeberg.png new file mode 100644 index 0000000..0d5d02e Binary files /dev/null and b/scripts/docs/assets/codeberg.png differ diff --git a/scripts/docs/assets/logo.png b/scripts/docs/assets/logo.png new file mode 100644 index 0000000..6d958d0 Binary files /dev/null and b/scripts/docs/assets/logo.png differ diff --git a/scripts/docs/javascripts/tablesort.js b/scripts/docs/javascripts/tablesort.js new file mode 100644 index 0000000..4014300 --- /dev/null +++ b/scripts/docs/javascripts/tablesort.js @@ -0,0 +1,6 @@ +document$.subscribe(function () { + var tables = document.querySelectorAll("article table:not([class])"); + tables.forEach(function (table) { + new Tablesort(table); + }); +}); diff --git a/scripts/docs/javascripts/tablesort@5.3.0/tablesort.min.js b/scripts/docs/javascripts/tablesort@5.3.0/tablesort.min.js new file mode 100644 index 0000000..65a83b1 --- /dev/null +++ b/scripts/docs/javascripts/tablesort@5.3.0/tablesort.min.js @@ -0,0 +1,6 @@ +/*! + * tablesort v5.2.1 (2021-10-30) + * http://tristen.ca/tablesort/demo/ + * Copyright (c) 2021 ; Licensed MIT +*/ +!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a,b){return a.getAttribute(b.sortAttribute||"data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&n.push(m),o++;if(!n)return}for(o=0;o list[dict[str, str]]: + result = [] + for fep in index(): + data = fep.parsed_frontmatter + data["title"] = fep.title + data["implementations"] = fep.implementation_count + result.append(data) + + return result diff --git a/scripts/tools/fep_file.py b/scripts/fep_tools/fep_file.py similarity index 54% rename from scripts/tools/fep_file.py rename to scripts/fep_tools/fep_file.py index 6e50c4b..37b7225 100644 --- a/scripts/tools/fep_file.py +++ b/scripts/fep_tools/fep_file.py @@ -1,22 +1,38 @@ -def unquote(value): +from dataclasses import dataclass, field + + +def unquote(value: str) -> str: if value.startswith('"') and value.endswith('"'): return value[1:-1] else: return value +@dataclass class FepFile: - def __init__(self, fep): - self.fep = fep + fep: str + frontmatter: list[str] = field(default_factory=list) + content: list[str] = field(default_factory=list) + + def __post_init__(self): with open(self.filename) as f: - self.frontmatter, self.content = FepFile.parsefile(f) + lines = f.readlines() + + status = 0 + + for line in lines: + if line == "---\n" and status <= 2: + status += 1 + elif status == 1: + self.frontmatter.append(line.removesuffix("\n")) + elif status >= 2: + self.content.append(line.removesuffix("\n")) @property - def filename(self): + def filename(self) -> str: return f"fep/{self.fep}/fep-{self.fep}.md" - @property - def summary(self): + def find_section_by_name(self, name): result = [] is_summary = False for x in self.content: @@ -24,9 +40,13 @@ class FepFile: if x.startswith("##"): return "\n".join(result) result.append(x) - elif x == "## Summary": + elif x == f"## {name}": is_summary = True + @property + def summary(self): + return self.find_section_by_name("Summary") + def write(self): with open(self.filename, "w") as f: f.write("---\n") @@ -42,7 +62,11 @@ class FepFile: return {a: unquote(b.strip()) for a, b in split} @property - def title(self): + def status(self): + return self.parsed_frontmatter["status"] + + @property + def title(self) -> str: titles = [x for x in self.content if x.startswith("# ")] assert len(titles) > 0 @@ -56,25 +80,23 @@ class FepFile: return true_title @property - def implementations(self): + def implementation_count(self): if self.parsed_frontmatter.get("type") != "implementation": return 0 implementations = [] in_section = False for line in self.content: - if line.startswith('#') and "Implementations" in line: + if line.startswith("#") and "Implementations" in line: in_section = True - elif in_section is True and line.startswith('#'): + elif in_section is True and line.startswith("#"): in_section = False - elif ( - in_section is True - and (line.startswith("-") or line.startswith("*")) - ): + elif in_section is True and (line.startswith("-") or line.startswith("*")): implementations.append(line) + return len(implementations) @staticmethod - def parsefile(f): + def parsefile(f) -> tuple[list[str], list[str]]: lines = f.readlines() status = 0 @@ -90,3 +112,21 @@ class FepFile: content.append(line.removesuffix("\n")) return frontmatter, content + + def frontmatter_table(self) -> str: + keys = " | ".join(self.parsed_frontmatter.keys()) + values = " | ".join(self.parsed_frontmatter.values()) + divider = " | ".join(["---"] * len(self.parsed_frontmatter)) + + return f""" +| {keys} | +| {divider} | +| {values} | +""" + + def content_and_title(self) -> tuple[str, str]: + for j, line in enumerate(self.content): + if line.startswith("# "): + return line, "\n".join(self.content[j + 1 :]) + + raise Exception("Could not determine title and content") diff --git a/scripts/tools/issue.py b/scripts/fep_tools/issue.py similarity index 97% rename from scripts/tools/issue.py rename to scripts/fep_tools/issue.py index 3c221eb..33dfc43 100644 --- a/scripts/tools/issue.py +++ b/scripts/fep_tools/issue.py @@ -72,7 +72,7 @@ def update_fep_file_with_date_received(fep_file: FepFile, date_received: date): fep_file.frontmatter.append(f"dateReceived: {date_received.isoformat()}") -def create_issue(owner, repo, token, slug): +def create_issue(owner: str, repo: str, token: str, slug: str): fep_file = FepFile(slug) if "trackingIssue" in fep_file.parsed_frontmatter: diff --git a/scripts/fep_tools/site_fep_page.py b/scripts/fep_tools/site_fep_page.py new file mode 100644 index 0000000..87ac9ab --- /dev/null +++ b/scripts/fep_tools/site_fep_page.py @@ -0,0 +1,124 @@ +import re +import shutil +from pathlib import Path + +try: + import nh3 + from markdown import markdown +except Exception: + ... + +from .fep_file import FepFile +from .table import TableLineBuilder + + +def transform_key(x: str) -> str: + if x in ["dateReceived", "dateWithdrawn", "trackingIssue"]: + return re.sub(r"(? str: + """Creates a markdown table containing FEP metadata""" + matter_keys = [transform_key(x) for x in fep.parsed_frontmatter.keys()] + matter_keys = [x for x in possible_matter_keys if x in matter_keys] + ["repository"] + + line_builder = TableLineBuilder(fep=fep, columns=list(matter_keys)) + keys = " | ".join(x.replace("_", " ").capitalize() for x in matter_keys) + values = line_builder.line + + divider = " | ".join(["---"] * len(matter_keys)) + + return f""" +| {keys} | +| {divider} | +{values} +""" + + +def clean_markdown(text: str) -> str: + """Transforms markdown to a plain text string. To be used in the frontmatter""" + transformed = nh3.clean(markdown(text), tags=set()) + transformed = re.sub(r"\[(.+?)\]\[(.+?)\]", r"\1", transformed) + transformed = transformed.replace( + ":", " " + ) # See https://codeberg.org/fediverse/fep/pulls/673/files#issuecomment-7514311 + return transformed.replace("\n", " ") + + +def frontmatter_for_fep(fep: FepFile) -> str: + summary = fep.summary + if summary is None: + return """--- +hide: + - navigation +--- + +""" + description = clean_markdown(summary) + + return f"""--- +hide: + - navigation +description: {description} +--- + +""" + + +def transform_content(content: str) -> str: + """Transforms links to other FEPs. These need to be adjusted due to + the renaming of `fep-slug.md` to `index.md`.""" + content = re.sub( + r"\[(.+?)\]\((\.\./.+)/fep-(.+).md\)", r"[\1](\2/index.md)", content + ) + content = re.sub(r"\[(.+?)\]: (\.\./.+)/fep-(.+).md", r"[\1]: \2/index.md", content) + return content + + +def fep_to_site_mkdocs(fep: FepFile) -> str: + """Transforms the FEP to markdown suitable for publishing as a static website""" + title, content = fep.content_and_title() + + file_content = ( + frontmatter_for_fep(fep) + + title + + "\n\n" + + format_metatable(fep) + + "\n\n" + + transform_content(content) + ) + + return file_content + + +def make_page_for_fep(fep: FepFile): + """Copies relevant files for the FEP and adjusts index.md""" + slug = fep.fep + + base_path = f"scripts/docs/fep/{slug}/" + Path(base_path).mkdir(exist_ok=True, parents=True) + + shutil.rmtree(base_path) + shutil.copytree(f"fep/{slug}/", base_path) + shutil.move(f"{base_path}/fep-{slug}.md", f"{base_path}/index.md") + + with open(f"{base_path}/index.md", "w") as fp: + fp.write(fep_to_site_mkdocs(fep)) diff --git a/scripts/fep_tools/site_overview_page.py b/scripts/fep_tools/site_overview_page.py new file mode 100644 index 0000000..d5d9222 --- /dev/null +++ b/scripts/fep_tools/site_overview_page.py @@ -0,0 +1,39 @@ +from . import index, table_for_feps + +markdown_frontmatter = """--- +hide: +- toc +- navigation +--- +""" + + +def columns_for_status(status: str) -> list[str]: + columns = [ + "title", + "repo_link_image", + "tracking_issue", + "discussions", + "date_received", + ] + if status == "FINAL": + columns.append("date_finalized") + elif status == "WITHDRAWN": + columns.append("date_withdrawn") + return columns + + +def write_overview_file(filename: str, status: str) -> None: + columns = columns_for_status(status) + feps = [x for x in index() if x.status == status] + + table = "".join( + table_for_feps(feps, columns=columns, generate_title=True, format="static") + ) + + title = status[0] + status[1:].lower() + + with open(filename, "w") as f: + f.write(markdown_frontmatter) + f.write(f"\n# {title}\n") + f.write(table) diff --git a/scripts/fep_tools/table.py b/scripts/fep_tools/table.py new file mode 100644 index 0000000..6ace63a --- /dev/null +++ b/scripts/fep_tools/table.py @@ -0,0 +1,138 @@ +from dataclasses import dataclass + +from .fep_file import FepFile + + +repo_base = "https://codeberg.org/fediverse/fep/src/branch/main/" + + +@dataclass +class TableLineBuilder: + fep: FepFile + columns: list[str] + format: str = "readme" + + @property + def parsed(self): + return self.fep.parsed_frontmatter + + @property + def title(self): + if self.format == "readme": + return f"[FEP-{self.fep.fep}: {self.fep.title}](./{self.fep.filename})" + + return f"[FEP-{self.fep.fep}: {self.fep.title}](fep/{self.fep.fep}/index.md)" + + @property + def repo_link_image(self): + return f"[codeberg]({repo_base}{self.fep.filename})" + + @property + def tracking_issue(self): + url = self.parsed["trackingIssue"] + url_number = url.split("/")[-1] + return f"[#{url_number}]({url})" + + @property + def status(self): + status = self.parsed["status"] + return f"`{status}`" + + @property + def date_received(self): + return self.parsed.get("dateReceived", "-") + + @property + def date_finalized(self): + return self.parsed.get("dateFinalized", "-") + + @property + def date_withdrawn(self): + return self.parsed.get("dateWithdrawn", "-") + + @property + def date_final(self): + if "dateFinalized" in self.parsed: + return self.parsed["dateFinalized"] + if "dateWithdrawn" in self.parsed: + return self.parsed["dateWithdrawn"] + return "-" + + @property + def discussions(self): + discussions = self.parsed["discussionsTo"] + + if discussions == self.parsed["trackingIssue"]: + return "-" + + return f"[Discussions]({discussions})" + + @property + def repository(self) -> str: + url = f"{repo_base}fep/{self.fep.fep}/fep-{self.fep.fep}.md" + return f"[codeberg]({url})" + + def get_attribute(self, column: str): + try: + return self.__getattribute__(column) + except AttributeError: + return self.parsed.get(column, "-") + + @property + def line(self) -> str: + values = [self.get_attribute(column) for column in self.columns] + middle = " | ".join(values) + return f"| {middle} |\n" + + +@dataclass +class TableTitle: + columns: list[str] + + column_to_title = { + "title": "Title", + "status": "Status", + "tracking_issue": "Tracking issue", + "discussions": "Discussions", + "date_received": "Received", + "date_final": "Finalized / Withdrawn", + "date_finalized": "Finalized", + "date_withdrawn": "Withdrawn", + } + + @property + def title_line(self): + center = " | ".join( + [self.column_to_title.get(column, "") for column in self.columns] + ) + + return f"| {center} |\n" + + @property + def second_line(self): + center = " | ".join(["---"] * len(self.columns)) + return f"| {center} |\n" + + @property + def result(self) -> list[str]: + return [self.title_line, self.second_line] + + +def table_for_feps( + feps: list[FepFile], + columns: list[str] = [ + "title", + "status", + "tracking_issue", + "date_received", + # Finalized / Withdrawn + "date_final", + ], + generate_title: bool = False, + format: str = "readme", +): + lines = [TableLineBuilder(fep, columns, format=format).line for fep in feps] + if generate_title: + return TableTitle(columns).result + lines + + return lines diff --git a/scripts/fep_tools/test_fep_file.py b/scripts/fep_tools/test_fep_file.py new file mode 100644 index 0000000..7214fd7 --- /dev/null +++ b/scripts/fep_tools/test_fep_file.py @@ -0,0 +1,13 @@ +from . import fep_file + + +def test_fep_file(): + file = fep_file.FepFile("a4ed") + + assert file.status == "FINAL" + + +def test_implementation_count(): + file = fep_file.FepFile("8b32") + + assert file.implementation_count > 2 diff --git a/scripts/tools/test_feps.py b/scripts/fep_tools/test_feps.py similarity index 88% rename from scripts/tools/test_feps.py rename to scripts/fep_tools/test_feps.py index 5fb15be..4e447ed 100644 --- a/scripts/tools/test_feps.py +++ b/scripts/fep_tools/test_feps.py @@ -3,7 +3,7 @@ import pytest from urllib.parse import urlparse -from scripts.tools import get_fep_ids, FepFile, title_to_slug +from scripts.fep_tools import get_fep_ids, FepFile, title_to_slug @pytest.mark.parametrize("fep", get_fep_ids()) @@ -20,9 +20,9 @@ def test_fep_front_matter(fep): discussions_to = parsed_frontmatter["discussionsTo"] - assert not urlparse(discussions_to).netloc.endswith( - ".example" - ), "Update discussionsTo to a valid URL for a discussion topic" + assert not urlparse(discussions_to).netloc.endswith(".example"), ( + "Update discussionsTo to a valid URL for a discussion topic" + ) if parsed_frontmatter["status"] == "FINAL": assert "dateFinalized" in parsed_frontmatter @@ -36,6 +36,7 @@ def test_fep_front_matter(fep): if "type" in parsed_frontmatter: assert parsed_frontmatter["type"] in ["informational", "implementation"] + @pytest.mark.parametrize("fep", get_fep_ids()) def test_fep_content(fep): fep_file = FepFile(fep) diff --git a/scripts/tools/test_issue.py b/scripts/fep_tools/test_issue.py similarity index 100% rename from scripts/tools/test_issue.py rename to scripts/fep_tools/test_issue.py diff --git a/scripts/fep_tools/test_json.py b/scripts/fep_tools/test_json.py new file mode 100644 index 0000000..b564dfc --- /dev/null +++ b/scripts/fep_tools/test_json.py @@ -0,0 +1,12 @@ +import json +from . import data_for_json_file + + +def test_data_for_json_file(): + result = data_for_json_file() + + assert isinstance(result, list) + + dumped = json.dumps(result) + + assert isinstance(dumped, str) diff --git a/scripts/tools/test_readme.py b/scripts/fep_tools/test_readme.py similarity index 62% rename from scripts/tools/test_readme.py rename to scripts/fep_tools/test_readme.py index 0c79359..7bb511c 100644 --- a/scripts/tools/test_readme.py +++ b/scripts/fep_tools/test_readme.py @@ -1,6 +1,6 @@ import pytest -from scripts.tools import Readme +from scripts.fep_tools import Readme @pytest.mark.skip("Only is correct for main branch and not pull requests") @@ -11,3 +11,9 @@ def test_readme(): expected = Readme().content assert lines == expected + + +def test_readme_can_be_created(): + content = Readme().content + + assert isinstance(content, list) diff --git a/scripts/fep_tools/test_site_fep_page.py b/scripts/fep_tools/test_site_fep_page.py new file mode 100644 index 0000000..ed6bcf1 --- /dev/null +++ b/scripts/fep_tools/test_site_fep_page.py @@ -0,0 +1,71 @@ +import pytest +from . import fep_file +from .site_fep_page import format_metatable, clean_markdown, transform_content + + +def skip_due_to_no_nh3_markdown(): + try: + import nh3 # type: ignore # noqa + import markdown # type: ignore # noqa + except Exception: + return True + return False + + +def test_format_metatable(): + file = fep_file.FepFile("a4ed") + + table = format_metatable(file) + + assert ( + table + == """ +| Authors | Status | Date received | Date final | Tracking issue | Discussions | Repository | +| --- | --- | --- | --- | --- | --- | --- | +| pukkamustard | `FINAL` | 2020-10-16 | 2021-01-18 | [#201](https://codeberg.org/fediverse/fep/issues/201) | - | [codeberg](https://codeberg.org/fediverse/fep/src/branch/main/fep/a4ed/fep-a4ed.md) | + +""" + ) + + +@pytest.mark.skipif( + skip_due_to_no_nh3_markdown(), reason="nh3 and markdown not installed" +) +@pytest.mark.parametrize( + "text,expected", + [ + ("[link](http://test.example)", "link"), + ("[link][link]", "link"), + ("[a][a] [b][b]", "a b"), + ("a:b", "a b"), + ], +) +def test_clean_markdown(text: str, expected: str): + result = clean_markdown(text) + assert result == expected + + +@pytest.mark.skipif( + skip_due_to_no_nh3_markdown(), reason="nh3 and markdown not installed" +) +@pytest.mark.parametrize( + "unchanged", + [ + "[link](http://remote.tests)", + "[fep-f1d5](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)", + "[fep-f1d5]: https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md", + ], +) +def test_content_unchanged(unchanged: str): + assert transform_content(unchanged) == unchanged + + +@pytest.mark.skipif( + skip_due_to_no_nh3_markdown(), reason="nh3 and markdown not installed" +) +def test_content(): + changed = "[link](../4adb/fep-4adb.md)" + assert transform_content(changed) == "[link](../4adb/index.md)" + + changed = "[FEP-4adb]: ../4adb/fep-4adb.md" + assert transform_content(changed) == "[FEP-4adb]: ../4adb/index.md" diff --git a/scripts/fep_tools/test_table.py b/scripts/fep_tools/test_table.py new file mode 100644 index 0000000..c0e2377 --- /dev/null +++ b/scripts/fep_tools/test_table.py @@ -0,0 +1,33 @@ +from .fep_file import FepFile + +from .table import table_for_feps, TableTitle + + +def test_table_for_feps(): + fep = FepFile("a4ed") + result = table_for_feps([fep]) + + assert len(result) == 1 + line = result[0] + + assert ( + line + == "| [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md) | `FINAL` | [#201](https://codeberg.org/fediverse/fep/issues/201) | 2020-10-16 | 2021-01-18 |\n" + ) + + +def test_table_title(): + columns = [ + "title", + "tracking_issue", + "discussions", + "date_received", + "date_final", + ] + + result = TableTitle(columns).result + + assert result == [ + "| Title | Tracking issue | Discussions | Received | Finalized / Withdrawn |\n", + "| --- | --- | --- | --- | --- |\n", + ] diff --git a/scripts/impl_count.py b/scripts/impl_count.py index 8788274..a395482 100644 --- a/scripts/impl_count.py +++ b/scripts/impl_count.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from tools import index +from fep_tools import index result = [] print("| Title | Status | Count |") @@ -9,6 +9,8 @@ for fep in index(): slug = fep.parsed_frontmatter["slug"] status = fep.parsed_frontmatter["status"] typ = fep.parsed_frontmatter.get("type") or "-" - if fep.implementations == 0: + if fep.implementation_count == 0: continue - print(f"| [{fep.title}](https://codeberg.org/fediverse/fep/src/branch/main/fep/{slug}/fep-{slug}.md) | {status} | {fep.implementations} |") + print( + f"| [{fep.title}](https://codeberg.org/fediverse/fep/src/branch/main/fep/{slug}/fep-{slug}.md) | {status} | {fep.implementation_count} |" + ) diff --git a/scripts/mkdocs.yml b/scripts/mkdocs.yml new file mode 100644 index 0000000..a4dc088 --- /dev/null +++ b/scripts/mkdocs.yml @@ -0,0 +1,56 @@ +site_name: Fediverse Enhancement Proposals +site_url: https://helge.codeberg.page/fep +repo_url: https://codeberg.org/fediverse/fep +repo_name: fediverse/fep + +theme: + name: material + logo: assets/logo.png + favicon: assets/logo.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + font: false + +markdown_extensions: + - toc: + permalink: true + - github-callouts + - tables + - attr_list + - md_in_html + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - footnotes + +extra_javascript: + - javascripts/tablesort@5.3.0/tablesort.min.js + - javascripts/tablesort.js +extra_css: + - styles/theme.css +nav: + - index.md + - final.md + - draft.md + - withdrawn.md + +plugins: + - search diff --git a/scripts/new_proposal.py b/scripts/new_proposal.py index 143f87e..5c8e161 100755 --- a/scripts/new_proposal.py +++ b/scripts/new_proposal.py @@ -4,7 +4,7 @@ from argparse import ArgumentParser import os import shutil -from tools import title_to_slug, FepFile +from fep_tools import title_to_slug, FepFile def build_parser(): diff --git a/scripts/pull_request.py b/scripts/pull_request.py index d6da146..fa01775 100644 --- a/scripts/pull_request.py +++ b/scripts/pull_request.py @@ -1,6 +1,6 @@ from argparse import ArgumentParser from datetime import date, timedelta -from tools import FepFile +from fep_tools import FepFile import json diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml new file mode 100644 index 0000000..f6eff7d --- /dev/null +++ b/scripts/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "fep" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mkdocs>=1.6.1", + "mkdocs-material[imaging]>=9.5.40", + "pytest>=8.3.4", + "markdown>=3.7", + "pytest-watcher>=0.4.3", + "markdown-callouts>=0.4.0", + "fep", + "nh3>=0.3.0", +] + +[tool.setuptools] +packages = ["fep_tools"] diff --git a/scripts/backmatter.md b/scripts/snippets/backmatter.md similarity index 100% rename from scripts/backmatter.md rename to scripts/snippets/backmatter.md diff --git a/scripts/snippets/fep_title.md b/scripts/snippets/fep_title.md new file mode 100644 index 0000000..b863435 --- /dev/null +++ b/scripts/snippets/fep_title.md @@ -0,0 +1,4 @@ +## FEPs + +| Title | Status | Tracking issue | `dateReceived` | `dateFinalized` (or `dateWithdrawn`) | +| --- | --- | ----- | ------- | ------ | diff --git a/scripts/frontmatter.md b/scripts/snippets/frontmatter.md similarity index 67% rename from scripts/frontmatter.md rename to scripts/snippets/frontmatter.md index 9a8a5ca..96d29d2 100644 --- a/scripts/frontmatter.md +++ b/scripts/snippets/frontmatter.md @@ -1,9 +1,3 @@ -# Fediverse Enhancement Proposals - - - -This is the Git repository containing Fediverse Enhancment Proposals (FEPs). - A Fediverse Enhancement Proposal (FEP) is a document that provides information to the Fediverse community. The goal of a FEP is to improve interoperability and well-being of diverse services, applications and communities that form the Fediverse. The FEP Process is an initiative of the [SocialHub](https://socialhub.activitypub.rocks) developer community, a liaison of the [W3C Social Web Incubator Community Group](https://www.w3.org/community/SocialCG/). For ongoing and past discussion see the [SocialHub FEP category](https://socialhub.activitypub.rocks/c/standards/fep/54). @@ -23,8 +17,8 @@ $ echo -n "The title of my proposal" | sha256sum | cut -c-4 b3f0 ``` -4. Create a subdirectory of [`fep/`](./fep/) using the identifier you just computed. -5. Copy the FEP template ([fep-xxxx-template.md](./fep-xxxx-template.md)) to this subdirectory and change the filename appropriately. +4. Create a subdirectory of [`fep/`](https://codeberg.org/fediverse/fep/src/branch/main/fep/) using the identifier you just computed. +5. Copy the FEP template ([fep-xxxx-template.md](https://codeberg.org/fediverse/fep/src/branch/main/fep-xxxx-template.md)) to this subdirectory and change the filename appropriately. 6. Use the identifer as the "slug" when filling out the frontmatter. - For example, if your computed identifier was `abcd`, then your file would be located at `fep/abcd/fep-abcd.md` and your frontmatter would include `slug: "abcd"`. @@ -33,7 +27,7 @@ b3f0 8. When you are ready to submit the FEP, change the value of `dateReceived` field in the frontmatter to the current date. 9. Create a discussion topic for your FEP. You can use [ActivityPub category](https://socialhub.activitypub.rocks/c/activitypub/5) on the SocialHub forum. 10. Add `discussionsTo` field containing the URL of the discussion topic to the front matter of your FEP. -11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](./fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed. +11. Create a Pull Request to complete Step 1 of [FEP-a4ed: The Fediverse Enhancement Proposal Process](https://codeberg.org/fediverse/fep/src/branch/main/fep/a4ed/fep-a4ed.md). Further process is described in FEP-a4ed. Alternatively to steps 3. to 6., you can run @@ -45,15 +39,9 @@ that should create a prefilled template for you. ## Facilitators -The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues. +The list of FEP's is maintained by the facilitators who are listed in the [FACILITATORS.md](https://codeberg.org/fediverse/fep/src/branch/main/FACILITATORS.md) file. Facilitators are neutral custodians of the FEP process, who merge PR's and create tracking issues. ## Contributing Do you have ideas to improve the FEP Process? Post your suggestions to the issue tracker, or on the [SocialHub](https://socialhub.activitypub.rocks) forum. The SocialHub developer community is a "DoOcracy" which means: “pick up any task you want, and then steer it to completion”. Your contributions are most welcome, so delve in and find out how you can help. -## FEPs - - - -| Title | Status | Tracking issue | `dateReceived` | `dateFinalized` (or `dateWithdrawn`) | -| --- | --- | ----- | ------- | ------ | diff --git a/scripts/snippets/frontmatter_git.md b/scripts/snippets/frontmatter_git.md new file mode 100644 index 0000000..454aabc --- /dev/null +++ b/scripts/snippets/frontmatter_git.md @@ -0,0 +1,6 @@ +# Fediverse Enhancement Proposals + + + +This is the Git repository containing Fediverse Enhancment Proposals (FEPs). + diff --git a/scripts/tools/__init__.py b/scripts/tools/__init__.py deleted file mode 100644 index e772147..0000000 --- a/scripts/tools/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -import glob -import hashlib - -from .fep_file import FepFile - - -def title_to_slug(title): - return hashlib.sha256(title.encode("utf-8")).hexdigest()[:4] - - -def get_fep_ids(): - for fep in glob.glob("fep/*"): - yield fep.removeprefix("fep/") - - -def build_url_link(url): - url_number = url.split("/")[-1] - return f"[#{url_number}]({url})" - - -def index(): - fep_files = [FepFile(fep) for fep in get_fep_ids()] - fep_files = sorted( - fep_files, - key=lambda x: (x.parsed_frontmatter["dateReceived"], x.parsed_frontmatter["slug"]), - ) - return fep_files - - -class Readme: - @property - def content(self): - return self.frontmatter + self.table + self.backmatter - - @property - def frontmatter(self): - with open("scripts/frontmatter.md") as f: - return f.readlines() - - @property - def backmatter(self): - with open("scripts/backmatter.md") as f: - return f.readlines() - - @property - def table(self): - result = [] - - for fep in index(): - link = f"[FEP-{fep.fep}: {fep.title}](./{fep.filename})" - parsed = fep.parsed_frontmatter - - tracking_issue = build_url_link(parsed["trackingIssue"]) - - if "dateFinalized" in parsed: - date_final = parsed["dateFinalized"] - elif "dateWithdrawn" in parsed: - date_final = parsed["dateWithdrawn"] - else: - date_final = "-" - result.append( - f"""| {link} | `{parsed["status"]}` | {tracking_issue} | {parsed["dateReceived"]} | {date_final} |\n""" - ) - return result diff --git a/scripts/update_issues.py b/scripts/update_issues.py index a79013c..3f11d88 100755 --- a/scripts/update_issues.py +++ b/scripts/update_issues.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -from tools import get_fep_ids, FepFile -from tools.issue import create_issue +from fep_tools import get_fep_ids, FepFile +from fep_tools.issue import create_issue import os @@ -9,10 +9,23 @@ owner = os.environ.get("CI_REPO_OWNER") repo = os.environ.get("CI_REPO_NAME") token = os.environ.get("CODEBERG_API_TOKEN") +if not isinstance(owner, str): + print("Please set the owner of the repostory at CI_REPO_OWNER") + exit(1) + +if not isinstance(repo, str): + print("Please set the repository at CI_REPO_NAME") + exit(1) + +if not isinstance(token, str): + print("Please provide a codeberg api token") + exit(1) + + for slug in get_fep_ids(): fep_file = FepFile(slug) tracking_issue = fep_file.parsed_frontmatter.get("trackingIssue") if not tracking_issue: print(slug, tracking_issue) - create_issue(owner, repo, token, slug) \ No newline at end of file + create_issue(owner, repo, token, slug)