Skip to content

Commit 82e580b

Browse files
rod7760Fizzadar
andauthored
add metadata spec for pyinfra plugins
This is a start on improving plugin discovery in pyinfra. So far operations/facts have all been builtins and the lack of discovery/organisation has meant little sharing of external ops/facts (ie, plugins). As an example the metadata structure here might provide a way to include plugins within the main pyinfra documentation. There's a lot of potential future improvements in this area. This also hugely improves the docs by adding some categorisation to ops/facts with filtering UX. Co-authored-by: Nick Mills-Barrett <[email protected]>
1 parent e1ccd30 commit 82e580b

File tree

10 files changed

+1649
-576
lines changed

10 files changed

+1649
-576
lines changed

docs/conf.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import guzzle_sphinx_theme
66

77
from pyinfra import __version__, local
8+
from pyinfra.api import metadata
89

910
copyright = "Nick Barrett {0} — pyinfra v{1}".format(
1011
date.today().year,
@@ -57,9 +58,42 @@
5758
]
5859

5960

61+
def rstjinja(app, docname, source):
62+
"""
63+
Render certain pages as a jinja templates.
64+
"""
65+
# this should only be run when building html
66+
if app.builder.format != "html":
67+
return
68+
# We currently only render docs/operations.rst
69+
# and docs/facts.rst as jinja2 templates
70+
if docname in ["operations", "facts"]:
71+
src = source[0]
72+
rendered = app.builder.templates.render_string(src, app.config.html_context)
73+
source[0] = rendered
74+
75+
6076
def setup(app):
77+
app.connect("source-read", rstjinja)
6178
this_dir = path.dirname(path.realpath(__file__))
6279
scripts_dir = path.abspath(path.join(this_dir, "..", "scripts"))
80+
metadata_file = path.abspath(path.join(this_dir, "..", "pyinfra-metadata.toml"))
81+
if not path.exists(metadata_file):
82+
raise ValueError("No pyinfra-metadata.toml in project root")
83+
with open(metadata_file, "r") as file:
84+
metadata_text = file.read()
85+
plugins = metadata.parse_plugins(metadata_text)
86+
87+
operation_plugins = sorted([p for p in plugins if p.type == "operation"], key=lambda p: p.name)
88+
fact_plugins = sorted([p for p in plugins if p.type == "fact"], key=lambda p: p.name)
89+
html_context = {
90+
"operation_plugins": operation_plugins,
91+
"fact_plugins": fact_plugins,
92+
"tags": metadata.ALLOWED_TAGS,
93+
"docs_language": language,
94+
"docs_version": version,
95+
}
96+
app.config.html_context = html_context
6397

6498
for auto_docs_name in ("operations", "facts", "apidoc", "connectors"):
6599
auto_docs_path = path.join(this_dir, auto_docs_name)

docs/connectors.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Connectors Index
22
================
33

4+
.. raw:: html
5+
6+
<nav class="under-title-tabs">
7+
See also:
8+
<a href="operations.html">Operations Index</a>
9+
<a href="facts.html">Facts Index</a>
10+
</nav>
11+
412
Connectors enable pyinfra to integrate with other tools out of the box. Connectors can do one of three things:
513

614
+ Implement how commands are executed (``@ssh``, ``@local``)

docs/facts.rst

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Facts Index
22
===========
33

4+
.. raw:: html
5+
6+
<nav class="under-title-tabs">
7+
See also:
8+
<a href="operations.html">Operations Index</a>
9+
<a href="connectors.html">Connectors Index</a>
10+
</nav>
11+
412
pyinfra uses **facts** to determine the existing state of a remote server. Operations use this information to generate commands which alter the state. Facts are read-only and are populated at the beginning of the deploy.
513

614
Facts can be executed/tested via the command line:
@@ -35,26 +43,60 @@ You can leverage facts within :doc:`operations <using-operations>` like this:
3543
3644
**Want a new fact?** Check out :doc:`the writing facts guide <./api/operations>`.
3745

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

42-
<style type="text/css">
43-
#facts-index .toctree-wrapper > ul {
44-
padding: 0;
45-
}
46-
#facts-index .toctree-wrapper > ul > li {
47-
padding: 0;
48-
list-style: none;
49-
margin: 20px 0;
50-
}
51-
#facts-index .toctree-wrapper > ul > li > ul > li {
52-
display: inline-block;
53-
}
54-
</style>
48+
<div class="container my-4">
49+
<!-- Dropdown Filter -->
50+
<div class="mb-4">
51+
<label for="tag-dropdown" class="form-label">Filter by Tag:</label>
52+
<select class="form-select" id="tag-dropdown">
53+
<option value="All">All</option>
54+
{% for tag in tags %}
55+
<option value="{{ tag.title_case }}">{{ tag.title_case }}</option>
56+
{% endfor %}
57+
</select>
58+
</div>
59+
60+
<!-- Cards Grid -->
61+
<div class="row" id="card-container">
62+
{% for plugin in fact_plugins %}
63+
<div class="col-md-4 mb-4 card-item">
64+
<div class="card h-100">
65+
<div class="card-body">
66+
<h5 class="card-title">
67+
<a href="./facts/{{ plugin.name }}.html">
68+
{{ plugin.name }}
69+
</a>
70+
<p class="card-text">{{ plugin.description }}</p>
71+
{% for tag in plugin.tags %}
72+
<span class="badge bg-secondary">{{ tag.title_case }}</span>
73+
{% endfor %}
74+
</div>
75+
</div>
76+
</div>
77+
{% endfor %}
78+
</div>
79+
</div>
80+
<script>
81+
document.getElementById('tag-dropdown').addEventListener('change', function () {
82+
const selectedTag = this.value;
83+
const cards = document.querySelectorAll('.card-item');
84+
85+
cards.forEach(card => {
86+
const tags = Array.from(card.querySelectorAll('.badge')).map(badge => badge.textContent.trim());
87+
88+
if (selectedTag === 'All' || tags.includes(selectedTag)) {
89+
card.style.display = 'block';
90+
} else {
91+
card.style.display = 'none';
92+
}
93+
});
94+
});
95+
</script>
5596

5697
.. toctree::
5798
:maxdepth: 2
5899
:glob:
100+
:hidden:
59101

60102
facts/*

docs/operations.rst

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,73 @@
11
Operations Index
22
================
33

4-
Operations are used to describe changes to make to systems in the inventory. Use them to define state and pyinfra will make any necessary changes to reach that state. All operations accept a set of :doc:`global arguments <arguments>` and are grouped as Python modules.
5-
6-
**Want a new operation?** Check out :doc:`the writing operations guide <./api/operations>`.
7-
84
.. raw:: html
95

10-
<h3>Popular operations by category</h3>
11-
12-
.. admonition:: Basics
13-
:class: note inline
14-
15-
:doc:`operations/files`, :doc:`operations/server`, :doc:`operations/git`, :doc:`operations/systemd`
16-
17-
.. admonition:: System Packages
18-
:class: note inline
19-
20-
:doc:`operations/apt`, :doc:`operations/apk`, :doc:`operations/brew`, :doc:`operations/dnf`, :doc:`operations/yum`
6+
<nav class="under-title-tabs">
7+
See also:
8+
<a href="facts.html">Facts Index</a>
9+
<a href="connectors.html">Connectors Index</a>
10+
</nav>
2111

22-
.. admonition:: Language Packages
23-
:class: note inline
24-
25-
:doc:`operations/gem`, :doc:`operations/npm`, :doc:`operations/pip`
26-
27-
.. admonition:: Databases
28-
:class: note inline
29-
30-
:doc:`operations/postgresql`, :doc:`operations/mysql`
31-
32-
.. raw:: html
12+
Operations are used to describe changes to make to systems in the inventory. Use them to define state and pyinfra will make any necessary changes to reach that state. All operations accept a set of :doc:`global arguments <arguments>` and are grouped as Python modules.
3313

34-
<h3>All operations alphabetically</h3>
14+
**Want a new operation?** Check out :doc:`the writing operations guide <./api/operations>`.
3515

3616
.. raw:: html
3717

38-
<style type="text/css">
39-
#operations-index .toctree-wrapper > ul {
40-
padding: 0;
41-
}
42-
#operations-index .toctree-wrapper > ul > li {
43-
padding: 0;
44-
list-style: none;
45-
margin: 20px 0;
46-
}
47-
#operations-index .toctree-wrapper > ul > li > ul > li {
48-
display: inline-block;
49-
}
50-
</style>
18+
<div class="container my-4">
19+
<!-- Dropdown Filter -->
20+
<div class="mb-4">
21+
<label for="tag-dropdown" class="form-label">Filter by Tag:</label>
22+
<select class="form-select" id="tag-dropdown">
23+
<option value="All">All</option>
24+
{% for tag in tags %}
25+
<option value="{{ tag.title_case }}">{{ tag.title_case }}</option>
26+
{% endfor %}
27+
</select>
28+
</div>
29+
30+
<!-- Cards Grid -->
31+
<div class="row" id="card-container">
32+
{% for plugin in operation_plugins %}
33+
<div class="col-md-4 mb-4 card-item">
34+
<div class="card h-100">
35+
<div class="card-body">
36+
<h5 class="card-title">
37+
<a href="./operations/{{ plugin.name }}.html">
38+
{{ plugin.name }}
39+
</a>
40+
</h5>
41+
<p class="card-text">{{ plugin.description }}</p>
42+
{% for tag in plugin.tags %}
43+
<span class="badge bg-secondary">{{ tag.title_case }}</span>
44+
{% endfor %}
45+
</div>
46+
</div>
47+
</div>
48+
{% endfor %}
49+
</div>
50+
</div>
51+
<script>
52+
document.getElementById('tag-dropdown').addEventListener('change', function () {
53+
const selectedTag = this.value;
54+
const cards = document.querySelectorAll('.card-item');
55+
56+
cards.forEach(card => {
57+
const tags = Array.from(card.querySelectorAll('.badge')).map(badge => badge.textContent.trim());
58+
59+
if (selectedTag === 'All' || tags.includes(selectedTag)) {
60+
card.style.display = 'block';
61+
} else {
62+
card.style.display = 'none';
63+
}
64+
});
65+
});
66+
</script>
5167

5268
.. toctree::
5369
:maxdepth: 2
5470
:glob:
71+
:hidden:
5572

5673
operations/*

pyinfra-metadata-schema-1.0.0.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "pyinfra-metadata Configuration",
4+
"type": "object",
5+
"properties": {
6+
"$schema": {
7+
"type": "string"
8+
},
9+
"pyinfra": {
10+
"type": "object",
11+
"properties": {
12+
"plugins": {
13+
"type": "object",
14+
"minProperties": 1,
15+
"patternProperties": {
16+
"^.*$": {
17+
"type": "object",
18+
"properties": {
19+
"name": {
20+
"type": "string"
21+
},
22+
"type": {
23+
"type": "string",
24+
"enum": [
25+
"operation",
26+
"fact",
27+
"connector"
28+
]
29+
},
30+
"path": {
31+
"type": "string"
32+
},
33+
"tags": {
34+
"type": "array",
35+
"items": {
36+
"type": "string"
37+
}
38+
}
39+
},
40+
"required": [
41+
"name",
42+
"type",
43+
"path",
44+
"tags"
45+
],
46+
"additionalProperties": false
47+
}
48+
}
49+
}},
50+
"required": [
51+
"plugins"
52+
],
53+
"additionalProperties": false
54+
}
55+
},
56+
"required": ["pyinfra"]
57+
}

0 commit comments

Comments
 (0)