Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import sys
from datetime import date
from os import environ, mkdir, path
from shutil import rmtree
Expand All @@ -6,6 +8,9 @@

from pyinfra import __version__, local

sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
import metadata # noqa # this is a local module

copyright = "Nick Barrett {0} — pyinfra v{1}".format(
date.today().year,
__version__,
Expand Down Expand Up @@ -57,9 +62,39 @@
]


def rstjinja(app, docname, source):
"""
Render our pages as a jinja template for fancy templating goodness.
"""
# this should only be run when building html
if app.builder.format != "html":
return
src = source[0]
rendered = app.builder.templates.render_string(src, app.config.html_context)
source[0] = rendered


def setup(app):
app.connect("source-read", rstjinja)
this_dir = path.dirname(path.realpath(__file__))
scripts_dir = path.abspath(path.join(this_dir, "..", "scripts"))
metadata_file = path.abspath(path.join(this_dir, "..", "pyinfra-metadata.toml"))
if not path.exists(metadata_file):
raise ValueError("No pyinfra-metadata.toml in project root")
with open(metadata_file, "r") as file:
metadata_text = file.read()
plugins = metadata.parse_plugins(metadata_text)

operation_plugins = [p for p in plugins if p.type == "operation"]
fact_plugins = [p for p in plugins if p.type == "fact"]
html_context = {
"operation_plugins": operation_plugins,
"fact_plugins": fact_plugins,
"tags": metadata.ALLOWED_TAGS,
"docs_language": language,
"docs_version": version,
}
app.config.html_context = html_context

for auto_docs_name in ("operations", "facts", "apidoc", "connectors"):
auto_docs_path = path.join(this_dir, auto_docs_name)
Expand Down
69 changes: 48 additions & 21 deletions docs/facts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,53 @@ You can leverage facts within :doc:`operations <using-operations>` like this:

**Want a new fact?** Check out :doc:`the writing facts guide <./api/operations>`.

Facts, like :doc:`operations <operations>`, are namespaced as different modules - shortcuts to each of these can be found in the sidebar.

.. raw:: html

<style type="text/css">
#facts-index .toctree-wrapper > ul {
padding: 0;
}
#facts-index .toctree-wrapper > ul > li {
padding: 0;
list-style: none;
margin: 20px 0;
}
#facts-index .toctree-wrapper > ul > li > ul > li {
display: inline-block;
}
</style>

.. toctree::
:maxdepth: 2
:glob:

facts/*
<div class="container my-4">
<!-- Dropdown Filter -->
<div class="mb-4">
<label for="tag-dropdown" class="form-label">Filter by Tag:</label>
<select class="form-select" id="tag-dropdown">
<option value="All">All</option>
{% for tag in tags %}
<option value="{{ tag.title_case }}">{{ tag.title_case }}</option>
{% endfor %}
</select>
</div>

<!-- Cards Grid -->
<div class="row" id="card-container">
{% for plugin in fact_plugins %}
<div class="col-md-4 mb-4 card-item">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">
<a href="{{ docs_language }}/{{ docs_version }}/facts/{{ plugin.name }}.html">
{{ plugin.name }}
</a>
<p class="card-text">{{ plugin.description }}</p>
{% for tag in plugin.tags %}
<span class="badge bg-secondary">{{ tag.title_case }}</span>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
document.getElementById('tag-dropdown').addEventListener('change', function () {
const selectedTag = this.value;
const cards = document.querySelectorAll('.card-item');

cards.forEach(card => {
const tags = Array.from(card.querySelectorAll('.badge')).map(badge => badge.textContent.trim());

if (selectedTag === 'All' || tags.includes(selectedTag)) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
});
</script>
80 changes: 80 additions & 0 deletions docs/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Support parsing pyinfra plugins and parsing
their metadata to docs generator.
"""

import tomllib
from dataclasses import dataclass
from typing import Literal

AllowedTagType = Literal[
"boot",
"containers",
"database",
"service-management",
"package-manager",
"python",
"ruby",
"javascript",
"configuration-management",
"security",
"storage",
"system",
"system-ops",
"system-facts",
"rust",
"version-control-system",
]


@dataclass(frozen=True)
class Tag:
"""Representation of a plugin tag."""

value: AllowedTagType

def __post_init__(self):
allowed_tags = set(AllowedTagType.__args__)
if self.value not in allowed_tags:
raise ValueError(f"Invalid tag: {self.value}. Allowed: {allowed_tags}")

@property
def title_case(self) -> str:
return " ".join([t.title() for t in self.value.split("-")])

def __eq__(self, other):
if isinstance(other, Tag):
return self.value == other.value
if isinstance(other, str):
return self.value == other
return NotImplemented


ALLOWED_TAGS = [Tag(tag) for tag in set(AllowedTagType.__args__)]


@dataclass
class Plugin:
"""Representation of a pyinfra plugin."""

name: str
# description: str # FUTURE we should grab these from doc strings
path: str
type: Literal["operation", "fact", "connector", "deploy"]
tags: list[Tag]


def parse_plugins(metadata_text: str) -> list[Plugin]:
"""Given the contents of a pyinfra-metadata.toml parse out the plugins."""
pyinfra_metadata = tomllib.loads(metadata_text).get("pyinfra", None)
if not pyinfra_metadata:
raise ValueError("Missing [pyinfra.plugins] section in pyinfra-metadata.toml")

plugins = []
for p in pyinfra_metadata["plugins"]:
data = pyinfra_metadata["plugins"][p]
# ensure Tag types and not strings
data["tags"] = [Tag(t) for t in data["tags"]]
plugin = Plugin(**data)
plugins.append(plugin)
return plugins
96 changes: 49 additions & 47 deletions docs/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,52 @@ Operations are used to describe changes to make to systems in the inventory. Use

.. raw:: html

<h3>Popular operations by category</h3>

.. admonition:: Basics
:class: note inline

:doc:`operations/files`, :doc:`operations/server`, :doc:`operations/git`, :doc:`operations/systemd`

.. admonition:: System Packages
:class: note inline

:doc:`operations/apt`, :doc:`operations/apk`, :doc:`operations/brew`, :doc:`operations/dnf`, :doc:`operations/yum`

.. admonition:: Language Packages
:class: note inline

:doc:`operations/gem`, :doc:`operations/npm`, :doc:`operations/pip`

.. admonition:: Databases
:class: note inline

:doc:`operations/postgresql`, :doc:`operations/mysql`

.. raw:: html

<h3>All operations alphabetically</h3>

.. raw:: html

<style type="text/css">
#operations-index .toctree-wrapper > ul {
padding: 0;
}
#operations-index .toctree-wrapper > ul > li {
padding: 0;
list-style: none;
margin: 20px 0;
}
#operations-index .toctree-wrapper > ul > li > ul > li {
display: inline-block;
}
</style>

.. toctree::
:maxdepth: 2
:glob:

operations/*
<div class="container my-4">
<!-- Dropdown Filter -->
<div class="mb-4">
<label for="tag-dropdown" class="form-label">Filter by Tag:</label>
<select class="form-select" id="tag-dropdown">
<option value="All">All</option>
{% for tag in tags %}
<option value="{{ tag.title_case }}">{{ tag.title_case }}</option>
{% endfor %}
</select>
</div>

<!-- Cards Grid -->
<div class="row" id="card-container">
{% for plugin in operation_plugins %}
<div class="col-md-4 mb-4 card-item">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">
<a href="{{ docs_language }}/{{ docs_version }}/operations/{{ plugin.name }}.html">
{{ plugin.name }}
</a>
</h5>
<p class="card-text">{{ plugin.description }}</p>
{% for tag in plugin.tags %}
<span class="badge bg-secondary">{{ tag.title_case }}</span>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
document.getElementById('tag-dropdown').addEventListener('change', function () {
const selectedTag = this.value;
const cards = document.querySelectorAll('.card-item');

cards.forEach(card => {
const tags = Array.from(card.querySelectorAll('.badge')).map(badge => badge.textContent.trim());

if (selectedTag === 'All' || tags.includes(selectedTag)) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
});
</script>
57 changes: 57 additions & 0 deletions pyinfra-metadata-schema-1.0.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "pyinfra-metadata Configuration",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"pyinfra": {
"type": "object",
"properties": {
"plugins": {
"type": "object",
"minProperties": 1,
"patternProperties": {
"^.*$": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"operation",
"fact",
"connector"
]
},
"path": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"name",
"type",
"path",
"tags"
],
"additionalProperties": false
}
}
}},
"required": [
"plugins"
],
"additionalProperties": false
}
},
"required": ["pyinfra"]
}
Loading